Compare commits
60 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7117f00480 | ||
|
|
d4f37343a2 | ||
|
|
71e15e9cab | ||
|
|
eadf00feba | ||
|
|
95f28fad2b | ||
|
|
9753137a72 | ||
|
|
e4f7436dfb | ||
|
|
d39211310d | ||
|
|
0a6fb3ec0a | ||
|
|
5232cf7cec | ||
|
|
6e16ffe05f | ||
|
|
2d6895aeea | ||
|
|
b5311e1448 | ||
|
|
cc63eb383d | ||
|
|
01736ca685 | ||
|
|
be47bb7263 | ||
|
|
bcb7d88ed7 | ||
|
|
cf58c1b4b5 | ||
|
|
70c57928e7 | ||
|
|
c8219b29c0 | ||
|
|
15a9f80430 | ||
|
|
0684dfe869 | ||
|
|
83a89566ac | ||
|
|
04f486b003 | ||
|
|
f135c92434 | ||
|
|
78b095d01a | ||
|
|
1b8bd494e2 | ||
|
|
481925ac78 | ||
|
|
4854b2b1c0 | ||
|
|
2d7b33459e | ||
|
|
27e0c7421b | ||
|
|
b26c3d050c | ||
|
|
439370e25a | ||
|
|
1be4f6e20c | ||
|
|
2714c7cce9 | ||
|
|
952935de23 | ||
|
|
bdb8b5ea39 | ||
|
|
3ad4e28a2c | ||
|
|
48d0d068d1 | ||
|
|
56e166d61a | ||
|
|
672d753adf | ||
|
|
0d9ba92db4 | ||
|
|
8cf25d3602 | ||
|
|
a6bc44dc10 | ||
|
|
408d66ee74 | ||
|
|
b136bb74b8 | ||
|
|
18b2b6f447 | ||
|
|
490d1775a2 | ||
|
|
458de2d2e0 | ||
|
|
51ae3fc62f | ||
|
|
58c5c55d09 | ||
|
|
4c2bcb9e6b | ||
|
|
498379bb7e | ||
|
|
e7f3b115a4 | ||
|
|
0ebfe85d8e | ||
|
|
8e29a990cb | ||
|
|
a960ccd786 | ||
|
|
6b86e836d7 | ||
|
|
fb35b9b10a | ||
|
|
a45773e1ca |
@@ -1,4 +1,4 @@
|
||||
[] (https://ci.appveyor.com/project/bitwarden/mobile)
|
||||
[](https://ci.appveyor.com/project/bitwarden/mobile)
|
||||
[](https://gitter.im/bitwarden/Lobby)
|
||||
|
||||
# bitwarden mobile
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<GenerateSerializationAssemblies>Off</GenerateSerializationAssemblies>
|
||||
<AndroidManifest>Properties\AndroidManifest.xml</AndroidManifest>
|
||||
<AndroidUseLatestPlatformSdk>True</AndroidUseLatestPlatformSdk>
|
||||
<TargetFrameworkVersion>v6.0</TargetFrameworkVersion>
|
||||
<TargetFrameworkVersion>v7.1</TargetFrameworkVersion>
|
||||
<AndroidSupportedAbis>armeabi,armeabi-v7a,x86</AndroidSupportedAbis>
|
||||
<AndroidStoreUncompressedFileExtensions />
|
||||
<MandroidI18n />
|
||||
@@ -76,12 +76,10 @@
|
||||
<Private>True</Private>
|
||||
</Reference>
|
||||
<Reference Include="Acr.UserDialogs, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\Acr.UserDialogs.6.3.3\lib\MonoAndroid10\Acr.UserDialogs.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
<HintPath>..\..\packages\Acr.UserDialogs.6.3.10\lib\MonoAndroid10\Acr.UserDialogs.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Acr.UserDialogs.Interface, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\Acr.UserDialogs.6.3.3\lib\MonoAndroid10\Acr.UserDialogs.Interface.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
<HintPath>..\..\packages\Acr.UserDialogs.6.3.10\lib\MonoAndroid10\Acr.UserDialogs.Interface.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="AndHUD, Version=1.2.0.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\AndHUD.1.2.0\lib\MonoAndroid\AndHUD.dll</HintPath>
|
||||
@@ -92,32 +90,25 @@
|
||||
<Private>True</Private>
|
||||
</Reference>
|
||||
<Reference Include="FFImageLoading, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\Xamarin.FFImageLoading.2.2.8\lib\MonoAndroid10\FFImageLoading.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
<HintPath>..\..\packages\Xamarin.FFImageLoading.2.2.9\lib\MonoAndroid10\FFImageLoading.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="FFImageLoading.Forms, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\Xamarin.FFImageLoading.Forms.2.2.8\lib\MonoAndroid10\FFImageLoading.Forms.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
<HintPath>..\..\packages\Xamarin.FFImageLoading.Forms.2.2.9\lib\MonoAndroid10\FFImageLoading.Forms.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="FFImageLoading.Forms.Droid, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\Xamarin.FFImageLoading.Forms.2.2.8\lib\MonoAndroid10\FFImageLoading.Forms.Droid.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
<HintPath>..\..\packages\Xamarin.FFImageLoading.Forms.2.2.9\lib\MonoAndroid10\FFImageLoading.Forms.Droid.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="FFImageLoading.Platform, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\Xamarin.FFImageLoading.2.2.8\lib\MonoAndroid10\FFImageLoading.Platform.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
<HintPath>..\..\packages\Xamarin.FFImageLoading.2.2.9\lib\MonoAndroid10\FFImageLoading.Platform.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="FormsViewGroup, Version=2.0.0.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\Xamarin.Forms.2.3.3.180\lib\MonoAndroid10\FormsViewGroup.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
<HintPath>..\..\packages\Xamarin.Forms.2.3.4.231\lib\MonoAndroid10\FormsViewGroup.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="HockeySDK, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\HockeySDK.Xamarin.4.1.0\lib\MonoAndroid403\HockeySDK.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
<HintPath>..\..\packages\HockeySDK.Xamarin.4.1.2\lib\MonoAndroid403\HockeySDK.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="HockeySDK.AndroidBindings, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\HockeySDK.Xamarin.4.1.0\lib\MonoAndroid403\HockeySDK.AndroidBindings.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
<HintPath>..\..\packages\HockeySDK.Xamarin.4.1.2\lib\MonoAndroid403\HockeySDK.AndroidBindings.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Microsoft.Practices.ServiceLocation, Version=1.3.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\CommonServiceLocator.1.3\lib\portable-net4+sl5+netcore45+wpa81+wp8\Microsoft.Practices.ServiceLocation.dll</HintPath>
|
||||
@@ -153,13 +144,11 @@
|
||||
<HintPath>..\..\packages\PInvoke.Windows.Core.0.3.152\lib\portable-net45+win+wpa81+MonoAndroid10+xamarinios10+MonoTouch10\PInvoke.Windows.Core.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
</Reference>
|
||||
<Reference Include="Plugin.Connectivity, Version=2.2.12.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\Xam.Plugin.Connectivity.2.2.12\lib\MonoAndroid10\Plugin.Connectivity.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
<Reference Include="Plugin.Connectivity, Version=2.3.0.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\Xam.Plugin.Connectivity.2.3.0\lib\MonoAndroid10\Plugin.Connectivity.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Plugin.Connectivity.Abstractions, Version=2.2.12.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\Xam.Plugin.Connectivity.2.2.12\lib\MonoAndroid10\Plugin.Connectivity.Abstractions.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
<Reference Include="Plugin.Connectivity.Abstractions, Version=2.3.0.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\Xam.Plugin.Connectivity.2.3.0\lib\MonoAndroid10\Plugin.Connectivity.Abstractions.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Plugin.CurrentActivity, Version=1.0.1.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\Plugin.CurrentActivity.1.0.1\lib\MonoAndroid10\Plugin.CurrentActivity.dll</HintPath>
|
||||
@@ -173,13 +162,11 @@
|
||||
<HintPath>..\..\packages\Plugin.Fingerprint.1.2.0\lib\MonoAndroid\Plugin.Fingerprint.Abstractions.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
</Reference>
|
||||
<Reference Include="Plugin.Settings, Version=2.5.1.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\Xam.Plugins.Settings.2.5.1.0\lib\MonoAndroid10\Plugin.Settings.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
<Reference Include="Plugin.Settings, Version=2.5.4.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\Xam.Plugins.Settings.2.5.4\lib\MonoAndroid10\Plugin.Settings.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Plugin.Settings.Abstractions, Version=2.5.1.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\Xam.Plugins.Settings.2.5.1.0\lib\MonoAndroid10\Plugin.Settings.Abstractions.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
<Reference Include="Plugin.Settings.Abstractions, Version=2.5.4.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\Xam.Plugins.Settings.2.5.4\lib\MonoAndroid10\Plugin.Settings.Abstractions.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="PushNotification.Plugin, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\Xam.Plugin.PushNotification.1.2.4\lib\MonoAndroid10\PushNotification.Plugin.dll</HintPath>
|
||||
@@ -269,20 +256,16 @@
|
||||
<Private>True</Private>
|
||||
</Reference>
|
||||
<Reference Include="Xamarin.Forms.Core, Version=2.0.0.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\Xamarin.Forms.2.3.3.180\lib\MonoAndroid10\Xamarin.Forms.Core.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
<HintPath>..\..\packages\Xamarin.Forms.2.3.4.231\lib\MonoAndroid10\Xamarin.Forms.Core.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Xamarin.Forms.Platform, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\Xamarin.Forms.2.3.3.180\lib\MonoAndroid10\Xamarin.Forms.Platform.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
<HintPath>..\..\packages\Xamarin.Forms.2.3.4.231\lib\MonoAndroid10\Xamarin.Forms.Platform.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Xamarin.Forms.Platform.Android, Version=2.0.0.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\Xamarin.Forms.2.3.3.180\lib\MonoAndroid10\Xamarin.Forms.Platform.Android.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
<HintPath>..\..\packages\Xamarin.Forms.2.3.4.231\lib\MonoAndroid10\Xamarin.Forms.Platform.Android.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Xamarin.Forms.Xaml, Version=2.0.0.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\Xamarin.Forms.2.3.3.180\lib\MonoAndroid10\Xamarin.Forms.Xaml.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
<HintPath>..\..\packages\Xamarin.Forms.2.3.4.231\lib\MonoAndroid10\Xamarin.Forms.Xaml.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Xamarin.GooglePlayServices.Analytics, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\Xamarin.GooglePlayServices.Analytics.29.0.0.2\lib\MonoAndroid41\Xamarin.GooglePlayServices.Analytics.dll</HintPath>
|
||||
@@ -321,7 +304,6 @@
|
||||
<Compile Include="Controls\ExtendedButtonRenderer.cs" />
|
||||
<Compile Include="Controls\ExtendedTabbedPageRenderer.cs" />
|
||||
<Compile Include="Controls\ExtendedTableViewRenderer.cs" />
|
||||
<Compile Include="CustomAndroidClientHandler.cs" />
|
||||
<Compile Include="HockeyAppCrashManagerListener.cs" />
|
||||
<Compile Include="AutofillService.cs" />
|
||||
<Compile Include="Controls\ExtendedEditorRenderer.cs" />
|
||||
@@ -762,9 +744,6 @@
|
||||
<ItemGroup>
|
||||
<AndroidResource Include="Resources\drawable\icon.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AndroidEnvironment Include="EnvironmentVariables.txt" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AndroidResource Include="Resources\drawable\notification_sm.png" />
|
||||
</ItemGroup>
|
||||
@@ -846,6 +825,36 @@
|
||||
<ItemGroup>
|
||||
<AndroidResource Include="Resources\drawable-xxxhdpi\search.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AndroidResource Include="Resources\drawable\share.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AndroidResource Include="Resources\drawable-hdpi\share.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AndroidResource Include="Resources\drawable-xhdpi\share.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AndroidResource Include="Resources\drawable-xxhdpi\share.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AndroidResource Include="Resources\drawable-xxxhdpi\share.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AndroidResource Include="Resources\drawable\share_tools.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AndroidResource Include="Resources\drawable-hdpi\share_tools.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AndroidResource Include="Resources\drawable-xhdpi\share_tools.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AndroidResource Include="Resources\drawable-xxhdpi\share_tools.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AndroidResource Include="Resources\drawable-xxxhdpi\share_tools.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">
|
||||
@@ -854,10 +863,10 @@
|
||||
</PropertyGroup>
|
||||
<Error Condition="!Exists('..\..\packages\Xamarin.Android.Support.Vector.Drawable.23.3.0\build\Xamarin.Android.Support.Vector.Drawable.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\Xamarin.Android.Support.Vector.Drawable.23.3.0\build\Xamarin.Android.Support.Vector.Drawable.targets'))" />
|
||||
<Error Condition="!Exists('..\..\packages\Xamarin.GooglePlayServices.Basement.29.0.0.2\build\Xamarin.GooglePlayServices.Basement.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\Xamarin.GooglePlayServices.Basement.29.0.0.2\build\Xamarin.GooglePlayServices.Basement.targets'))" />
|
||||
<Error Condition="!Exists('..\..\packages\Xamarin.Forms.2.3.3.180\build\portable-win+net45+wp80+win81+wpa81+MonoAndroid10+Xamarin.iOS10+xamarinmac20\Xamarin.Forms.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\Xamarin.Forms.2.3.3.180\build\portable-win+net45+wp80+win81+wpa81+MonoAndroid10+Xamarin.iOS10+xamarinmac20\Xamarin.Forms.targets'))" />
|
||||
<Error Condition="!Exists('..\..\packages\Xamarin.Forms.2.3.4.231\build\portable-win+net45+wp80+win81+wpa81+MonoAndroid10+Xamarin.iOS10+xamarinmac20\Xamarin.Forms.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\Xamarin.Forms.2.3.4.231\build\portable-win+net45+wp80+win81+wpa81+MonoAndroid10+Xamarin.iOS10+xamarinmac20\Xamarin.Forms.targets'))" />
|
||||
</Target>
|
||||
<Import Project="..\..\packages\Xamarin.GooglePlayServices.Basement.29.0.0.2\build\Xamarin.GooglePlayServices.Basement.targets" Condition="Exists('..\..\packages\Xamarin.GooglePlayServices.Basement.29.0.0.2\build\Xamarin.GooglePlayServices.Basement.targets')" />
|
||||
<Import Project="..\..\packages\Xamarin.Forms.2.3.3.180\build\portable-win+net45+wp80+win81+wpa81+MonoAndroid10+Xamarin.iOS10+xamarinmac20\Xamarin.Forms.targets" Condition="Exists('..\..\packages\Xamarin.Forms.2.3.3.180\build\portable-win+net45+wp80+win81+wpa81+MonoAndroid10+Xamarin.iOS10+xamarinmac20\Xamarin.Forms.targets')" />
|
||||
<Import Project="..\..\packages\Xamarin.Forms.2.3.4.231\build\portable-win+net45+wp80+win81+wpa81+MonoAndroid10+Xamarin.iOS10+xamarinmac20\Xamarin.Forms.targets" Condition="Exists('..\..\packages\Xamarin.Forms.2.3.4.231\build\portable-win+net45+wp80+win81+wpa81+MonoAndroid10+Xamarin.iOS10+xamarinmac20\Xamarin.Forms.targets')" />
|
||||
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
|
||||
Other similar extension points exist, see Microsoft.Common.targets.
|
||||
<Target Name="BeforeBuild">
|
||||
|
||||
@@ -63,9 +63,11 @@ 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 notificationManager = (NotificationManager)GetSystemService(NotificationService);
|
||||
switch(e.EventType)
|
||||
|
||||
@@ -1,774 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Android.Runtime;
|
||||
using Java.IO;
|
||||
using Java.Net;
|
||||
using Java.Security;
|
||||
using Java.Security.Cert;
|
||||
using Javax.Net.Ssl;
|
||||
|
||||
namespace Xamarin.Android.Net
|
||||
{
|
||||
/// <summary>
|
||||
/// A custom implementation of <see cref="System.Net.Http.HttpClientHandler"/> which internally uses <see cref="Java.Net.HttpURLConnection"/>
|
||||
/// (or its HTTPS incarnation) to send HTTP requests.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>Instance of this class is used to configure <see cref="System.Net.Http.HttpClient"/> instance
|
||||
/// in the following way:
|
||||
///
|
||||
/// <example>
|
||||
/// var handler = new AndroidClientHandler {
|
||||
/// UseCookies = true,
|
||||
/// AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip,
|
||||
/// };
|
||||
///
|
||||
/// var httpClient = new HttpClient (handler);
|
||||
/// var response = httpClient.GetAsync ("http://example.com")?.Result as AndroidHttpResponseMessage;
|
||||
/// </example></para>
|
||||
/// <para>
|
||||
/// The class supports pre-authentication of requests albeit in a slightly "manual" way. Namely, whenever a request to a server requiring authentication
|
||||
/// is made and no authentication credentials are provided in the <see cref="PreAuthenticationData"/> property (which is usually the case on the first
|
||||
/// request), the <see cref="RequestNeedsAuthorization"/> property will return <c>true</c> and the <see cref="RequestedAuthentication"/> property will
|
||||
/// contain all the authentication information gathered from the server. The application must then fill in the blanks (i.e. the credentials) and re-send
|
||||
/// the request configured to perform pre-authentication. The reason for this manual process is that the underlying Java HTTP client API supports only a
|
||||
/// single, VM-wide, authentication handler which cannot be configured to handle credentials for several requests. AndroidClientHandler, therefore, implements
|
||||
/// the authentication in managed .NET code. Message handler supports both Basic and Digest authentication. If an authentication scheme that's not supported
|
||||
/// by AndroidClientHandler is requested by the server, the application can provide its own authentication module (<see cref="AuthenticationData"/>,
|
||||
/// <see cref="PreAuthenticationData"/>) to handle the protocol authorization.</para>
|
||||
/// <para>AndroidClientHandler also supports requests to servers with "invalid" (e.g. self-signed) SSL certificates. Since this process is a bit convoluted using
|
||||
/// the Java APIs, AndroidClientHandler defines two ways to handle the situation. First, easier, is to store the necessary certificates (either CA or server certificates)
|
||||
/// in the <see cref="TrustedCerts"/> collection or, after deriving a custom class from AndroidClientHandler, by overriding one or more methods provided for this purpose
|
||||
/// (<see cref="ConfigureTrustManagerFactory"/>, <see cref="ConfigureKeyManagerFactory"/> and <see cref="ConfigureKeyStore"/>). The former method should be sufficient
|
||||
/// for most use cases, the latter allows the application to provide fully customized key store, trust manager and key manager, if needed. Note that the instance of
|
||||
/// AndroidClientHandler configured to accept an "invalid" certificate from the particular server will most likely fail to validate certificates from other servers (even
|
||||
/// if they use a certificate with a fully validated trust chain) unless you store the CA certificates from your Android system in <see cref="TrustedCerts"/> along with
|
||||
/// the self-signed certificate(s).</para>
|
||||
/// </remarks>
|
||||
public class CustomAndroidClientHandler : HttpClientHandler
|
||||
{
|
||||
sealed class RequestRedirectionState
|
||||
{
|
||||
public Uri NewUrl;
|
||||
public int RedirectCounter;
|
||||
public HttpMethod Method;
|
||||
}
|
||||
|
||||
internal const string LOG_APP = "monodroid-net";
|
||||
|
||||
const string GZIP_ENCODING = "gzip";
|
||||
const string DEFLATE_ENCODING = "deflate";
|
||||
const string IDENTITY_ENCODING = "identity";
|
||||
|
||||
static readonly HashSet<string> known_content_headers = new HashSet<string>(StringComparer.OrdinalIgnoreCase) {
|
||||
"Allow",
|
||||
"Content-Disposition",
|
||||
"Content-Encoding",
|
||||
"Content-Language",
|
||||
"Content-Length",
|
||||
"Content-Location",
|
||||
"Content-MD5",
|
||||
"Content-Range",
|
||||
"Content-Type",
|
||||
"Expires",
|
||||
"Last-Modified"
|
||||
};
|
||||
|
||||
static readonly List<IAndroidAuthenticationModule> authModules = new List<IAndroidAuthenticationModule> {
|
||||
new AuthModuleBasic (),
|
||||
// COMMENTED OUT: Kyle
|
||||
//new AuthModuleDigest ()
|
||||
};
|
||||
|
||||
bool disposed;
|
||||
|
||||
// Now all hail Java developers! Get this... HttpURLClient defaults to accepting AND
|
||||
// uncompressing the gzip content encoding UNLESS you set the Accept-Encoding header to ANY
|
||||
// value. So if we set it to 'gzip' below we WILL get gzipped stream but HttpURLClient will NOT
|
||||
// uncompress it any longer, doh. And they don't support 'deflate' so we need to handle it ourselves.
|
||||
bool decompress_here;
|
||||
|
||||
/// <summary>
|
||||
/// <para>
|
||||
/// Gets or sets the pre authentication data for the request. This property must be set by the application
|
||||
/// before the request is made. Generally the value can be taken from <see cref="RequestedAuthentication"/>
|
||||
/// after the initial request, without any authentication data, receives the authorization request from the
|
||||
/// server. The application must then store credentials in instance of <see cref="AuthenticationData"/> and
|
||||
/// assign the instance to this propery before retrying the request.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The property is never set by AndroidClientHandler.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <value>The pre authentication data.</value>
|
||||
public AuthenticationData PreAuthenticationData { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If the website requires authentication, this property will contain data about each scheme supported
|
||||
/// by the server after the response. Note that unauthorized request will return a valid response - you
|
||||
/// need to check the status code and and (re)configure AndroidClientHandler instance accordingly by providing
|
||||
/// both the credentials and the authentication scheme by setting the <see cref="PreAuthenticationData"/>
|
||||
/// property. If AndroidClientHandler is not able to detect the kind of authentication scheme it will store an
|
||||
/// instance of <see cref="AuthenticationData"/> with its <see cref="AuthenticationData.Scheme"/> property
|
||||
/// set to <c>AuthenticationScheme.Unsupported</c> and the application will be responsible for providing an
|
||||
/// instance of <see cref="IAndroidAuthenticationModule"/> which handles this kind of authorization scheme
|
||||
/// (<see cref="AuthenticationData.AuthModule"/>
|
||||
/// </summary>
|
||||
public IList<AuthenticationData> RequestedAuthentication { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Server authentication response indicates that the request to authorize comes from a proxy if this property is <c>true</c>.
|
||||
/// All the instances of <see cref="AuthenticationData"/> stored in the <see cref="RequestedAuthentication"/> property will
|
||||
/// have their <see cref="AuthenticationData.UseProxyAuthentication"/> preset to the same value as this property.
|
||||
/// </summary>
|
||||
public bool ProxyAuthenticationRequested { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// If <c>true</c> then the server requested authorization and the application must use information
|
||||
/// found in <see cref="RequestedAuthentication"/> to set the value of <see cref="PreAuthenticationData"/>
|
||||
/// </summary>
|
||||
public bool RequestNeedsAuthorization
|
||||
{
|
||||
get { return RequestedAuthentication?.Count > 0; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <para>
|
||||
/// If the request is to the server protected with a self-signed (or otherwise untrusted) SSL certificate, the request will
|
||||
/// fail security chain verification unless the application provides either the CA certificate of the entity which issued the
|
||||
/// server's certificate or, alternatively, provides the server public key. Whichever the case, the certificate(s) must be stored
|
||||
/// in this property in order for AndroidClientHandler to configure the request to accept the server certificate.</para>
|
||||
/// <para>AndroidClientHandler uses a custom <see cref="KeyStore"/> and <see cref="TrustManagerFactory"/> to configure the connection.
|
||||
/// If, however, the application requires finer control over the SSL configuration (e.g. it implements its own TrustManager) then
|
||||
/// it should leave this property empty and instead derive a custom class from AndroidClientHandler and override, as needed, the
|
||||
/// <see cref="ConfigureTrustManagerFactory"/>, <see cref="ConfigureKeyManagerFactory"/> and <see cref="ConfigureKeyStore"/> methods
|
||||
/// instead</para>
|
||||
/// </summary>
|
||||
/// <value>The trusted certs.</value>
|
||||
public IList<Certificate> TrustedCerts { get; set; }
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
disposed = true;
|
||||
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
protected void AssertSelf()
|
||||
{
|
||||
if(!disposed)
|
||||
return;
|
||||
throw new ObjectDisposedException(nameof(AndroidClientHandler));
|
||||
}
|
||||
|
||||
string EncodeUrl(Uri url)
|
||||
{
|
||||
if(url == null)
|
||||
return String.Empty;
|
||||
|
||||
if(String.IsNullOrEmpty(url.Query))
|
||||
return Uri.EscapeUriString(url.ToString());
|
||||
|
||||
// UriBuilder takes care of encoding everything properly
|
||||
var bldr = new UriBuilder(url);
|
||||
if(url.IsDefaultPort)
|
||||
bldr.Port = -1; // Avoids adding :80 or :443 to the host name in the result
|
||||
|
||||
// bldr.Uri.ToString () would ruin the good job UriBuilder did
|
||||
return bldr.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates, configures and processes an asynchronous request to the indicated resource.
|
||||
/// </summary>
|
||||
/// <returns>Task in which the request is executed</returns>
|
||||
/// <param name="request">Request provided by <see cref="System.Net.Http.HttpClient"/></param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
AssertSelf();
|
||||
if(request == null)
|
||||
throw new ArgumentNullException(nameof(request));
|
||||
|
||||
if(!request.RequestUri.IsAbsoluteUri)
|
||||
throw new ArgumentException("Must represent an absolute URI", "request");
|
||||
|
||||
var redirectState = new RequestRedirectionState
|
||||
{
|
||||
NewUrl = request.RequestUri,
|
||||
RedirectCounter = 0,
|
||||
Method = request.Method
|
||||
};
|
||||
while(true)
|
||||
{
|
||||
URL java_url = new URL(EncodeUrl(redirectState.NewUrl));
|
||||
URLConnection java_connection = java_url.OpenConnection();
|
||||
HttpURLConnection httpConnection = await SetupRequestInternal(request, java_connection);
|
||||
HttpResponseMessage response = await ProcessRequest(request, java_url, httpConnection, cancellationToken, redirectState);
|
||||
if(response != null)
|
||||
return response;
|
||||
|
||||
if(redirectState.NewUrl == null)
|
||||
throw new InvalidOperationException("Request redirected but no new URI specified");
|
||||
request.Method = redirectState.Method;
|
||||
}
|
||||
}
|
||||
|
||||
Task<HttpResponseMessage> ProcessRequest(HttpRequestMessage request, URL javaUrl, HttpURLConnection httpConnection, CancellationToken cancellationToken, RequestRedirectionState redirectState)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
httpConnection.InstanceFollowRedirects = false; // We handle it ourselves
|
||||
RequestedAuthentication = null;
|
||||
ProxyAuthenticationRequested = false;
|
||||
|
||||
return DoProcessRequest(request, javaUrl, httpConnection, cancellationToken, redirectState);
|
||||
}
|
||||
|
||||
async Task<HttpResponseMessage> DoProcessRequest(HttpRequestMessage request, URL javaUrl, HttpURLConnection httpConnection, CancellationToken cancellationToken, RequestRedirectionState redirectState)
|
||||
{
|
||||
if(cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await Task.WhenAny(
|
||||
httpConnection.ConnectAsync(),
|
||||
Task.Run(() => { cancellationToken.WaitHandle.WaitOne(); }))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
catch(Java.Net.ConnectException ex)
|
||||
{
|
||||
// Wrap it nicely in a "standard" exception so that it's compatible with HttpClientHandler
|
||||
throw new WebException(ex.Message, ex, WebExceptionStatus.ConnectFailure, null);
|
||||
}
|
||||
|
||||
if(cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
httpConnection.Disconnect();
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
}
|
||||
cancellationToken.Register(httpConnection.Disconnect);
|
||||
|
||||
if(httpConnection.DoOutput)
|
||||
{
|
||||
using(var stream = await request.Content.ReadAsStreamAsync())
|
||||
{
|
||||
await stream.CopyToAsync(httpConnection.OutputStream, 4096, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
if(cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
httpConnection.Disconnect();
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
}
|
||||
|
||||
var statusCode = await Task.Run(() => (HttpStatusCode)httpConnection.ResponseCode).ConfigureAwait(false);
|
||||
var connectionUri = new Uri(httpConnection.URL.ToString());
|
||||
|
||||
// If the request was redirected we need to put the new URL in the request
|
||||
request.RequestUri = connectionUri;
|
||||
var ret = new AndroidHttpResponseMessage(javaUrl, httpConnection)
|
||||
{
|
||||
RequestMessage = request,
|
||||
ReasonPhrase = httpConnection.ResponseMessage,
|
||||
StatusCode = statusCode,
|
||||
};
|
||||
|
||||
bool disposeRet;
|
||||
if(HandleRedirect(statusCode, httpConnection, redirectState, out disposeRet))
|
||||
{
|
||||
if(disposeRet)
|
||||
{
|
||||
ret.Dispose();
|
||||
ret = null;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
switch(statusCode)
|
||||
{
|
||||
case HttpStatusCode.Unauthorized:
|
||||
case HttpStatusCode.ProxyAuthenticationRequired:
|
||||
// We don't resend the request since that would require new set of credentials if the
|
||||
// ones provided in Credentials are invalid (or null) and that, in turn, may require asking the
|
||||
// user which is not something that should be taken care of by us and in this
|
||||
// context. The application should be responsible for this.
|
||||
// HttpClientHandler throws an exception in this instance, but I think it's not a good
|
||||
// idea. We'll return the response message with all the information required by the
|
||||
// application to fill in the blanks and provide the requested credentials instead.
|
||||
//
|
||||
// We return the body of the response too, but the Java client will throw
|
||||
// a FileNotFound exception if we attempt to access the input stream.
|
||||
// Instead we try to read the error stream and return an default message if the error stream isn't readable.
|
||||
ret.Content = GetErrorContent(httpConnection, new StringContent("Unauthorized", Encoding.ASCII));
|
||||
CopyHeaders(httpConnection, ret);
|
||||
|
||||
if(ret.Headers.WwwAuthenticate != null)
|
||||
{
|
||||
ProxyAuthenticationRequested = false;
|
||||
CollectAuthInfo(ret.Headers.WwwAuthenticate);
|
||||
}
|
||||
else if(ret.Headers.ProxyAuthenticate != null)
|
||||
{
|
||||
ProxyAuthenticationRequested = true;
|
||||
CollectAuthInfo(ret.Headers.ProxyAuthenticate);
|
||||
}
|
||||
|
||||
// COMMENTED OUT: Kyle
|
||||
//ret.RequestedAuthentication = RequestedAuthentication;
|
||||
return ret;
|
||||
}
|
||||
|
||||
if(!IsErrorStatusCode(statusCode))
|
||||
{
|
||||
ret.Content = GetContent(httpConnection, httpConnection.InputStream);
|
||||
}
|
||||
else
|
||||
{
|
||||
// For 400 >= response code <= 599 the Java client throws the FileNotFound exception when attempting to read from the input stream.
|
||||
// Instead we try to read the error stream and return an empty string if the error stream isn't readable.
|
||||
ret.Content = GetErrorContent(httpConnection, new StringContent(String.Empty, Encoding.ASCII));
|
||||
}
|
||||
|
||||
CopyHeaders(httpConnection, ret);
|
||||
|
||||
IEnumerable<string> cookieHeaderValue;
|
||||
if(!UseCookies || CookieContainer == null || !ret.Headers.TryGetValues("Set-Cookie", out cookieHeaderValue) || cookieHeaderValue == null)
|
||||
{
|
||||
return ret;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
CookieContainer.SetCookies(connectionUri, String.Join(",", cookieHeaderValue));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
// We don't want to terminate the response because of a bad cookie, hence just reporting
|
||||
// the issue. We might consider adding a virtual method to let the user handle the
|
||||
// issue, but not sure if it's really needed. Set-Cookie header will be part of the
|
||||
// header collection so the user can always examine it if they spot an error.
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
HttpContent GetErrorContent(HttpURLConnection httpConnection, HttpContent fallbackContent)
|
||||
{
|
||||
var contentStream = httpConnection.ErrorStream;
|
||||
|
||||
if(contentStream != null)
|
||||
{
|
||||
return GetContent(httpConnection, contentStream);
|
||||
}
|
||||
|
||||
return fallbackContent;
|
||||
}
|
||||
|
||||
HttpContent GetContent(URLConnection httpConnection, Stream contentStream)
|
||||
{
|
||||
Stream inputStream = new BufferedStream(contentStream);
|
||||
if(decompress_here)
|
||||
{
|
||||
string[] encodings = httpConnection.ContentEncoding?.Split(',');
|
||||
if(encodings != null)
|
||||
{
|
||||
if(encodings.Contains(GZIP_ENCODING, StringComparer.OrdinalIgnoreCase))
|
||||
inputStream = new GZipStream(inputStream, CompressionMode.Decompress);
|
||||
else if(encodings.Contains(DEFLATE_ENCODING, StringComparer.OrdinalIgnoreCase))
|
||||
inputStream = new DeflateStream(inputStream, CompressionMode.Decompress);
|
||||
}
|
||||
}
|
||||
return new StreamContent(inputStream);
|
||||
}
|
||||
|
||||
bool HandleRedirect(HttpStatusCode redirectCode, HttpURLConnection httpConnection, RequestRedirectionState redirectState, out bool disposeRet)
|
||||
{
|
||||
if(!AllowAutoRedirect)
|
||||
{
|
||||
disposeRet = false;
|
||||
return true; // We shouldn't follow and there's no data to fetch, just return
|
||||
}
|
||||
disposeRet = true;
|
||||
|
||||
redirectState.NewUrl = null;
|
||||
switch(redirectCode)
|
||||
{
|
||||
case HttpStatusCode.MultipleChoices: // 300
|
||||
break;
|
||||
|
||||
case HttpStatusCode.Moved: // 301
|
||||
case HttpStatusCode.Redirect: // 302
|
||||
case HttpStatusCode.SeeOther: // 303
|
||||
redirectState.Method = HttpMethod.Get;
|
||||
break;
|
||||
|
||||
case HttpStatusCode.TemporaryRedirect: // 307
|
||||
break;
|
||||
|
||||
default:
|
||||
if((int)redirectCode >= 300 && (int)redirectCode < 400)
|
||||
throw new InvalidOperationException($"HTTP Redirection status code {redirectCode} ({(int)redirectCode}) not supported");
|
||||
return false;
|
||||
}
|
||||
|
||||
IDictionary<string, IList<string>> headers = httpConnection.HeaderFields;
|
||||
IList<string> locationHeader;
|
||||
if(!headers.TryGetValue("Location", out locationHeader) || locationHeader == null || locationHeader.Count == 0)
|
||||
throw new InvalidOperationException($"HTTP connection redirected with code {redirectCode} ({(int)redirectCode}) but no Location header found in response");
|
||||
|
||||
|
||||
redirectState.RedirectCounter++;
|
||||
if(redirectState.RedirectCounter >= MaxAutomaticRedirections)
|
||||
throw new WebException($"Maximum automatic redirections exceeded (allowed {MaxAutomaticRedirections}, redirected {redirectState.RedirectCounter} times)");
|
||||
|
||||
Uri location = new Uri(locationHeader[0], UriKind.Absolute);
|
||||
redirectState.NewUrl = location;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool IsErrorStatusCode(HttpStatusCode statusCode)
|
||||
{
|
||||
return (int)statusCode >= 400 && (int)statusCode <= 599;
|
||||
}
|
||||
|
||||
void CollectAuthInfo(HttpHeaderValueCollection<AuthenticationHeaderValue> headers)
|
||||
{
|
||||
var authData = new List<AuthenticationData>(headers.Count);
|
||||
|
||||
foreach(AuthenticationHeaderValue ahv in headers)
|
||||
{
|
||||
var data = new AuthenticationData
|
||||
{
|
||||
Scheme = GetAuthScheme(ahv.Scheme),
|
||||
// COMMENTED OUT: Kyle
|
||||
//Challenge = $"{ahv.Scheme} {ahv.Parameter}",
|
||||
UseProxyAuthentication = ProxyAuthenticationRequested
|
||||
};
|
||||
authData.Add(data);
|
||||
}
|
||||
|
||||
RequestedAuthentication = authData.AsReadOnly();
|
||||
}
|
||||
|
||||
AuthenticationScheme GetAuthScheme(string scheme)
|
||||
{
|
||||
if(String.Compare("basic", scheme, StringComparison.OrdinalIgnoreCase) == 0)
|
||||
return AuthenticationScheme.Basic;
|
||||
if(String.Compare("digest", scheme, StringComparison.OrdinalIgnoreCase) == 0)
|
||||
return AuthenticationScheme.Digest;
|
||||
|
||||
return AuthenticationScheme.Unsupported;
|
||||
}
|
||||
|
||||
void CopyHeaders(HttpURLConnection httpConnection, HttpResponseMessage response)
|
||||
{
|
||||
IDictionary<string, IList<string>> headers = httpConnection.HeaderFields;
|
||||
foreach(string key in headers.Keys)
|
||||
{
|
||||
if(key == null) // First header entry has null key, it corresponds to the response message
|
||||
continue;
|
||||
|
||||
HttpHeaders item_headers;
|
||||
string kind;
|
||||
if(known_content_headers.Contains(key))
|
||||
{
|
||||
kind = "content";
|
||||
item_headers = response.Content.Headers;
|
||||
}
|
||||
else
|
||||
{
|
||||
kind = "response";
|
||||
item_headers = response.Headers;
|
||||
}
|
||||
item_headers.TryAddWithoutValidation(key, headers[key]);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configure the <see cref="HttpURLConnection"/> before the request is sent. This method is meant to be overriden
|
||||
/// by applications which need to perform some extra configuration steps on the connection. It is called with all
|
||||
/// the request headers set, pre-authentication performed (if applicable) but before the request body is set
|
||||
/// (e.g. for POST requests). The default implementation in AndroidClientHandler does nothing.
|
||||
/// </summary>
|
||||
/// <param name="request">Request data</param>
|
||||
/// <param name="conn">Pre-configured connection instance</param>
|
||||
protected virtual Task SetupRequest(HttpRequestMessage request, HttpURLConnection conn)
|
||||
{
|
||||
return Task.Factory.StartNew(AssertSelf);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configures the key store. The <paramref name="keyStore"/> parameter is set to instance of <see cref="KeyStore"/>
|
||||
/// created using the <see cref="KeyStore.DefaultType"/> type and with populated with certificates provided in the <see cref="TrustedCerts"/>
|
||||
/// property. AndroidClientHandler implementation simply returns the instance passed in the <paramref name="keyStore"/> parameter
|
||||
/// </summary>
|
||||
/// <returns>The key store.</returns>
|
||||
/// <param name="keyStore">Key store to configure.</param>
|
||||
protected virtual KeyStore ConfigureKeyStore(KeyStore keyStore)
|
||||
{
|
||||
AssertSelf();
|
||||
|
||||
return keyStore;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create and configure an instance of <see cref="KeyManagerFactory"/>. The <paramref name="keyStore"/> parameter is set to the
|
||||
/// return value of the <see cref="ConfigureKeyStore"/> method, so it might be null if the application overrode the method and provided
|
||||
/// no key store. It will not be <c>null</c> when the default implementation is used. The application can return <c>null</c> here since
|
||||
/// KeyManagerFactory is not required for the custom SSL configuration, but it might be used by the application to implement a more advanced
|
||||
/// mechanism of key management.
|
||||
/// </summary>
|
||||
/// <returns>The key manager factory or <c>null</c>.</returns>
|
||||
/// <param name="keyStore">Key store.</param>
|
||||
protected virtual KeyManagerFactory ConfigureKeyManagerFactory(KeyStore keyStore)
|
||||
{
|
||||
AssertSelf();
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create and configure an instance of <see cref="TrustManagerFactory"/>. The <paramref name="keyStore"/> parameter is set to the
|
||||
/// return value of the <see cref="ConfigureKeyStore"/> method, so it might be null if the application overrode the method and provided
|
||||
/// no key store. It will not be <c>null</c> when the default implementation is used. The application can return <c>null</c> from this
|
||||
/// method in which case AndroidClientHandler will create its own instance of the trust manager factory provided that the <see cref="TrustCerts"/>
|
||||
/// list contains at least one valid certificate. If there are no valid certificates and this method returns <c>null</c>, no custom
|
||||
/// trust manager will be created since that would make all the HTTPS requests fail.
|
||||
/// </summary>
|
||||
/// <returns>The trust manager factory.</returns>
|
||||
/// <param name="keyStore">Key store.</param>
|
||||
protected virtual TrustManagerFactory ConfigureTrustManagerFactory(KeyStore keyStore)
|
||||
{
|
||||
AssertSelf();
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
void AppendEncoding(string encoding, ref List<string> list)
|
||||
{
|
||||
if(list == null)
|
||||
list = new List<string>();
|
||||
if(list.Contains(encoding))
|
||||
return;
|
||||
list.Add(encoding);
|
||||
}
|
||||
|
||||
async Task<HttpURLConnection> SetupRequestInternal(HttpRequestMessage request, URLConnection conn)
|
||||
{
|
||||
if(conn == null)
|
||||
throw new ArgumentNullException(nameof(conn));
|
||||
var httpConnection = conn.JavaCast<HttpURLConnection>();
|
||||
if(httpConnection == null)
|
||||
throw new InvalidOperationException($"Unsupported URL scheme {conn.URL.Protocol}");
|
||||
|
||||
httpConnection.RequestMethod = request.Method.ToString();
|
||||
|
||||
// SSL context must be set up as soon as possible, before adding any content or
|
||||
// headers. Otherwise Java won't use the socket factory
|
||||
SetupSSL(httpConnection as HttpsURLConnection);
|
||||
if(request.Content != null)
|
||||
AddHeaders(httpConnection, request.Content.Headers);
|
||||
AddHeaders(httpConnection, request.Headers);
|
||||
|
||||
List<string> accept_encoding = null;
|
||||
|
||||
decompress_here = false;
|
||||
if((AutomaticDecompression & DecompressionMethods.GZip) != 0)
|
||||
{
|
||||
AppendEncoding(GZIP_ENCODING, ref accept_encoding);
|
||||
decompress_here = true;
|
||||
}
|
||||
|
||||
if((AutomaticDecompression & DecompressionMethods.Deflate) != 0)
|
||||
{
|
||||
AppendEncoding(DEFLATE_ENCODING, ref accept_encoding);
|
||||
decompress_here = true;
|
||||
}
|
||||
|
||||
if(AutomaticDecompression == DecompressionMethods.None)
|
||||
{
|
||||
accept_encoding?.Clear();
|
||||
AppendEncoding(IDENTITY_ENCODING, ref accept_encoding); // Turns off compression for the Java client
|
||||
}
|
||||
|
||||
if(accept_encoding?.Count > 0)
|
||||
httpConnection.SetRequestProperty("Accept-Encoding", String.Join(",", accept_encoding));
|
||||
|
||||
if(UseCookies && CookieContainer != null)
|
||||
{
|
||||
string cookieHeaderValue = CookieContainer.GetCookieHeader(request.RequestUri);
|
||||
if(!String.IsNullOrEmpty(cookieHeaderValue))
|
||||
httpConnection.SetRequestProperty("Cookie", cookieHeaderValue);
|
||||
}
|
||||
|
||||
HandlePreAuthentication(httpConnection);
|
||||
await SetupRequest(request, httpConnection);
|
||||
SetupRequestBody(httpConnection, request);
|
||||
|
||||
return httpConnection;
|
||||
}
|
||||
|
||||
void SetupSSL(HttpsURLConnection httpsConnection)
|
||||
{
|
||||
if(httpsConnection == null)
|
||||
return;
|
||||
|
||||
KeyStore keyStore = KeyStore.GetInstance(KeyStore.DefaultType);
|
||||
keyStore.Load(null, null);
|
||||
bool gotCerts = TrustedCerts?.Count > 0;
|
||||
if(gotCerts)
|
||||
{
|
||||
for(int i = 0; i < TrustedCerts.Count; i++)
|
||||
{
|
||||
Certificate cert = TrustedCerts[i];
|
||||
if(cert == null)
|
||||
continue;
|
||||
keyStore.SetCertificateEntry($"ca{i}", cert);
|
||||
}
|
||||
}
|
||||
keyStore = ConfigureKeyStore(keyStore);
|
||||
KeyManagerFactory kmf = ConfigureKeyManagerFactory(keyStore);
|
||||
TrustManagerFactory tmf = ConfigureTrustManagerFactory(keyStore);
|
||||
|
||||
if(tmf == null)
|
||||
{
|
||||
// If there are no certs and no trust manager factory, we can't use a custom manager
|
||||
// because it will cause all the HTTPS requests to fail because of unverified trust
|
||||
// chain
|
||||
if(!gotCerts)
|
||||
return;
|
||||
|
||||
tmf = TrustManagerFactory.GetInstance(TrustManagerFactory.DefaultAlgorithm);
|
||||
tmf.Init(keyStore);
|
||||
}
|
||||
|
||||
SSLContext context = SSLContext.GetInstance("TLS");
|
||||
context.Init(kmf?.GetKeyManagers(), tmf.GetTrustManagers(), null);
|
||||
httpsConnection.SSLSocketFactory = context.SocketFactory;
|
||||
}
|
||||
|
||||
void HandlePreAuthentication(HttpURLConnection httpConnection)
|
||||
{
|
||||
AuthenticationData data = PreAuthenticationData;
|
||||
if(!PreAuthenticate || data == null)
|
||||
return;
|
||||
|
||||
ICredentials creds = data.UseProxyAuthentication ? Proxy?.Credentials : Credentials;
|
||||
if(creds == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
IAndroidAuthenticationModule auth = data.Scheme == AuthenticationScheme.Unsupported ? data.AuthModule : authModules.Find(m => m?.Scheme == data.Scheme);
|
||||
if(auth == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Authorization authorization = auth.Authenticate(data.Challenge, httpConnection, creds);
|
||||
if(authorization == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
httpConnection.SetRequestProperty(data.UseProxyAuthentication ? "Proxy-Authorization" : "Authorization", authorization.Message);
|
||||
}
|
||||
|
||||
void AddHeaders(HttpURLConnection conn, HttpHeaders headers)
|
||||
{
|
||||
if(headers == null)
|
||||
return;
|
||||
|
||||
foreach(KeyValuePair<string, IEnumerable<string>> header in headers)
|
||||
{
|
||||
conn.SetRequestProperty(header.Key, header.Value != null ? String.Join(",", header.Value) : String.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
void SetupRequestBody(HttpURLConnection httpConnection, HttpRequestMessage request)
|
||||
{
|
||||
if(request.Content == null)
|
||||
{
|
||||
// Pilfered from System.Net.Http.HttpClientHandler:SendAync
|
||||
if(HttpMethod.Post.Equals(request.Method) || HttpMethod.Put.Equals(request.Method) || HttpMethod.Delete.Equals(request.Method))
|
||||
{
|
||||
// Explicitly set this to make sure we're sending a "Content-Length: 0" header.
|
||||
// This fixes the issue that's been reported on the forums:
|
||||
// http://forums.xamarin.com/discussion/17770/length-required-error-in-http-post-since-latest-release
|
||||
httpConnection.SetRequestProperty("Content-Length", "0");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
httpConnection.DoOutput = true;
|
||||
long? contentLength = request.Content.Headers.ContentLength;
|
||||
if(contentLength != null)
|
||||
httpConnection.SetFixedLengthStreamingMode((int)contentLength);
|
||||
else
|
||||
httpConnection.SetChunkedStreamingMode(0);
|
||||
}
|
||||
}
|
||||
|
||||
sealed class AuthModuleBasic : IAndroidAuthenticationModule
|
||||
{
|
||||
public AuthenticationScheme Scheme { get; } = AuthenticationScheme.Basic;
|
||||
public string AuthenticationType { get; } = "Basic";
|
||||
public bool CanPreAuthenticate { get; } = true;
|
||||
|
||||
public Authorization Authenticate(string challenge, HttpURLConnection request, ICredentials credentials)
|
||||
{
|
||||
string header = challenge?.Trim();
|
||||
if(credentials == null || String.IsNullOrEmpty(header))
|
||||
return null;
|
||||
|
||||
if(header.IndexOf("basic", StringComparison.OrdinalIgnoreCase) == -1)
|
||||
return null;
|
||||
|
||||
return InternalAuthenticate(request, credentials);
|
||||
}
|
||||
|
||||
public Authorization PreAuthenticate(HttpURLConnection request, ICredentials credentials)
|
||||
{
|
||||
return InternalAuthenticate(request, credentials);
|
||||
}
|
||||
|
||||
Authorization InternalAuthenticate(HttpURLConnection request, ICredentials credentials)
|
||||
{
|
||||
if(request == null || credentials == null)
|
||||
return null;
|
||||
|
||||
NetworkCredential cred = credentials.GetCredential(new Uri(request.URL.ToString()), AuthenticationType.ToLowerInvariant());
|
||||
if(cred == null)
|
||||
return null;
|
||||
|
||||
if(String.IsNullOrEmpty(cred.UserName))
|
||||
return null;
|
||||
|
||||
string domain = cred.Domain?.Trim();
|
||||
string response = String.Empty;
|
||||
|
||||
// If domain is set, MS sends "domain\user:password".
|
||||
if(!String.IsNullOrEmpty(domain))
|
||||
response = domain + "\\";
|
||||
response += cred.UserName + ":" + cred.Password;
|
||||
|
||||
return new Authorization($"{AuthenticationType} {Convert.ToBase64String(Encoding.ASCII.GetBytes(response))}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
XA_HTTP_CLIENT_HANDLER_TYPE=Xamarin.Android.Net.AndroidClientHandler
|
||||
@@ -73,7 +73,8 @@ namespace Bit.Android
|
||||
Resolver.Resolve<ILockService>(),
|
||||
Resolver.Resolve<IGoogleAnalyticsService>(),
|
||||
Resolver.Resolve<ILocalizeService>(),
|
||||
Resolver.Resolve<IAppInfoService>()));
|
||||
Resolver.Resolve<IAppInfoService>(),
|
||||
Resolver.Resolve<IAppSettingsService>()));
|
||||
|
||||
MessagingCenter.Subscribe<Xamarin.Forms.Application>(Xamarin.Forms.Application.Current, "RateApp", (sender) =>
|
||||
{
|
||||
|
||||
@@ -39,9 +39,6 @@ namespace Bit.Android
|
||||
public MainApplication(IntPtr handle, JniHandleOwnership transer)
|
||||
: base(handle, transer)
|
||||
{
|
||||
// NOTE: This is just here to stop the linker from removing AndroidClientHandler references
|
||||
var handler = new AndroidClientHandler();
|
||||
|
||||
if(!Resolver.IsSet)
|
||||
{
|
||||
SetIoc(this);
|
||||
@@ -214,6 +211,7 @@ namespace Bit.Android
|
||||
.RegisterType<ITokenService, TokenService>(new ContainerControlledLifetimeManager())
|
||||
.RegisterType<ISettingsService, SettingsService>(new ContainerControlledLifetimeManager())
|
||||
.RegisterType<IMemoryService, MemoryService>(new ContainerControlledLifetimeManager())
|
||||
.RegisterType<IAppSettingsService, AppSettingsService>(new ContainerControlledLifetimeManager())
|
||||
// Repositories
|
||||
.RegisterType<IFolderRepository, FolderRepository>(new ContainerControlledLifetimeManager())
|
||||
.RegisterType<IFolderApiRepository, FolderApiRepository>(new ContainerControlledLifetimeManager())
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.x8bit.bitwarden" android:versionName="1.4.4" android:installLocation="auto" android:versionCode="502">
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.x8bit.bitwarden" android:versionName="1.5.0" 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" />
|
||||
|
||||
24
src/Android/Resources/Resource.Designer.cs
generated
@@ -2742,8 +2742,8 @@ namespace Bit.Android
|
||||
// aapt resource value: 0x7f0200e4
|
||||
public const int notification_sm = 2130837732;
|
||||
|
||||
// aapt resource value: 0x7f0200f1
|
||||
public const int notification_template_icon_bg = 2130837745;
|
||||
// aapt resource value: 0x7f0200f3
|
||||
public const int notification_template_icon_bg = 2130837747;
|
||||
|
||||
// aapt resource value: 0x7f0200e5
|
||||
public const int plus = 2130837733;
|
||||
@@ -2761,25 +2761,31 @@ namespace Bit.Android
|
||||
public const int search = 2130837737;
|
||||
|
||||
// aapt resource value: 0x7f0200ea
|
||||
public const int splash_screen = 2130837738;
|
||||
public const int share = 2130837738;
|
||||
|
||||
// aapt resource value: 0x7f0200eb
|
||||
public const int star = 2130837739;
|
||||
public const int share_tools = 2130837739;
|
||||
|
||||
// aapt resource value: 0x7f0200ec
|
||||
public const int star_selected = 2130837740;
|
||||
public const int splash_screen = 2130837740;
|
||||
|
||||
// aapt resource value: 0x7f0200ed
|
||||
public const int tools = 2130837741;
|
||||
public const int star = 2130837741;
|
||||
|
||||
// aapt resource value: 0x7f0200ee
|
||||
public const int tools_selected = 2130837742;
|
||||
public const int star_selected = 2130837742;
|
||||
|
||||
// aapt resource value: 0x7f0200ef
|
||||
public const int upload = 2130837743;
|
||||
public const int tools = 2130837743;
|
||||
|
||||
// aapt resource value: 0x7f0200f0
|
||||
public const int user = 2130837744;
|
||||
public const int tools_selected = 2130837744;
|
||||
|
||||
// aapt resource value: 0x7f0200f1
|
||||
public const int upload = 2130837745;
|
||||
|
||||
// aapt resource value: 0x7f0200f2
|
||||
public const int user = 2130837746;
|
||||
|
||||
static Drawable()
|
||||
{
|
||||
|
||||
BIN
src/Android/Resources/drawable-hdpi/share.png
Normal file
|
After Width: | Height: | Size: 403 B |
BIN
src/Android/Resources/drawable-hdpi/share_tools.png
Normal file
|
After Width: | Height: | Size: 1009 B |
BIN
src/Android/Resources/drawable-xhdpi/share.png
Normal file
|
After Width: | Height: | Size: 503 B |
BIN
src/Android/Resources/drawable-xhdpi/share_tools.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
src/Android/Resources/drawable-xxhdpi/share.png
Normal file
|
After Width: | Height: | Size: 649 B |
BIN
src/Android/Resources/drawable-xxhdpi/share_tools.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
src/Android/Resources/drawable-xxxhdpi/share.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
src/Android/Resources/drawable-xxxhdpi/share_tools.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
src/Android/Resources/drawable/share.png
Normal file
|
After Width: | Height: | Size: 298 B |
BIN
src/Android/Resources/drawable/share_tools.png
Normal file
|
After Width: | Height: | Size: 817 B |
@@ -1,5 +1,7 @@
|
||||
using System;
|
||||
using Bit.App;
|
||||
using Bit.App.Abstractions;
|
||||
using Plugin.Settings.Abstractions;
|
||||
using Android.Gms.Analytics;
|
||||
using Android.Content;
|
||||
|
||||
@@ -7,8 +9,6 @@ namespace Bit.Android.Services
|
||||
{
|
||||
public class GoogleAnalyticsService : IGoogleAnalyticsService
|
||||
{
|
||||
private const string UserId = "&uid";
|
||||
|
||||
private readonly GoogleAnalytics _instance;
|
||||
private readonly IAuthService _authService;
|
||||
private readonly Tracker _tracker;
|
||||
@@ -17,7 +17,8 @@ namespace Bit.Android.Services
|
||||
public GoogleAnalyticsService(
|
||||
Context appContext,
|
||||
IAppIdService appIdService,
|
||||
IAuthService authService)
|
||||
IAuthService authService,
|
||||
ISettings settings)
|
||||
{
|
||||
_authService = authService;
|
||||
|
||||
@@ -25,16 +26,13 @@ namespace Bit.Android.Services
|
||||
_instance.SetLocalDispatchPeriod(10);
|
||||
|
||||
_tracker = _instance.NewTracker("UA-81915606-2");
|
||||
_tracker.EnableExceptionReporting(true);
|
||||
_tracker.EnableExceptionReporting(false);
|
||||
_tracker.EnableAdvertisingIdCollection(true);
|
||||
_tracker.EnableAutoActivityTracking(true);
|
||||
_tracker.SetClientId(appIdService.AnonymousAppId);
|
||||
}
|
||||
|
||||
public void RefreshUserId()
|
||||
{
|
||||
_tracker.Set(UserId, null);
|
||||
_setUserId = true;
|
||||
var gaOptOut = settings.GetValueOrDefault(Constants.SettingGaOptOut, false);
|
||||
SetAppOptOut(gaOptOut);
|
||||
}
|
||||
|
||||
public void TrackAppEvent(string eventName, string label = null)
|
||||
@@ -57,7 +55,6 @@ namespace Bit.Android.Services
|
||||
builder.SetLabel(label);
|
||||
}
|
||||
|
||||
SetUserId();
|
||||
_tracker.Send(builder.Build());
|
||||
}
|
||||
|
||||
@@ -67,30 +64,24 @@ namespace Bit.Android.Services
|
||||
builder.SetDescription(message);
|
||||
builder.SetFatal(fatal);
|
||||
|
||||
SetUserId();
|
||||
_tracker.Send(builder.Build());
|
||||
}
|
||||
|
||||
public void TrackPage(string pageName)
|
||||
{
|
||||
SetUserId();
|
||||
_tracker.SetScreenName(pageName);
|
||||
_tracker.Send(new HitBuilders.ScreenViewBuilder().Build());
|
||||
}
|
||||
|
||||
private void SetUserId()
|
||||
{
|
||||
if(_setUserId && _authService.IsAuthenticated)
|
||||
{
|
||||
_tracker.Set(UserId, _authService.UserId);
|
||||
_setUserId = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispatch(Action completionHandler = null)
|
||||
{
|
||||
_instance.DispatchLocalHits();
|
||||
completionHandler?.Invoke();
|
||||
}
|
||||
|
||||
public void SetAppOptOut(bool optOut)
|
||||
{
|
||||
_instance.AppOptOut = optOut;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,6 @@ namespace Bit.Android.Services
|
||||
{
|
||||
public class HttpService : IHttpService
|
||||
{
|
||||
public ApiHttpClient Client => new ApiHttpClient(new CustomAndroidClientHandler());
|
||||
public ApiHttpClient Client => new ApiHttpClient(new AndroidClientHandler());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="Acr.Support" version="2.1.0" targetFramework="monoandroid60" />
|
||||
<package id="Acr.UserDialogs" version="6.3.3" targetFramework="monoandroid60" />
|
||||
<package id="Acr.UserDialogs" version="6.3.10" targetFramework="monoandroid71" />
|
||||
<package id="AndHUD" version="1.2.0" targetFramework="monoandroid60" />
|
||||
<package id="BouncyCastle" version="1.8.1" targetFramework="monoandroid60" />
|
||||
<package id="CommonServiceLocator" version="1.3" targetFramework="monoandroid60" />
|
||||
<package id="HockeySDK.Xamarin" version="4.1.0" targetFramework="monoandroid60" />
|
||||
<package id="HockeySDK.Xamarin" version="4.1.2" targetFramework="monoandroid71" />
|
||||
<package id="Newtonsoft.Json" version="9.0.1" targetFramework="monoandroid60" />
|
||||
<package id="PCLCrypto" version="2.0.147" targetFramework="monoandroid60" />
|
||||
<package id="PInvoke.BCrypt" version="0.3.152" targetFramework="monoandroid60" />
|
||||
@@ -25,9 +25,9 @@
|
||||
<package id="SQLitePCLRaw.provider.e_sqlite3.android" version="1.1.2" targetFramework="monoandroid60" />
|
||||
<package id="Unity" version="3.5.1405-prerelease" targetFramework="monoandroid60" />
|
||||
<package id="Validation" version="2.3.7" targetFramework="monoandroid60" />
|
||||
<package id="Xam.Plugin.Connectivity" version="2.2.12" targetFramework="monoandroid60" />
|
||||
<package id="Xam.Plugin.Connectivity" version="2.3.0" targetFramework="monoandroid71" />
|
||||
<package id="Xam.Plugin.PushNotification" version="1.2.4" targetFramework="monoandroid60" developmentDependency="true" />
|
||||
<package id="Xam.Plugins.Settings" version="2.5.1.0" targetFramework="monoandroid60" />
|
||||
<package id="Xam.Plugins.Settings" version="2.5.4" targetFramework="monoandroid71" />
|
||||
<package id="Xamarin.Android.Support.Animated.Vector.Drawable" version="23.3.0" targetFramework="monoandroid60" />
|
||||
<package id="Xamarin.Android.Support.Design" version="23.3.0" targetFramework="monoandroid60" />
|
||||
<package id="Xamarin.Android.Support.v4" version="23.3.0" targetFramework="monoandroid60" />
|
||||
@@ -36,9 +36,9 @@
|
||||
<package id="Xamarin.Android.Support.v7.MediaRouter" version="23.3.0" targetFramework="monoandroid60" />
|
||||
<package id="Xamarin.Android.Support.v7.RecyclerView" version="23.3.0" targetFramework="monoandroid60" />
|
||||
<package id="Xamarin.Android.Support.Vector.Drawable" version="23.3.0" targetFramework="monoandroid60" />
|
||||
<package id="Xamarin.FFImageLoading" version="2.2.8" targetFramework="monoandroid60" />
|
||||
<package id="Xamarin.FFImageLoading.Forms" version="2.2.8" targetFramework="monoandroid60" />
|
||||
<package id="Xamarin.Forms" version="2.3.3.180" targetFramework="monoandroid60" />
|
||||
<package id="Xamarin.FFImageLoading" version="2.2.9" targetFramework="monoandroid71" />
|
||||
<package id="Xamarin.FFImageLoading.Forms" version="2.2.9" targetFramework="monoandroid71" />
|
||||
<package id="Xamarin.Forms" version="2.3.4.231" targetFramework="monoandroid71" />
|
||||
<package id="Xamarin.GooglePlayServices.Analytics" version="29.0.0.2" targetFramework="monoandroid60" />
|
||||
<package id="Xamarin.GooglePlayServices.Base" version="29.0.0.2" targetFramework="monoandroid60" />
|
||||
<package id="Xamarin.GooglePlayServices.Basement" version="29.0.0.2" targetFramework="monoandroid60" />
|
||||
|
||||
@@ -8,6 +8,8 @@ namespace Bit.App.Abstractions
|
||||
{
|
||||
Task<ApiResult> PostRegisterAsync(RegisterRequest requestObj);
|
||||
Task<ApiResult> PostPasswordHintAsync(PasswordHintRequest requestObj);
|
||||
Task<ApiResult<DateTime?>> GetAccountRevisionDate();
|
||||
Task<ApiResult<DateTime?>> GetAccountRevisionDateAsync();
|
||||
Task<ApiResult<ProfileResponse>> GetProfileAsync();
|
||||
Task<ApiResult<KeysResponse>> GetKeys();
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,5 @@ namespace Bit.App.Abstractions
|
||||
{
|
||||
Task<ApiResult<CipherResponse>> GetByIdAsync(string id);
|
||||
Task<ApiResult<ListResponse<CipherResponse>>> GetAsync();
|
||||
Task<ApiResult<CipherHistoryResponse>> GetByRevisionDateWithHistoryAsync(DateTime since);
|
||||
}
|
||||
}
|
||||
10
src/App/Abstractions/Services/IAppSettingsService.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using System;
|
||||
|
||||
namespace Bit.App.Abstractions
|
||||
{
|
||||
public interface IAppSettingsService
|
||||
{
|
||||
bool Locked { get; set; }
|
||||
DateTime LastActivity { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Models.Api;
|
||||
using System;
|
||||
using Bit.App.Models;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Bit.App.Abstractions
|
||||
{
|
||||
@@ -13,7 +12,9 @@ namespace Bit.App.Abstractions
|
||||
string Email { get; set; }
|
||||
string PIN { get; set; }
|
||||
|
||||
bool BelongsToOrganization(string orgId);
|
||||
void LogOut();
|
||||
Task<ApiResult<TokenResponse>> TokenPostAsync(TokenRequest request);
|
||||
Task<FullLoginResult> TokenPostAsync(string email, string masterPassword);
|
||||
Task<LoginResult> TokenPostTwoFactorAsync(string token, string email, string masterPasswordHash, SymmetricCryptoKey key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,30 @@
|
||||
using Bit.App.Models;
|
||||
using Bit.App.Models.Api;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Bit.App.Abstractions
|
||||
{
|
||||
public interface ICryptoService
|
||||
{
|
||||
string Base64Key { get; }
|
||||
byte[] Key { get; set; }
|
||||
byte[] PreviousKey { get; }
|
||||
SymmetricCryptoKey Key { get; set; }
|
||||
SymmetricCryptoKey PreviousKey { get; }
|
||||
bool KeyChanged { get; }
|
||||
byte[] PrivateKey { get; }
|
||||
IDictionary<string, SymmetricCryptoKey> OrgKeys { get; }
|
||||
|
||||
string Decrypt(CipherString encyptedValue);
|
||||
CipherString Encrypt(string plaintextValue);
|
||||
byte[] MakeKeyFromPassword(string password, string salt);
|
||||
void SetPrivateKey(CipherString privateKeyEnc);
|
||||
void SetOrgKeys(ProfileResponse profile);
|
||||
void SetOrgKeys(Dictionary<string, string> orgKeysEncDict);
|
||||
SymmetricCryptoKey GetOrgKey(string orgId);
|
||||
void ClearKeys();
|
||||
string Decrypt(CipherString encyptedValue, SymmetricCryptoKey key = null);
|
||||
byte[] DecryptToBytes(CipherString encyptedValue, SymmetricCryptoKey key = null);
|
||||
byte[] RsaDecryptToBytes(CipherString encyptedValue, byte[] privateKey);
|
||||
CipherString Encrypt(string plaintextValue, SymmetricCryptoKey key = null);
|
||||
SymmetricCryptoKey MakeKeyFromPassword(string password, string salt);
|
||||
string MakeKeyFromPasswordBase64(string password, string salt);
|
||||
byte[] HashPassword(byte[] key, string password);
|
||||
string HashPasswordBase64(byte[] key, string password);
|
||||
byte[] HashPassword(SymmetricCryptoKey key, string password);
|
||||
string HashPasswordBase64(SymmetricCryptoKey key, string password);
|
||||
}
|
||||
}
|
||||
@@ -4,12 +4,12 @@ namespace Bit.App.Abstractions
|
||||
{
|
||||
public interface IGoogleAnalyticsService
|
||||
{
|
||||
void RefreshUserId();
|
||||
void TrackPage(string pageName);
|
||||
void TrackAppEvent(string eventName, string label = null);
|
||||
void TrackExtensionEvent(string eventName, string label = null);
|
||||
void TrackEvent(string category, string eventName, string label = null);
|
||||
void TrackException(string message, bool fatal);
|
||||
void Dispatch(Action completionHandler = null);
|
||||
void SetAppOptOut(bool optOut);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
using Bit.App.Enums;
|
||||
using System;
|
||||
|
||||
namespace Bit.App.Abstractions
|
||||
{
|
||||
public interface ILockService
|
||||
{
|
||||
void UpdateLastActivity(DateTime? activityDate = null);
|
||||
LockType GetLockType(bool forceLock);
|
||||
}
|
||||
}
|
||||
@@ -6,9 +6,12 @@ namespace Bit.App.Abstractions
|
||||
public interface ISyncService
|
||||
{
|
||||
bool SyncInProgress { get; }
|
||||
Task<bool> SyncAsync(string id);
|
||||
Task<bool> SyncCipherAsync(string id);
|
||||
Task<bool> SyncFolderAsync(string id);
|
||||
Task<bool> SyncDeleteFolderAsync(string id, DateTime revisionDate);
|
||||
Task<bool> SyncDeleteLoginAsync(string id);
|
||||
Task<bool> SyncSettingsAsync();
|
||||
Task<bool> SyncProfileAsync();
|
||||
Task<bool> FullSyncAsync(bool forceSync = false);
|
||||
Task<bool> FullSyncAsync(TimeSpan syncThreshold, bool forceSync = false);
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ namespace Bit.App
|
||||
private readonly IGoogleAnalyticsService _googleAnalyticsService;
|
||||
private readonly ILocalizeService _localizeService;
|
||||
private readonly IAppInfoService _appInfoService;
|
||||
private readonly IAppSettingsService _appSettingsService;
|
||||
|
||||
public App(
|
||||
string uri,
|
||||
@@ -46,7 +47,8 @@ namespace Bit.App
|
||||
ILockService lockService,
|
||||
IGoogleAnalyticsService googleAnalyticsService,
|
||||
ILocalizeService localizeService,
|
||||
IAppInfoService appInfoService)
|
||||
IAppInfoService appInfoService,
|
||||
IAppSettingsService appSettingsService)
|
||||
{
|
||||
_uri = uri;
|
||||
_databaseService = databaseService;
|
||||
@@ -60,6 +62,7 @@ namespace Bit.App
|
||||
_googleAnalyticsService = googleAnalyticsService;
|
||||
_localizeService = localizeService;
|
||||
_appInfoService = appInfoService;
|
||||
_appSettingsService = appSettingsService;
|
||||
|
||||
SetCulture();
|
||||
SetStyles();
|
||||
@@ -123,7 +126,7 @@ namespace Bit.App
|
||||
|
||||
if(Device.OS == TargetPlatform.Android && !TopPageIsLock())
|
||||
{
|
||||
_settings.AddOrUpdateValue(Constants.LastActivityDate, DateTime.UtcNow);
|
||||
_lockService.UpdateLastActivity();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,7 +229,6 @@ namespace Bit.App
|
||||
await Task.Run(() => deviceApiRepository.PutClearTokenAsync(appIdService.AppId)).ConfigureAwait(false);
|
||||
|
||||
_googleAnalyticsService.TrackAppEvent("LoggedOut");
|
||||
_googleAnalyticsService.RefreshUserId();
|
||||
|
||||
Device.BeginInvokeOnMainThread(() => Current.MainPage = new ExtendedNavigationPage(new HomePage()));
|
||||
if(!string.IsNullOrWhiteSpace(logoutMessage))
|
||||
@@ -249,7 +251,7 @@ namespace Bit.App
|
||||
return;
|
||||
}
|
||||
|
||||
_settings.AddOrUpdateValue(Constants.Locked, true);
|
||||
_appSettingsService.Locked = true;
|
||||
switch(lockType)
|
||||
{
|
||||
case Enums.LockType.Fingerprint:
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
<Compile Include="Abstractions\Repositories\IAccountsApiRepository.cs" />
|
||||
<Compile Include="Abstractions\Repositories\IDeviceApiRepository.cs" />
|
||||
<Compile Include="Abstractions\Repositories\ISettingsRepository.cs" />
|
||||
<Compile Include="Abstractions\Services\IAppSettingsService.cs" />
|
||||
<Compile Include="Abstractions\Services\IMemoryService.cs" />
|
||||
<Compile Include="Abstractions\Services\ISettingsService.cs" />
|
||||
<Compile Include="Abstractions\Services\ITokenService.cs" />
|
||||
@@ -80,8 +81,11 @@
|
||||
<Compile Include="Controls\FormEntryCell.cs" />
|
||||
<Compile Include="Controls\PinControl.cs" />
|
||||
<Compile Include="Controls\VaultListViewCell.cs" />
|
||||
<Compile Include="Enums\EncryptionType.cs" />
|
||||
<Compile Include="Enums\OrganizationUserType.cs" />
|
||||
<Compile Include="Enums\LockType.cs" />
|
||||
<Compile Include="Enums\CipherType.cs" />
|
||||
<Compile Include="Enums\OrganizationUserStatusType.cs" />
|
||||
<Compile Include="Enums\PushType.cs" />
|
||||
<Compile Include="Enums\ReturnType.cs" />
|
||||
<Compile Include="Abstractions\Services\ILocalizeService.cs" />
|
||||
@@ -103,17 +107,21 @@
|
||||
<Compile Include="Models\Api\Response\ListResponse.cs" />
|
||||
<Compile Include="Models\Api\Response\DeviceResponse.cs" />
|
||||
<Compile Include="Models\Api\Response\LoginResponse.cs" />
|
||||
<Compile Include="Models\Api\Response\ProfileOrganizationResponse.cs" />
|
||||
<Compile Include="Models\Api\Response\KeysResponse.cs" />
|
||||
<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\SymmetricCryptoKey.cs" />
|
||||
<Compile Include="Models\Data\SettingsData.cs" />
|
||||
<Compile Include="Models\Data\FolderData.cs" />
|
||||
<Compile Include="Abstractions\IDataObject.cs" />
|
||||
<Compile Include="Models\Data\LoginData.cs" />
|
||||
<Compile Include="Models\DomainName.cs" />
|
||||
<Compile Include="Models\Folder.cs" />
|
||||
<Compile Include="Models\LoginResult.cs" />
|
||||
<Compile Include="Models\Page\AppExtensionPageModel.cs" />
|
||||
<Compile Include="Models\Page\SettingsFolderPageModel.cs" />
|
||||
<Compile Include="Models\Page\PinPageModel.cs" />
|
||||
@@ -196,6 +204,7 @@
|
||||
<DesignTime>True</DesignTime>
|
||||
<DependentUpon>AppResources.zh-Hans.resx</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Services\AppSettingsService.cs" />
|
||||
<Compile Include="Services\SettingsService.cs" />
|
||||
<Compile Include="Services\TokenService.cs" />
|
||||
<Compile Include="Services\AppIdService.cs" />
|
||||
@@ -254,28 +263,22 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="Acr.UserDialogs, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\Acr.UserDialogs.6.3.3\lib\portable-win+net45+wp8+win8+wpa81\Acr.UserDialogs.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
<HintPath>..\..\packages\Acr.UserDialogs.6.3.10\lib\portable-win+net45+wp8+win8+wpa81\Acr.UserDialogs.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Acr.UserDialogs.Interface, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\Acr.UserDialogs.6.3.3\lib\portable-win+net45+wp8+win8+wpa81\Acr.UserDialogs.Interface.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
<HintPath>..\..\packages\Acr.UserDialogs.6.3.10\lib\portable-win+net45+wp8+win8+wpa81\Acr.UserDialogs.Interface.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="FFImageLoading, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\Xamarin.FFImageLoading.2.2.8\lib\portable-net45+win8+wpa81+wp8\FFImageLoading.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
<HintPath>..\..\packages\Xamarin.FFImageLoading.2.2.9\lib\portable-net45+win8+wpa81+wp8\FFImageLoading.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="FFImageLoading.Forms, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\Xamarin.FFImageLoading.Forms.2.2.8\lib\portable-net45+win8+wpa81+wp8\FFImageLoading.Forms.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
<HintPath>..\..\packages\Xamarin.FFImageLoading.Forms.2.2.9\lib\portable-net45+win8+wpa81+wp8\FFImageLoading.Forms.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="FFImageLoading.Platform, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\Xamarin.FFImageLoading.2.2.8\lib\portable-net45+win8+wpa81+wp8\FFImageLoading.Platform.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
<HintPath>..\..\packages\Xamarin.FFImageLoading.2.2.9\lib\portable-net45+win8+wpa81+wp8\FFImageLoading.Platform.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="HockeySDK, Version=1.0.6103.22141, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\HockeySDK.Xamarin.4.1.0\lib\portable-net45+wp8+wpa81+win8+MonoAndroid10+MonoTouch10+Xamarin.iOS10\HockeySDK.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
<Reference Include="HockeySDK, Version=1.0.6288.33979, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\HockeySDK.Xamarin.4.1.2\lib\portable-net45+wp8+wpa81+win8+MonoAndroid10+MonoTouch10+Xamarin.iOS10\HockeySDK.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Microsoft.Practices.ServiceLocation, Version=1.3.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\CommonServiceLocator.1.3\lib\portable-net4+sl5+netcore45+wpa81+wp8\Microsoft.Practices.ServiceLocation.dll</HintPath>
|
||||
@@ -312,13 +315,11 @@
|
||||
<HintPath>..\..\packages\PInvoke.Windows.Core.0.3.152\lib\portable-net45+win+wpa81+MonoAndroid10+xamarinios10+MonoTouch10\PInvoke.Windows.Core.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
</Reference>
|
||||
<Reference Include="Plugin.Connectivity, Version=2.2.12.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\Xam.Plugin.Connectivity.2.2.12\lib\portable-net45+wp80+wp81+wpa81+win8+MonoAndroid10+MonoTouch10+Xamarin.iOS10+Xamarin.Mac20+UAP10\Plugin.Connectivity.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
<Reference Include="Plugin.Connectivity, Version=2.3.0.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\Xam.Plugin.Connectivity.2.3.0\lib\portable-net45+wp80+win8+wpa81\Plugin.Connectivity.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Plugin.Connectivity.Abstractions, Version=2.2.12.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\Xam.Plugin.Connectivity.2.2.12\lib\portable-net45+wp80+wp81+wpa81+win8+MonoAndroid10+MonoTouch10+Xamarin.iOS10+Xamarin.Mac20+UAP10\Plugin.Connectivity.Abstractions.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
<Reference Include="Plugin.Connectivity.Abstractions, Version=2.3.0.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\Xam.Plugin.Connectivity.2.3.0\lib\portable-net45+wp80+win8+wpa81\Plugin.Connectivity.Abstractions.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Plugin.Fingerprint, Version=1.2.0.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\Plugin.Fingerprint.1.2.0\lib\portable-net45+win8+wpa81+wp8\Plugin.Fingerprint.dll</HintPath>
|
||||
@@ -328,13 +329,11 @@
|
||||
<HintPath>..\..\packages\Plugin.Fingerprint.1.2.0\lib\portable-net45+win8+wpa81+wp8\Plugin.Fingerprint.Abstractions.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
</Reference>
|
||||
<Reference Include="Plugin.Settings, Version=2.5.1.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\Xam.Plugins.Settings.2.5.1.0\lib\portable-net45+wp80+win8+wpa81\Plugin.Settings.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
<Reference Include="Plugin.Settings, Version=2.5.4.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\Xam.Plugins.Settings.2.5.4\lib\portable-net45+wp80+win8+wpa81\Plugin.Settings.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Plugin.Settings.Abstractions, Version=2.5.1.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\Xam.Plugins.Settings.2.5.1.0\lib\portable-net45+wp80+win8+wpa81\Plugin.Settings.Abstractions.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
<Reference Include="Plugin.Settings.Abstractions, Version=2.5.4.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\Xam.Plugins.Settings.2.5.4\lib\portable-net45+wp80+win8+wpa81\Plugin.Settings.Abstractions.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="PushNotification.Plugin, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\Xam.Plugin.PushNotification.1.2.4\lib\portable-net45+wp8+wpa81+win8+MonoAndroid10+MonoTouch10+Xamarin.iOS10+UAP10\PushNotification.Plugin.dll</HintPath>
|
||||
@@ -377,16 +376,13 @@
|
||||
<Private>True</Private>
|
||||
</Reference>
|
||||
<Reference Include="Xamarin.Forms.Core, Version=2.0.0.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\Xamarin.Forms.2.3.3.180\lib\portable-win+net45+wp80+win81+wpa81+MonoAndroid10+Xamarin.iOS10+xamarinmac20\Xamarin.Forms.Core.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
<HintPath>..\..\packages\Xamarin.Forms.2.3.4.231\lib\portable-win+net45+wp80+win81+wpa81+MonoAndroid10+Xamarin.iOS10+xamarinmac20\Xamarin.Forms.Core.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Xamarin.Forms.Platform, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\Xamarin.Forms.2.3.3.180\lib\portable-win+net45+wp80+win81+wpa81+MonoAndroid10+Xamarin.iOS10+xamarinmac20\Xamarin.Forms.Platform.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
<HintPath>..\..\packages\Xamarin.Forms.2.3.4.231\lib\portable-win+net45+wp80+win81+wpa81+MonoAndroid10+Xamarin.iOS10+xamarinmac20\Xamarin.Forms.Platform.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Xamarin.Forms.Xaml, Version=2.0.0.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\Xamarin.Forms.2.3.3.180\lib\portable-win+net45+wp80+win81+wpa81+MonoAndroid10+Xamarin.iOS10+xamarinmac20\Xamarin.Forms.Xaml.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
<HintPath>..\..\packages\Xamarin.Forms.2.3.4.231\lib\portable-win+net45+wp80+win81+wpa81+MonoAndroid10+Xamarin.iOS10+xamarinmac20\Xamarin.Forms.Xaml.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="XLabs.Ioc, Version=2.0.5782.12218, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\XLabs.IoC.2.0.5782\lib\portable-net45+netcore45+wpa81+wp8+MonoAndroid1+MonoTouch1+Xamarin.iOS10\XLabs.Ioc.dll</HintPath>
|
||||
@@ -403,12 +399,12 @@
|
||||
<Compile Include="Services\PushNotificationListener.cs" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\Portable\$(TargetFrameworkVersion)\Microsoft.Portable.CSharp.targets" />
|
||||
<Import Project="..\..\packages\Xamarin.Forms.2.3.3.180\build\portable-win+net45+wp80+win81+wpa81+MonoAndroid10+Xamarin.iOS10+xamarinmac20\Xamarin.Forms.targets" Condition="Exists('..\..\packages\Xamarin.Forms.2.3.3.180\build\portable-win+net45+wp80+win81+wpa81+MonoAndroid10+Xamarin.iOS10+xamarinmac20\Xamarin.Forms.targets')" />
|
||||
<Import Project="..\..\packages\Xamarin.Forms.2.3.4.231\build\portable-win+net45+wp80+win81+wpa81+MonoAndroid10+Xamarin.iOS10+xamarinmac20\Xamarin.Forms.targets" Condition="Exists('..\..\packages\Xamarin.Forms.2.3.4.231\build\portable-win+net45+wp80+win81+wpa81+MonoAndroid10+Xamarin.iOS10+xamarinmac20\Xamarin.Forms.targets')" />
|
||||
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
|
||||
<PropertyGroup>
|
||||
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
|
||||
</PropertyGroup>
|
||||
<Error Condition="!Exists('..\..\packages\Xamarin.Forms.2.3.3.180\build\portable-win+net45+wp80+win81+wpa81+MonoAndroid10+Xamarin.iOS10+xamarinmac20\Xamarin.Forms.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\Xamarin.Forms.2.3.3.180\build\portable-win+net45+wp80+win81+wpa81+MonoAndroid10+Xamarin.iOS10+xamarinmac20\Xamarin.Forms.targets'))" />
|
||||
<Error Condition="!Exists('..\..\packages\Xamarin.Forms.2.3.4.231\build\portable-win+net45+wp80+win81+wpa81+MonoAndroid10+Xamarin.iOS10+xamarinmac20\Xamarin.Forms.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\Xamarin.Forms.2.3.4.231\build\portable-win+net45+wp80+win81+wpa81+MonoAndroid10+Xamarin.iOS10+xamarinmac20\Xamarin.Forms.targets'))" />
|
||||
</Target>
|
||||
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
|
||||
Other similar extension points exist, see Microsoft.Common.targets.
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
public const string SettingFingerprintUnlockOn = "setting:fingerprintUnlockOn";
|
||||
public const string SettingPinUnlockOn = "setting:pinUnlockOn";
|
||||
public const string SettingLockSeconds = "setting:lockSeconds";
|
||||
public const string SettingGaOptOut = "setting:googleAnalyticsOptOut";
|
||||
|
||||
public const string PasswordGeneratorLength = "pwGenerator:length";
|
||||
public const string PasswordGeneratorUppercase = "pwGenerator:uppercase";
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace Bit.App.Controls
|
||||
{
|
||||
private ISyncService _syncService;
|
||||
private IGoogleAnalyticsService _googleAnalyticsService;
|
||||
private ISettings _settings;
|
||||
private ILockService _lockService;
|
||||
private bool _syncIndicator;
|
||||
private bool _updateActivity;
|
||||
|
||||
@@ -20,7 +20,7 @@ namespace Bit.App.Controls
|
||||
_updateActivity = updateActivity;
|
||||
_syncService = Resolver.Resolve<ISyncService>();
|
||||
_googleAnalyticsService = Resolver.Resolve<IGoogleAnalyticsService>();
|
||||
_settings = Resolver.Resolve<ISettings>();
|
||||
_lockService = Resolver.Resolve<ILockService>();
|
||||
|
||||
BackgroundColor = Color.FromHex("efeff4");
|
||||
|
||||
@@ -45,11 +45,6 @@ namespace Bit.App.Controls
|
||||
IsBusy = _syncService.SyncInProgress;
|
||||
}
|
||||
|
||||
if(_updateActivity)
|
||||
{
|
||||
_settings.AddOrUpdateValue(Constants.LastActivityDate, DateTime.UtcNow);
|
||||
}
|
||||
|
||||
_googleAnalyticsService.TrackPage(GetType().Name);
|
||||
base.OnAppearing();
|
||||
}
|
||||
@@ -61,6 +56,11 @@ namespace Bit.App.Controls
|
||||
IsBusy = false;
|
||||
}
|
||||
|
||||
if(_updateActivity)
|
||||
{
|
||||
_lockService.UpdateLastActivity();
|
||||
}
|
||||
|
||||
base.OnDisappearing();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Xamarin.Forms;
|
||||
using FFImageLoading.Forms;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Controls
|
||||
{
|
||||
@@ -19,6 +20,14 @@ namespace Bit.App.Controls
|
||||
Style = (Style)Application.Current.Resources["text-muted"]
|
||||
};
|
||||
|
||||
LabelIcon = new CachedImage
|
||||
{
|
||||
WidthRequest = 16,
|
||||
HeightRequest = 16,
|
||||
HorizontalOptions = LayoutOptions.Start,
|
||||
Margin = new Thickness(5, 0, 0, 0)
|
||||
};
|
||||
|
||||
Button = new ExtendedButton
|
||||
{
|
||||
WidthRequest = 60
|
||||
@@ -32,11 +41,14 @@ 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.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(Button, 1, 0);
|
||||
grid.Children.Add(LabelIcon, 1, 0);
|
||||
grid.Children.Add(Button, 2, 0);
|
||||
Grid.SetColumnSpan(Detail, 2);
|
||||
Grid.SetRowSpan(Button, 2);
|
||||
|
||||
if(Device.OS == TargetPlatform.Android)
|
||||
@@ -49,6 +61,7 @@ namespace Bit.App.Controls
|
||||
|
||||
public Label Label { get; private set; }
|
||||
public Label Detail { get; private set; }
|
||||
public CachedImage LabelIcon { get; private set; }
|
||||
public Button Button { get; private set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,13 +12,16 @@ namespace Bit.App.Controls
|
||||
public VaultListViewCell(Action<VaultListPageModel.Login> moreClickedAction)
|
||||
{
|
||||
SetBinding(LoginParameterProperty, new Binding("."));
|
||||
Label.SetBinding<VaultListPageModel.Login>(Label.TextProperty, s => s.Name);
|
||||
Detail.SetBinding<VaultListPageModel.Login>(Label.TextProperty, s => s.Username);
|
||||
Label.SetBinding<VaultListPageModel.Login>(Label.TextProperty, l => l.Name);
|
||||
Detail.SetBinding<VaultListPageModel.Login>(Label.TextProperty, l => l.Username);
|
||||
LabelIcon.SetBinding<VaultListPageModel.Login>(VisualElement.IsVisibleProperty, l => l.Shared);
|
||||
|
||||
Button.Image = "more";
|
||||
Button.Command = new Command(() => moreClickedAction?.Invoke(LoginParameter));
|
||||
Button.BackgroundColor = Color.Transparent;
|
||||
|
||||
LabelIcon.Source = "share";
|
||||
|
||||
BackgroundColor = Color.White;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
{
|
||||
public enum CipherType : short
|
||||
{
|
||||
Folder = 0,
|
||||
// Folder deprecated
|
||||
//Folder = 0,
|
||||
Login = 1
|
||||
}
|
||||
}
|
||||
|
||||
11
src/App/Enums/EncryptionType.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace Bit.App.Enums
|
||||
{
|
||||
public enum EncryptionType : byte
|
||||
{
|
||||
AesCbc256_B64 = 0,
|
||||
AesCbc128_HmacSha256_B64 = 1,
|
||||
AesCbc256_HmacSha256_B64 = 2,
|
||||
Rsa2048_OaepSha256_B64 = 3,
|
||||
Rsa2048_OaepSha1_B64 = 4
|
||||
}
|
||||
}
|
||||
9
src/App/Enums/OrganizationUserStatusType.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace Bit.App.Enums
|
||||
{
|
||||
public enum OrganizationUserStatusType : byte
|
||||
{
|
||||
Invited = 0,
|
||||
Accepted = 1,
|
||||
Confirmed = 2
|
||||
}
|
||||
}
|
||||
9
src/App/Enums/OrganizationUserType.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace Bit.App.Enums
|
||||
{
|
||||
public enum OrganizationUserType : byte
|
||||
{
|
||||
Owner = 0,
|
||||
Admin = 1,
|
||||
User = 2
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,13 @@
|
||||
SyncCipherCreate = 1,
|
||||
SyncLoginDelete = 2,
|
||||
SyncFolderDelete = 3,
|
||||
SyncCiphers = 4
|
||||
SyncCiphers = 4,
|
||||
|
||||
SyncVault = 5,
|
||||
SyncOrgKeys = 6,
|
||||
SyncFolderCreate = 7,
|
||||
SyncFolderUpdate = 8,
|
||||
SyncCipherDelete = 9,
|
||||
SyncSettings = 10
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
{
|
||||
public LoginRequest(Login login)
|
||||
{
|
||||
OrganizationId = login.OrganizationId;
|
||||
FolderId = login.FolderId;
|
||||
Name = login.Name?.EncryptedString;
|
||||
Uri = login.Uri?.EncryptedString;
|
||||
@@ -13,6 +14,7 @@
|
||||
Favorite = login.Favorite;
|
||||
}
|
||||
|
||||
public string OrganizationId { get; set; }
|
||||
public string FolderId { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Uri { get; set; }
|
||||
|
||||
@@ -8,6 +8,8 @@ namespace Bit.App.Models.Api
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string FolderId { get; set; }
|
||||
public string UserId { get; set; }
|
||||
public string OrganizationId { get; set; }
|
||||
public CipherType Type { get; set; }
|
||||
public bool Favorite { get; set; }
|
||||
public JObject Data { get; set; }
|
||||
|
||||
8
src/App/Models/Api/Response/KeysResponse.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace Bit.App.Models.Api
|
||||
{
|
||||
public class KeysResponse
|
||||
{
|
||||
public string PublicKey { get; set; }
|
||||
public string PrivateKey { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,8 @@ namespace Bit.App.Models.Api
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string FolderId { get; set; }
|
||||
public string UserId { get; set; }
|
||||
public string OrganizationId { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Uri { get; set; }
|
||||
public string Username { get; set; }
|
||||
@@ -13,8 +15,5 @@ namespace Bit.App.Models.Api
|
||||
public string Notes { get; set; }
|
||||
public bool Favorite { get; set; }
|
||||
public DateTime RevisionDate { get; set; }
|
||||
|
||||
// Expandables
|
||||
public FolderResponse Folder { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
14
src/App/Models/Api/Response/ProfileOrganizationResponse.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using Bit.App.Enums;
|
||||
|
||||
namespace Bit.App.Models.Api
|
||||
{
|
||||
public class ProfileOrganizationResponseModel
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Key { get; set; }
|
||||
public OrganizationUserStatusType Status { get; set; }
|
||||
public OrganizationUserType Type { get; set; }
|
||||
public bool Enabled { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
namespace Bit.App.Models.Api
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Bit.App.Models.Api
|
||||
{
|
||||
public class ProfileResponse
|
||||
{
|
||||
@@ -8,5 +10,6 @@
|
||||
public string MasterPasswordHint { get; set; }
|
||||
public string Culture { get; set; }
|
||||
public bool TwoFactorEnabled { get; set; }
|
||||
public IEnumerable<ProfileOrganizationResponseModel> Organizations { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,5 +14,6 @@ namespace Bit.App.Models.Api
|
||||
[JsonProperty("token_type")]
|
||||
public string TokenType { get; set; }
|
||||
public List<int> TwoFactorProviders { get; set; }
|
||||
public string PrivateKey { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using Bit.App.Abstractions;
|
||||
using XLabs.Ioc;
|
||||
using Bit.App.Enums;
|
||||
|
||||
namespace Bit.App.Models
|
||||
{
|
||||
@@ -10,15 +11,66 @@ namespace Bit.App.Models
|
||||
|
||||
public CipherString(string encryptedString)
|
||||
{
|
||||
if(string.IsNullOrWhiteSpace(encryptedString) || !encryptedString.Contains("|"))
|
||||
if(string.IsNullOrWhiteSpace(encryptedString))
|
||||
{
|
||||
throw new ArgumentException(nameof(encryptedString));
|
||||
}
|
||||
|
||||
var headerPieces = encryptedString.Split('.');
|
||||
string[] encPieces;
|
||||
|
||||
EncryptionType encType;
|
||||
if(headerPieces.Length == 2 && Enum.TryParse(headerPieces[0], out encType))
|
||||
{
|
||||
EncryptionType = encType;
|
||||
encPieces = headerPieces[1].Split('|');
|
||||
}
|
||||
else if(headerPieces.Length == 1)
|
||||
{
|
||||
encPieces = headerPieces[0].Split('|');
|
||||
EncryptionType = encPieces.Length == 3 ? EncryptionType.AesCbc128_HmacSha256_B64 : EncryptionType.AesCbc256_B64;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentException("Malformed header.");
|
||||
}
|
||||
|
||||
switch(EncryptionType)
|
||||
{
|
||||
case EncryptionType.AesCbc256_B64:
|
||||
if(encPieces.Length != 2)
|
||||
{
|
||||
throw new ArgumentException("Malformed encPieces.");
|
||||
}
|
||||
InitializationVector = encPieces[0];
|
||||
CipherText = encPieces[1];
|
||||
break;
|
||||
case EncryptionType.AesCbc128_HmacSha256_B64:
|
||||
case EncryptionType.AesCbc256_HmacSha256_B64:
|
||||
if(encPieces.Length != 3)
|
||||
{
|
||||
throw new ArgumentException("Malformed encPieces.");
|
||||
}
|
||||
InitializationVector = encPieces[0];
|
||||
CipherText = encPieces[1];
|
||||
Mac = encPieces[2];
|
||||
break;
|
||||
case EncryptionType.Rsa2048_OaepSha256_B64:
|
||||
case EncryptionType.Rsa2048_OaepSha1_B64:
|
||||
if(encPieces.Length != 1)
|
||||
{
|
||||
throw new ArgumentException("Malformed encPieces.");
|
||||
}
|
||||
CipherText = encPieces[0];
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentException("Unknown encType.");
|
||||
}
|
||||
|
||||
EncryptedString = encryptedString;
|
||||
}
|
||||
|
||||
public CipherString(string initializationVector, string cipherText, string mac = null)
|
||||
public CipherString(EncryptionType encryptionType, string initializationVector, string cipherText, string mac = null)
|
||||
{
|
||||
if(string.IsNullOrWhiteSpace(initializationVector))
|
||||
{
|
||||
@@ -30,40 +82,47 @@ namespace Bit.App.Models
|
||||
throw new ArgumentNullException(nameof(cipherText));
|
||||
}
|
||||
|
||||
EncryptionType = encryptionType;
|
||||
EncryptedString = string.Format("{0}|{1}", initializationVector, cipherText);
|
||||
|
||||
if(!string.IsNullOrWhiteSpace(mac))
|
||||
{
|
||||
EncryptedString = string.Format("{0}|{1}", EncryptedString, mac);
|
||||
}
|
||||
}
|
||||
|
||||
public string EncryptedString { get; private set; }
|
||||
public string InitializationVector => EncryptedString?.Split('|')[0] ?? null;
|
||||
public string CipherText => EncryptedString?.Split('|')[1] ?? null;
|
||||
public string Mac
|
||||
{
|
||||
get
|
||||
if(EncryptionType != EncryptionType.AesCbc256_B64)
|
||||
{
|
||||
var pieces = EncryptedString?.Split('|') ?? new string[0];
|
||||
if(pieces.Length > 2)
|
||||
{
|
||||
return pieces[2];
|
||||
}
|
||||
|
||||
return null;
|
||||
EncryptedString = string.Format("{0}.{1}", (byte)EncryptionType, EncryptedString);
|
||||
}
|
||||
|
||||
CipherText = cipherText;
|
||||
InitializationVector = initializationVector;
|
||||
Mac = mac;
|
||||
}
|
||||
public byte[] InitializationVectorBytes => Convert.FromBase64String(InitializationVector);
|
||||
|
||||
public EncryptionType EncryptionType { get; private set; }
|
||||
public string EncryptedString { get; private set; }
|
||||
public string InitializationVector { get; private set; }
|
||||
public string CipherText { get; private set; }
|
||||
public string Mac { get; private set; }
|
||||
public byte[] InitializationVectorBytes => string.IsNullOrWhiteSpace(InitializationVector) ?
|
||||
null : Convert.FromBase64String(InitializationVector);
|
||||
public byte[] CipherTextBytes => Convert.FromBase64String(CipherText);
|
||||
public byte[] MacBytes => Mac == null ? null : Convert.FromBase64String(Mac);
|
||||
|
||||
public string Decrypt()
|
||||
public string Decrypt(string orgId = null)
|
||||
{
|
||||
if(_decryptedValue == null)
|
||||
{
|
||||
var cryptoService = Resolver.Resolve<ICryptoService>();
|
||||
_decryptedValue = cryptoService.Decrypt(this);
|
||||
if(!string.IsNullOrWhiteSpace(orgId))
|
||||
{
|
||||
_decryptedValue = cryptoService.Decrypt(this, cryptoService.GetOrgKey(orgId));
|
||||
}
|
||||
else
|
||||
{
|
||||
_decryptedValue = cryptoService.Decrypt(this);
|
||||
}
|
||||
}
|
||||
|
||||
return _decryptedValue;
|
||||
|
||||
@@ -26,21 +26,6 @@ namespace Bit.App.Models.Data
|
||||
RevisionDateTime = folder.RevisionDate;
|
||||
}
|
||||
|
||||
public FolderData(CipherResponse cipher, string userId)
|
||||
{
|
||||
if(cipher.Type != Enums.CipherType.Folder)
|
||||
{
|
||||
throw new ArgumentException(nameof(cipher.Type));
|
||||
}
|
||||
|
||||
var data = cipher.Data.ToObject<LoginDataModel>();
|
||||
|
||||
Id = cipher.Id;
|
||||
UserId = userId;
|
||||
Name = data.Name;
|
||||
RevisionDateTime = cipher.RevisionDate;
|
||||
}
|
||||
|
||||
[PrimaryKey]
|
||||
public string Id { get; set; }
|
||||
[Indexed]
|
||||
|
||||
@@ -16,6 +16,7 @@ namespace Bit.App.Models.Data
|
||||
Id = login.Id;
|
||||
FolderId = login.FolderId;
|
||||
UserId = userId;
|
||||
OrganizationId = login.OrganizationId;
|
||||
Name = login.Name?.EncryptedString;
|
||||
Uri = login.Uri?.EncryptedString;
|
||||
Username = login.Username?.EncryptedString;
|
||||
@@ -29,6 +30,7 @@ namespace Bit.App.Models.Data
|
||||
Id = login.Id;
|
||||
FolderId = login.FolderId;
|
||||
UserId = userId;
|
||||
OrganizationId = login.OrganizationId;
|
||||
Name = login.Name;
|
||||
Uri = login.Uri;
|
||||
Username = login.Username;
|
||||
@@ -50,6 +52,7 @@ namespace Bit.App.Models.Data
|
||||
Id = cipher.Id;
|
||||
FolderId = cipher.FolderId;
|
||||
UserId = userId;
|
||||
OrganizationId = cipher.OrganizationId;
|
||||
Name = data.Name;
|
||||
Uri = data.Uri;
|
||||
Username = data.Username;
|
||||
@@ -64,6 +67,7 @@ namespace Bit.App.Models.Data
|
||||
public string FolderId { get; set; }
|
||||
[Indexed]
|
||||
public string UserId { get; set; }
|
||||
public string OrganizationId { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Uri { get; set; }
|
||||
public string Username { get; set; }
|
||||
|
||||
@@ -11,6 +11,8 @@ namespace Bit.App.Models
|
||||
public Login(LoginData data)
|
||||
{
|
||||
Id = data.Id;
|
||||
UserId = data.UserId;
|
||||
OrganizationId = data.OrganizationId;
|
||||
FolderId = data.FolderId;
|
||||
Name = data.Name != null ? new CipherString(data.Name) : null;
|
||||
Uri = data.Uri != null ? new CipherString(data.Uri) : null;
|
||||
@@ -23,6 +25,8 @@ namespace Bit.App.Models
|
||||
public Login(LoginResponse response)
|
||||
{
|
||||
Id = response.Id;
|
||||
UserId = response.UserId;
|
||||
OrganizationId = response.OrganizationId;
|
||||
FolderId = response.FolderId;
|
||||
Name = response.Name != null ? new CipherString(response.Name) : null;
|
||||
Uri = response.Uri != null ? new CipherString(response.Uri) : null;
|
||||
@@ -32,6 +36,8 @@ namespace Bit.App.Models
|
||||
Favorite = response.Favorite;
|
||||
}
|
||||
|
||||
public string UserId { get; set; }
|
||||
public string OrganizationId { get; set; }
|
||||
public string FolderId { get; set; }
|
||||
public CipherString Uri { get; set; }
|
||||
public CipherString Username { get; set; }
|
||||
|
||||
15
src/App/Models/LoginResult.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
namespace Bit.App.Models
|
||||
{
|
||||
public class LoginResult
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public string ErrorMessage { get; set; }
|
||||
}
|
||||
|
||||
public class FullLoginResult : LoginResult
|
||||
{
|
||||
public bool TwoFactorRequired { get; set; }
|
||||
public SymmetricCryptoKey Key { get; set; }
|
||||
public string MasterPasswordHash { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -11,14 +11,16 @@ namespace Bit.App.Models.Page
|
||||
public Login(Models.Login login)
|
||||
{
|
||||
Id = login.Id;
|
||||
Shared = !string.IsNullOrWhiteSpace(login.OrganizationId);
|
||||
FolderId = login.FolderId;
|
||||
Name = login.Name?.Decrypt();
|
||||
Username = login.Username?.Decrypt() ?? " ";
|
||||
Password = new Lazy<string>(() => login.Password?.Decrypt());
|
||||
Uri = new Lazy<string>(() => login.Uri?.Decrypt());
|
||||
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));
|
||||
}
|
||||
|
||||
public string Id { get; set; }
|
||||
public bool Shared { get; set; }
|
||||
public string FolderId { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Username { get; set; }
|
||||
|
||||
@@ -171,11 +171,11 @@ namespace Bit.App.Models.Page
|
||||
|
||||
public void Update(Login login)
|
||||
{
|
||||
Name = login.Name?.Decrypt();
|
||||
Username = login.Username?.Decrypt();
|
||||
Password = login.Password?.Decrypt();
|
||||
Uri = login.Uri?.Decrypt();
|
||||
Notes = login.Notes?.Decrypt();
|
||||
Name = login.Name?.Decrypt(login.OrganizationId);
|
||||
Username = login.Username?.Decrypt(login.OrganizationId);
|
||||
Password = login.Password?.Decrypt(login.OrganizationId);
|
||||
Uri = login.Uri?.Decrypt(login.OrganizationId);
|
||||
Notes = login.Notes?.Decrypt(login.OrganizationId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,19 +8,24 @@ namespace Bit.App.Models
|
||||
public PushType Type { get; set; }
|
||||
}
|
||||
|
||||
public abstract class SyncPushNotification : PushNotification
|
||||
{
|
||||
public string UserId { get; set; }
|
||||
}
|
||||
|
||||
public class SyncCipherPushNotification : SyncPushNotification
|
||||
public class SyncCipherPushNotification : PushNotification
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string UserId { get; set; }
|
||||
public string OrganizationId { get; set; }
|
||||
public DateTime RevisionDate { get; set; }
|
||||
}
|
||||
|
||||
public class SyncCiphersPushNotification : SyncPushNotification
|
||||
public class SyncFolderPushNotification : PushNotification
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string UserId { get; set; }
|
||||
public DateTime RevisionDate { get; set; }
|
||||
}
|
||||
|
||||
public class SyncUserPushNotification : PushNotification
|
||||
{
|
||||
public string UserId { get; set; }
|
||||
public DateTime Date { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
62
src/App/Models/SymmetricCryptoKey.cs
Normal file
@@ -0,0 +1,62 @@
|
||||
using Bit.App.Enums;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace Bit.App.Models
|
||||
{
|
||||
public class SymmetricCryptoKey
|
||||
{
|
||||
public SymmetricCryptoKey(byte[] rawBytes, EncryptionType? encType = null)
|
||||
{
|
||||
if(rawBytes == null || rawBytes.Length == 0)
|
||||
{
|
||||
throw new Exception("Must provide keyBytes.");
|
||||
}
|
||||
|
||||
if(encType == null)
|
||||
{
|
||||
if(rawBytes.Length == 32)
|
||||
{
|
||||
encType = EncryptionType.AesCbc256_B64;
|
||||
}
|
||||
else if(rawBytes.Length == 64)
|
||||
{
|
||||
encType = EncryptionType.AesCbc256_HmacSha256_B64;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception("Unable to determine encType.");
|
||||
}
|
||||
}
|
||||
|
||||
EncryptionType = encType.Value;
|
||||
Key = rawBytes;
|
||||
|
||||
if(EncryptionType == EncryptionType.AesCbc256_B64 && Key.Length == 32)
|
||||
{
|
||||
EncKey = Key;
|
||||
MacKey = null;
|
||||
}
|
||||
else if(EncryptionType == EncryptionType.AesCbc128_HmacSha256_B64 && Key.Length == 32)
|
||||
{
|
||||
EncKey = Key.Take(16).ToArray();
|
||||
MacKey = Key.Skip(16).Take(16).ToArray();
|
||||
}
|
||||
else if(EncryptionType == EncryptionType.AesCbc256_HmacSha256_B64 && Key.Length == 64)
|
||||
{
|
||||
EncKey = Key.Take(32).ToArray();
|
||||
MacKey = Key.Skip(32).Take(32).ToArray();
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception("Unsupported encType/key length.");
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] Key { get; set; }
|
||||
public string B64Key => Convert.ToBase64String(Key);
|
||||
public byte[] EncKey { get; set; }
|
||||
public byte[] MacKey { get; set; }
|
||||
public EncryptionType EncryptionType { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ using Xamarin.Forms;
|
||||
using XLabs.Ioc;
|
||||
using Plugin.Fingerprint.Abstractions;
|
||||
using Plugin.Settings.Abstractions;
|
||||
using Bit.App.Abstractions;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
@@ -13,6 +14,7 @@ namespace Bit.App.Pages
|
||||
{
|
||||
private readonly IFingerprint _fingerprint;
|
||||
private readonly ISettings _settings;
|
||||
private readonly IAppSettingsService _appSettings;
|
||||
private readonly bool _checkFingerprintImmediately;
|
||||
|
||||
public LockFingerprintPage(bool checkFingerprintImmediately)
|
||||
@@ -20,6 +22,7 @@ namespace Bit.App.Pages
|
||||
_checkFingerprintImmediately = checkFingerprintImmediately;
|
||||
_fingerprint = Resolver.Resolve<IFingerprint>();
|
||||
_settings = Resolver.Resolve<ISettings>();
|
||||
_appSettings = Resolver.Resolve<IAppSettingsService>();
|
||||
|
||||
Init();
|
||||
}
|
||||
@@ -79,7 +82,7 @@ namespace Bit.App.Pages
|
||||
var result = await _fingerprint.AuthenticateAsync(AppResources.FingerprintDirection);
|
||||
if(result.Authenticated)
|
||||
{
|
||||
_settings.AddOrUpdateValue(Constants.Locked, false);
|
||||
_appSettings.Locked = false;
|
||||
await Navigation.PopModalAsync();
|
||||
}
|
||||
else if(result.Status == FingerprintAuthenticationResultStatus.FallbackRequested)
|
||||
|
||||
@@ -6,20 +6,19 @@ using Xamarin.Forms;
|
||||
using XLabs.Ioc;
|
||||
using Bit.App.Controls;
|
||||
using System.Linq;
|
||||
using Plugin.Settings.Abstractions;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class LockPasswordPage : BaseLockPage
|
||||
{
|
||||
private readonly IAuthService _authService;
|
||||
private readonly ISettings _settings;
|
||||
private readonly IAppSettingsService _appSettingsService;
|
||||
private readonly ICryptoService _cryptoService;
|
||||
|
||||
public LockPasswordPage()
|
||||
{
|
||||
_authService = Resolver.Resolve<IAuthService>();
|
||||
_settings = Resolver.Resolve<ISettings>();
|
||||
_appSettingsService = Resolver.Resolve<IAppSettingsService>();
|
||||
_cryptoService = Resolver.Resolve<ICryptoService>();
|
||||
|
||||
Init();
|
||||
@@ -49,7 +48,7 @@ namespace Bit.App.Pages
|
||||
NoFooter = true,
|
||||
Root = new TableRoot
|
||||
{
|
||||
new TableSection
|
||||
new TableSection(" ")
|
||||
{
|
||||
PasswordCell
|
||||
}
|
||||
@@ -120,9 +119,9 @@ namespace Bit.App.Pages
|
||||
}
|
||||
|
||||
var key = _cryptoService.MakeKeyFromPassword(PasswordCell.Entry.Text, _authService.Email);
|
||||
if(key.SequenceEqual(_cryptoService.Key))
|
||||
if(key.Key.SequenceEqual(_cryptoService.Key.Key))
|
||||
{
|
||||
_settings.AddOrUpdateValue(Constants.Locked, false);
|
||||
_appSettingsService.Locked = false;
|
||||
await Navigation.PopModalAsync();
|
||||
}
|
||||
else
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Acr.UserDialogs;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Resources;
|
||||
using Xamarin.Forms;
|
||||
using XLabs.Ioc;
|
||||
using Plugin.Settings.Abstractions;
|
||||
using Bit.App.Models.Page;
|
||||
using Bit.App.Controls;
|
||||
|
||||
@@ -14,13 +11,13 @@ namespace Bit.App.Pages
|
||||
public class LockPinPage : BaseLockPage
|
||||
{
|
||||
private readonly IAuthService _authService;
|
||||
private readonly ISettings _settings;
|
||||
private readonly IAppSettingsService _appSettingsService;
|
||||
private TapGestureRecognizer _tgr;
|
||||
|
||||
public LockPinPage()
|
||||
{
|
||||
_authService = Resolver.Resolve<IAuthService>();
|
||||
_settings = Resolver.Resolve<ISettings>();
|
||||
_appSettingsService = Resolver.Resolve<IAppSettingsService>();
|
||||
|
||||
Init();
|
||||
}
|
||||
@@ -96,7 +93,7 @@ namespace Bit.App.Pages
|
||||
{
|
||||
if(Model.PIN == _authService.PIN)
|
||||
{
|
||||
_settings.AddOrUpdateValue(Constants.Locked, false);
|
||||
_appSettingsService.Locked = false;
|
||||
PinControl.Entry.Unfocus();
|
||||
Navigation.PopModalAsync();
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Controls;
|
||||
using Bit.App.Models.Api;
|
||||
using Bit.App.Resources;
|
||||
using Xamarin.Forms;
|
||||
using XLabs.Ioc;
|
||||
@@ -15,11 +13,7 @@ namespace Bit.App.Pages
|
||||
{
|
||||
public class LoginPage : ExtendedContentPage
|
||||
{
|
||||
private ICryptoService _cryptoService;
|
||||
private IAuthService _authService;
|
||||
private ITokenService _tokenService;
|
||||
private IDeviceInfoService _deviceInfoService;
|
||||
private IAppIdService _appIdService;
|
||||
private IUserDialogs _userDialogs;
|
||||
private ISyncService _syncService;
|
||||
private ISettings _settings;
|
||||
@@ -31,11 +25,7 @@ namespace Bit.App.Pages
|
||||
: base(updateActivity: false)
|
||||
{
|
||||
_email = email;
|
||||
_cryptoService = Resolver.Resolve<ICryptoService>();
|
||||
_authService = Resolver.Resolve<IAuthService>();
|
||||
_tokenService = Resolver.Resolve<ITokenService>();
|
||||
_deviceInfoService = Resolver.Resolve<IDeviceInfoService>();
|
||||
_appIdService = Resolver.Resolve<IAppIdService>();
|
||||
_userDialogs = Resolver.Resolve<IUserDialogs>();
|
||||
_syncService = Resolver.Resolve<ISyncService>();
|
||||
_settings = Resolver.Resolve<ISettings>();
|
||||
@@ -82,10 +72,11 @@ namespace Bit.App.Pages
|
||||
HasUnevenRows = true,
|
||||
EnableSelection = true,
|
||||
NoFooter = true,
|
||||
//NoHeader = true,
|
||||
VerticalOptions = LayoutOptions.Start,
|
||||
Root = new TableRoot
|
||||
{
|
||||
new TableSection()
|
||||
new TableSection(" ")
|
||||
{
|
||||
EmailCell,
|
||||
PasswordCell
|
||||
@@ -188,40 +179,22 @@ namespace Bit.App.Pages
|
||||
return;
|
||||
}
|
||||
|
||||
var normalizedEmail = EmailCell.Entry.Text.ToLower();
|
||||
|
||||
var key = _cryptoService.MakeKeyFromPassword(PasswordCell.Entry.Text, normalizedEmail);
|
||||
|
||||
var request = new TokenRequest
|
||||
{
|
||||
Email = normalizedEmail,
|
||||
MasterPasswordHash = _cryptoService.HashPasswordBase64(key, PasswordCell.Entry.Text),
|
||||
Device = new DeviceRequest(_appIdService, _deviceInfoService)
|
||||
};
|
||||
|
||||
_userDialogs.ShowLoading(AppResources.LoggingIn, MaskType.Black);
|
||||
var response = await _authService.TokenPostAsync(request);
|
||||
var result = await _authService.TokenPostAsync(EmailCell.Entry.Text, PasswordCell.Entry.Text);
|
||||
_userDialogs.HideLoading();
|
||||
if(!response.Succeeded)
|
||||
if(!result.Success)
|
||||
{
|
||||
await DisplayAlert(AppResources.AnErrorHasOccurred, response.Errors.FirstOrDefault()?.Message, AppResources.Ok);
|
||||
await DisplayAlert(AppResources.AnErrorHasOccurred, result.ErrorMessage, AppResources.Ok);
|
||||
return;
|
||||
}
|
||||
|
||||
if(response.Result.TwoFactorProviders != null && response.Result.TwoFactorProviders.Count > 0)
|
||||
if(result.TwoFactorRequired)
|
||||
{
|
||||
_googleAnalyticsService.TrackAppEvent("LoggedIn To Two-step");
|
||||
await Navigation.PushAsync(new LoginTwoFactorPage(request.Email, request.MasterPasswordHash, key));
|
||||
await Navigation.PushAsync(new LoginTwoFactorPage(EmailCell.Entry.Text, result.MasterPasswordHash, result.Key));
|
||||
return;
|
||||
}
|
||||
|
||||
_cryptoService.Key = key;
|
||||
_tokenService.Token = response.Result.AccessToken;
|
||||
_tokenService.RefreshToken = response.Result.RefreshToken;
|
||||
_authService.UserId = _tokenService.TokenUserId;
|
||||
_authService.Email = _tokenService.TokenEmail;
|
||||
_settings.AddOrUpdateValue(Constants.LastLoginEmail, _authService.Email);
|
||||
_googleAnalyticsService.RefreshUserId();
|
||||
_googleAnalyticsService.TrackAppEvent("LoggedIn");
|
||||
|
||||
if(Device.OS == TargetPlatform.Android)
|
||||
|
||||
@@ -1,49 +1,37 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Controls;
|
||||
using Bit.App.Models.Api;
|
||||
using Bit.App.Resources;
|
||||
using Xamarin.Forms;
|
||||
using XLabs.Ioc;
|
||||
using Acr.UserDialogs;
|
||||
using System.Threading.Tasks;
|
||||
using Plugin.Settings.Abstractions;
|
||||
using PushNotification.Plugin.Abstractions;
|
||||
using Bit.App.Models;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class LoginTwoFactorPage : ExtendedContentPage
|
||||
{
|
||||
private ICryptoService _cryptoService;
|
||||
private IAuthService _authService;
|
||||
private ITokenService _tokenService;
|
||||
private IDeviceInfoService _deviceInfoService;
|
||||
private IAppIdService _appIdService;
|
||||
private IUserDialogs _userDialogs;
|
||||
private ISyncService _syncService;
|
||||
private ISettings _settings;
|
||||
private IGoogleAnalyticsService _googleAnalyticsService;
|
||||
private IPushNotification _pushNotification;
|
||||
private readonly string _email;
|
||||
private readonly string _masterPasswordHash;
|
||||
private readonly byte[] _key;
|
||||
private readonly SymmetricCryptoKey _key;
|
||||
|
||||
public LoginTwoFactorPage(string email, string masterPasswordHash, byte[] key)
|
||||
public LoginTwoFactorPage(string email, string masterPasswordHash, SymmetricCryptoKey key)
|
||||
: base(updateActivity: false)
|
||||
{
|
||||
_email = email;
|
||||
_masterPasswordHash = masterPasswordHash;
|
||||
_key = key;
|
||||
|
||||
_cryptoService = Resolver.Resolve<ICryptoService>();
|
||||
_authService = Resolver.Resolve<IAuthService>();
|
||||
_tokenService = Resolver.Resolve<ITokenService>();
|
||||
_deviceInfoService = Resolver.Resolve<IDeviceInfoService>();
|
||||
_appIdService = Resolver.Resolve<IAppIdService>();
|
||||
_userDialogs = Resolver.Resolve<IUserDialogs>();
|
||||
_syncService = Resolver.Resolve<ISyncService>();
|
||||
_settings = Resolver.Resolve<ISettings>();
|
||||
_googleAnalyticsService = Resolver.Resolve<IGoogleAnalyticsService>();
|
||||
_pushNotification = Resolver.Resolve<IPushNotification>();
|
||||
|
||||
@@ -75,7 +63,7 @@ namespace Bit.App.Pages
|
||||
VerticalOptions = LayoutOptions.Start,
|
||||
Root = new TableRoot
|
||||
{
|
||||
new TableSection()
|
||||
new TableSection(" ")
|
||||
{
|
||||
CodeCell
|
||||
}
|
||||
@@ -117,7 +105,7 @@ namespace Bit.App.Pages
|
||||
|
||||
var continueToolbarItem = new ToolbarItem(AppResources.Continue, null, async () =>
|
||||
{
|
||||
await LogIn();
|
||||
await LogInAsync();
|
||||
}, ToolbarItemOrder.Default, 0);
|
||||
|
||||
ToolbarItems.Add(continueToolbarItem);
|
||||
@@ -147,10 +135,10 @@ namespace Bit.App.Pages
|
||||
|
||||
private async void Entry_Completed(object sender, EventArgs e)
|
||||
{
|
||||
await LogIn();
|
||||
await LogInAsync();
|
||||
}
|
||||
|
||||
private async Task LogIn()
|
||||
private async Task LogInAsync()
|
||||
{
|
||||
if(string.IsNullOrWhiteSpace(CodeCell.Entry.Text))
|
||||
{
|
||||
@@ -159,31 +147,15 @@ namespace Bit.App.Pages
|
||||
return;
|
||||
}
|
||||
|
||||
var request = new TokenRequest
|
||||
{
|
||||
Email = _email,
|
||||
MasterPasswordHash = _masterPasswordHash,
|
||||
Token = CodeCell.Entry.Text.Replace(" ", ""),
|
||||
Provider = 0, // Authenticator app (only 1 provider for now, so hard coded)
|
||||
Device = new DeviceRequest(_appIdService, _deviceInfoService)
|
||||
};
|
||||
|
||||
_userDialogs.ShowLoading(AppResources.ValidatingCode, MaskType.Black);
|
||||
var response = await _authService.TokenPostAsync(request);
|
||||
var response = await _authService.TokenPostTwoFactorAsync(CodeCell.Entry.Text, _email, _masterPasswordHash, _key);
|
||||
_userDialogs.HideLoading();
|
||||
if(!response.Succeeded)
|
||||
if(!response.Success)
|
||||
{
|
||||
await DisplayAlert(AppResources.AnErrorHasOccurred, response.Errors.FirstOrDefault()?.Message, AppResources.Ok);
|
||||
await DisplayAlert(AppResources.AnErrorHasOccurred, response.ErrorMessage, AppResources.Ok);
|
||||
return;
|
||||
}
|
||||
|
||||
_cryptoService.Key = _key;
|
||||
_tokenService.Token = response.Result.AccessToken;
|
||||
_tokenService.RefreshToken = response.Result.RefreshToken;
|
||||
_authService.UserId = _tokenService.TokenUserId;
|
||||
_authService.Email = _tokenService.TokenEmail;
|
||||
_settings.AddOrUpdateValue(Constants.LastLoginEmail, _authService.Email);
|
||||
_googleAnalyticsService.RefreshUserId();
|
||||
_googleAnalyticsService.TrackAppEvent("LoggedIn From Two-step");
|
||||
|
||||
if(Device.OS == TargetPlatform.Android)
|
||||
|
||||
@@ -49,7 +49,7 @@ namespace Bit.App.Pages
|
||||
VerticalOptions = LayoutOptions.Start,
|
||||
Root = new TableRoot
|
||||
{
|
||||
new TableSection()
|
||||
new TableSection(" ")
|
||||
{
|
||||
EmailCell
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ namespace Bit.App.Pages
|
||||
{
|
||||
Root = new TableRoot
|
||||
{
|
||||
new TableSection
|
||||
new TableSection(" ")
|
||||
{
|
||||
EmailCell,
|
||||
PasswordCell
|
||||
@@ -88,7 +88,7 @@ namespace Bit.App.Pages
|
||||
NoHeader = true,
|
||||
Root = new TableRoot
|
||||
{
|
||||
new TableSection
|
||||
new TableSection(" ")
|
||||
{
|
||||
ConfirmPasswordCell,
|
||||
PasswordHintCell
|
||||
|
||||
@@ -59,7 +59,7 @@ namespace Bit.App.Pages
|
||||
HasUnevenRows = true,
|
||||
Root = new TableRoot
|
||||
{
|
||||
new TableSection
|
||||
new TableSection(" ")
|
||||
{
|
||||
CreditsCell
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ namespace Bit.App.Pages
|
||||
HasUnevenRows = true,
|
||||
Root = new TableRoot
|
||||
{
|
||||
new TableSection()
|
||||
new TableSection(" ")
|
||||
{
|
||||
NameCell
|
||||
}
|
||||
|
||||
@@ -54,11 +54,11 @@ namespace Bit.App.Pages
|
||||
VerticalOptions = LayoutOptions.Start,
|
||||
Root = new TableRoot
|
||||
{
|
||||
new TableSection
|
||||
new TableSection(" ")
|
||||
{
|
||||
NameCell
|
||||
},
|
||||
new TableSection
|
||||
new TableSection(" ")
|
||||
{
|
||||
DeleteCell
|
||||
}
|
||||
@@ -152,7 +152,6 @@ namespace Bit.App.Pages
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
_userDialogs.ShowLoading(AppResources.Deleting, MaskType.Black);
|
||||
var deleteTask = await _folderService.DeleteAsync(_folderId);
|
||||
_userDialogs.HideLoading();
|
||||
|
||||
@@ -38,7 +38,7 @@ namespace Bit.App.Pages
|
||||
{
|
||||
Root = new TableRoot
|
||||
{
|
||||
new TableSection
|
||||
new TableSection(" ")
|
||||
{
|
||||
EmailCell
|
||||
}
|
||||
@@ -61,7 +61,7 @@ namespace Bit.App.Pages
|
||||
NoHeader = true,
|
||||
Root = new TableRoot
|
||||
{
|
||||
new TableSection
|
||||
new TableSection(" ")
|
||||
{
|
||||
WebsiteCell
|
||||
}
|
||||
@@ -84,7 +84,7 @@ namespace Bit.App.Pages
|
||||
NoHeader = true,
|
||||
Root = new TableRoot
|
||||
{
|
||||
new TableSection
|
||||
new TableSection(" ")
|
||||
{
|
||||
BugCell
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ namespace Bit.App.Pages
|
||||
private ExtendedTextCell TwoStepCell { get; set; }
|
||||
private ExtendedTextCell ChangeMasterPasswordCell { get; set; }
|
||||
private ExtendedTextCell ChangeEmailCell { get; set; }
|
||||
private ExtendedSwitchCell AnalyticsCell { get; set; }
|
||||
private ExtendedTextCell FoldersCell { get; set; }
|
||||
private ExtendedTextCell SyncCell { get; set; }
|
||||
private ExtendedTextCell LockCell { get; set; }
|
||||
@@ -103,6 +104,12 @@ namespace Bit.App.Pages
|
||||
ShowDisclousure = true
|
||||
};
|
||||
|
||||
AnalyticsCell = new ExtendedSwitchCell
|
||||
{
|
||||
Text = AppResources.DisableGA,
|
||||
On = _settings.GetValueOrDefault(Constants.SettingGaOptOut, false)
|
||||
};
|
||||
|
||||
FoldersCell = new ExtendedTextCell
|
||||
{
|
||||
Text = AppResources.Folders,
|
||||
@@ -139,6 +146,7 @@ namespace Bit.App.Pages
|
||||
|
||||
var otherSection = new TableSection(AppResources.Other)
|
||||
{
|
||||
AnalyticsCell,
|
||||
AboutCell,
|
||||
HelpCell
|
||||
};
|
||||
@@ -193,6 +201,7 @@ namespace Bit.App.Pages
|
||||
base.OnAppearing();
|
||||
|
||||
PinCell.OnChanged += PinCell_Changed;
|
||||
AnalyticsCell.OnChanged += AnalyticsCell_Changed;
|
||||
LockOptionsCell.Tapped += LockOptionsCell_Tapped;
|
||||
TwoStepCell.Tapped += TwoStepCell_Tapped;
|
||||
ChangeMasterPasswordCell.Tapped += ChangeMasterPasswordCell_Tapped;
|
||||
@@ -226,6 +235,7 @@ namespace Bit.App.Pages
|
||||
base.OnDisappearing();
|
||||
|
||||
PinCell.OnChanged -= PinCell_Changed;
|
||||
AnalyticsCell.OnChanged -= AnalyticsCell_Changed;
|
||||
LockOptionsCell.Tapped -= LockOptionsCell_Tapped;
|
||||
TwoStepCell.Tapped -= TwoStepCell_Tapped;
|
||||
ChangeMasterPasswordCell.Tapped -= ChangeMasterPasswordCell_Tapped;
|
||||
@@ -411,6 +421,18 @@ namespace Bit.App.Pages
|
||||
}
|
||||
}
|
||||
|
||||
private void AnalyticsCell_Changed(object sender, ToggledEventArgs e)
|
||||
{
|
||||
var cell = sender as ExtendedSwitchCell;
|
||||
if (cell == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_settings.AddOrUpdateValue(Constants.SettingGaOptOut, cell.On);
|
||||
_googleAnalyticsService.SetAppOptOut(cell.On);
|
||||
}
|
||||
|
||||
private void PinEntered(SettingsPinPage page)
|
||||
{
|
||||
page.PinControl.Entry.Unfocus();
|
||||
|
||||
@@ -25,6 +25,7 @@ namespace Bit.App.Pages
|
||||
|
||||
public ToolsViewCell GeneratorCell { get; set; }
|
||||
public ToolsViewCell WebCell { get; set; }
|
||||
public ToolsViewCell ShareCell { get; set; }
|
||||
public ToolsViewCell ImportCell { get; set; }
|
||||
public ToolsViewCell ExtensionCell { get; set; }
|
||||
public ToolsViewCell AutofillCell { get; set; }
|
||||
@@ -34,9 +35,10 @@ namespace Bit.App.Pages
|
||||
GeneratorCell = new ToolsViewCell(AppResources.PasswordGenerator, AppResources.PasswordGeneratorDescription,
|
||||
"refresh");
|
||||
WebCell = new ToolsViewCell(AppResources.WebVault, AppResources.WebVaultDescription, "globe");
|
||||
ShareCell = new ToolsViewCell(AppResources.ShareVault, AppResources.ShareVaultDescription, "share_tools");
|
||||
ImportCell = new ToolsViewCell(AppResources.ImportLogins, AppResources.ImportLoginsDescription, "cloudup");
|
||||
|
||||
var section = new TableSection { GeneratorCell };
|
||||
var section = new TableSection(" ") { GeneratorCell };
|
||||
|
||||
if(Device.OS == TargetPlatform.iOS)
|
||||
{
|
||||
@@ -46,13 +48,13 @@ namespace Bit.App.Pages
|
||||
}
|
||||
else
|
||||
{
|
||||
AutofillCell = new ToolsViewCell(
|
||||
string.Format("{0} ({1})", AppResources.BitwardenAutofillService, AppResources.Beta),
|
||||
AutofillCell = new ToolsViewCell(AppResources.BitwardenAutofillService,
|
||||
AppResources.BitwardenAutofillServiceDescription, "upload");
|
||||
section.Add(AutofillCell);
|
||||
}
|
||||
|
||||
section.Add(WebCell);
|
||||
section.Add(ShareCell);
|
||||
section.Add(ImportCell);
|
||||
|
||||
var table = new ExtendedTableView
|
||||
@@ -81,6 +83,7 @@ namespace Bit.App.Pages
|
||||
base.OnAppearing();
|
||||
GeneratorCell.Tapped += GeneratorCell_Tapped;
|
||||
WebCell.Tapped += WebCell_Tapped;
|
||||
ShareCell.Tapped += ShareCell_Tapped;
|
||||
ImportCell.Tapped += ImportCell_Tapped;
|
||||
if(ExtensionCell != null)
|
||||
{
|
||||
@@ -97,6 +100,7 @@ namespace Bit.App.Pages
|
||||
base.OnDisappearing();
|
||||
GeneratorCell.Tapped -= GeneratorCell_Tapped;
|
||||
WebCell.Tapped -= WebCell_Tapped;
|
||||
ShareCell.Tapped -= ShareCell_Tapped;
|
||||
ImportCell.Tapped -= ImportCell_Tapped;
|
||||
if(ExtensionCell != null)
|
||||
{
|
||||
@@ -130,6 +134,12 @@ namespace Bit.App.Pages
|
||||
Device.OpenUri(new Uri("https://vault.bitwarden.com"));
|
||||
}
|
||||
|
||||
private void ShareCell_Tapped(object sender, EventArgs e)
|
||||
{
|
||||
_googleAnalyticsService.TrackAppEvent("OpenedTool", "Share");
|
||||
Device.OpenUri(new Uri("https://vault.bitwarden.com/#/?org=free"));
|
||||
}
|
||||
|
||||
private async void ImportCell_Tapped(object sender, EventArgs e)
|
||||
{
|
||||
if(!await _userDialogs.ConfirmAsync(AppResources.ImportLoginsConfirmation, null, AppResources.Yes,
|
||||
|
||||
@@ -73,7 +73,7 @@ namespace Bit.App.Pages
|
||||
NoHeader = true,
|
||||
Root = new TableRoot
|
||||
{
|
||||
new TableSection
|
||||
new TableSection(" ")
|
||||
{
|
||||
RegenerateCell,
|
||||
CopyCell
|
||||
|
||||
@@ -77,19 +77,19 @@ namespace Bit.App.Pages
|
||||
EnableSelection = false,
|
||||
Root = new TableRoot
|
||||
{
|
||||
new TableSection
|
||||
new TableSection(" ")
|
||||
{
|
||||
UppercaseCell,
|
||||
LowercaseCell,
|
||||
NumbersCell,
|
||||
SpecialCell
|
||||
},
|
||||
new TableSection
|
||||
new TableSection(" ")
|
||||
{
|
||||
NumbersMinCell,
|
||||
SpecialMinCell
|
||||
},
|
||||
new TableSection
|
||||
new TableSection(" ")
|
||||
{
|
||||
AvoidAmbiguousCell
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ namespace Bit.App.Pages
|
||||
|
||||
private void Init()
|
||||
{
|
||||
NotesCell = new FormEditorCell(height: 90);
|
||||
NotesCell = new FormEditorCell(height: 180);
|
||||
PasswordCell = new FormEntryCell(AppResources.Password, isPassword: true, nextElement: NotesCell.Editor,
|
||||
useButton: true);
|
||||
PasswordCell.Button.Image = "eye";
|
||||
@@ -111,7 +111,7 @@ namespace Bit.App.Pages
|
||||
PasswordCell,
|
||||
GenerateCell
|
||||
},
|
||||
new TableSection
|
||||
new TableSection(" ")
|
||||
{
|
||||
FolderCell,
|
||||
favoriteCell
|
||||
@@ -230,6 +230,8 @@ namespace Bit.App.Pages
|
||||
AppResources.Ok);
|
||||
}
|
||||
}
|
||||
|
||||
NameCell.Entry.FocusWithDelay();
|
||||
}
|
||||
|
||||
protected override void OnDisappearing()
|
||||
|
||||
@@ -31,13 +31,14 @@ namespace Bit.App.Pages
|
||||
|
||||
Uri uri;
|
||||
DomainName domainName;
|
||||
if(!System.Uri.TryCreate(uriString, UriKind.Absolute, out uri) ||
|
||||
if(uriString?.StartsWith(Constants.AndroidAppProtocol) ?? false)
|
||||
{
|
||||
_name = uriString.Substring(Constants.AndroidAppProtocol.Length);
|
||||
}
|
||||
else if(!System.Uri.TryCreate(uriString, UriKind.Absolute, out uri) ||
|
||||
!DomainName.TryParse(uri.Host, out domainName))
|
||||
{
|
||||
if(uriString != null && uriString.StartsWith(Constants.AndroidAppProtocol))
|
||||
{
|
||||
_name = uriString.Substring(Constants.AndroidAppProtocol.Length);
|
||||
}
|
||||
_name = "--";
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -50,26 +50,26 @@ namespace Bit.App.Pages
|
||||
return;
|
||||
}
|
||||
|
||||
NotesCell = new FormEditorCell(height: 90);
|
||||
NotesCell.Editor.Text = login.Notes?.Decrypt();
|
||||
NotesCell = new FormEditorCell(height: 180);
|
||||
NotesCell.Editor.Text = login.Notes?.Decrypt(login.OrganizationId);
|
||||
|
||||
PasswordCell = new FormEntryCell(AppResources.Password, isPassword: true, nextElement: NotesCell.Editor,
|
||||
useButton: true);
|
||||
PasswordCell.Entry.Text = login.Password?.Decrypt();
|
||||
PasswordCell.Entry.Text = login.Password?.Decrypt(login.OrganizationId);
|
||||
PasswordCell.Button.Image = "eye";
|
||||
PasswordCell.Entry.DisableAutocapitalize = true;
|
||||
PasswordCell.Entry.Autocorrect = false;
|
||||
PasswordCell.Entry.FontFamily = Device.OnPlatform(iOS: "Courier", Android: "monospace", WinPhone: "Courier");
|
||||
|
||||
UsernameCell = new FormEntryCell(AppResources.Username, nextElement: PasswordCell.Entry);
|
||||
UsernameCell.Entry.Text = login.Username?.Decrypt();
|
||||
UsernameCell.Entry.Text = login.Username?.Decrypt(login.OrganizationId);
|
||||
UsernameCell.Entry.DisableAutocapitalize = true;
|
||||
UsernameCell.Entry.Autocorrect = false;
|
||||
|
||||
UriCell = new FormEntryCell(AppResources.URI, Keyboard.Url, nextElement: UsernameCell.Entry);
|
||||
UriCell.Entry.Text = login.Uri?.Decrypt();
|
||||
UriCell.Entry.Text = login.Uri?.Decrypt(login.OrganizationId);
|
||||
NameCell = new FormEntryCell(AppResources.Name, nextElement: UriCell.Entry);
|
||||
NameCell.Entry.Text = login.Name?.Decrypt();
|
||||
NameCell.Entry.Text = login.Name?.Decrypt(login.OrganizationId);
|
||||
|
||||
GenerateCell = new ExtendedTextCell
|
||||
{
|
||||
@@ -118,7 +118,7 @@ namespace Bit.App.Pages
|
||||
PasswordCell,
|
||||
GenerateCell
|
||||
},
|
||||
new TableSection
|
||||
new TableSection(" ")
|
||||
{
|
||||
FolderCell,
|
||||
favoriteCell
|
||||
@@ -127,7 +127,7 @@ namespace Bit.App.Pages
|
||||
{
|
||||
NotesCell
|
||||
},
|
||||
new TableSection
|
||||
new TableSection(" ")
|
||||
{
|
||||
DeleteCell
|
||||
}
|
||||
@@ -159,11 +159,11 @@ namespace Bit.App.Pages
|
||||
return;
|
||||
}
|
||||
|
||||
login.Uri = UriCell.Entry.Text?.Encrypt();
|
||||
login.Name = NameCell.Entry.Text?.Encrypt();
|
||||
login.Username = UsernameCell.Entry.Text?.Encrypt();
|
||||
login.Password = PasswordCell.Entry.Text?.Encrypt();
|
||||
login.Notes = NotesCell.Editor.Text?.Encrypt();
|
||||
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.Favorite = favoriteCell.On;
|
||||
|
||||
if(FolderCell.Picker.SelectedIndex > 0)
|
||||
|
||||
@@ -34,6 +34,7 @@ namespace Bit.App.Pages
|
||||
public LabeledValueCell UsernameCell { get; set; }
|
||||
public LabeledValueCell PasswordCell { get; set; }
|
||||
public LabeledValueCell UriCell { get; set; }
|
||||
public LabeledValueCell NotesCell { get; set; }
|
||||
private EditLoginToolBarItem EditItem { get; set; }
|
||||
|
||||
private void Init()
|
||||
@@ -76,9 +77,9 @@ namespace Bit.App.Pages
|
||||
UriCell.Button1.Command = new Command(() => Device.OpenUri(new Uri(Model.Uri)));
|
||||
|
||||
// Notes
|
||||
var notesCell = new LabeledValueCell();
|
||||
notesCell.Value.SetBinding<VaultViewLoginPageModel>(Label.TextProperty, s => s.Notes);
|
||||
notesCell.Value.LineBreakMode = LineBreakMode.WordWrap;
|
||||
NotesCell = new LabeledValueCell();
|
||||
NotesCell.Value.SetBinding<VaultViewLoginPageModel>(Label.TextProperty, s => s.Notes);
|
||||
NotesCell.Value.LineBreakMode = LineBreakMode.WordWrap;
|
||||
|
||||
LoginInformationSection = new TableSection(AppResources.LoginInformation)
|
||||
{
|
||||
@@ -87,7 +88,7 @@ namespace Bit.App.Pages
|
||||
|
||||
NotesSection = new TableSection(AppResources.Notes)
|
||||
{
|
||||
notesCell
|
||||
NotesCell
|
||||
};
|
||||
|
||||
Table = new ExtendedTableView
|
||||
@@ -124,6 +125,7 @@ namespace Bit.App.Pages
|
||||
|
||||
protected async override void OnAppearing()
|
||||
{
|
||||
NotesCell.Tapped += NotesCell_Tapped;
|
||||
EditItem.InitEvents();
|
||||
|
||||
var login = await _loginService.GetByIdAsync(_loginId);
|
||||
@@ -176,9 +178,15 @@ namespace Bit.App.Pages
|
||||
|
||||
protected override void OnDisappearing()
|
||||
{
|
||||
NotesCell.Tapped -= NotesCell_Tapped;
|
||||
EditItem.Dispose();
|
||||
}
|
||||
|
||||
private void NotesCell_Tapped(object sender, EventArgs e)
|
||||
{
|
||||
Copy(Model.Notes, AppResources.Notes);
|
||||
}
|
||||
|
||||
private void Copy(string copyText, string alertLabel)
|
||||
{
|
||||
_clipboardService.CopyToClipboard(copyText);
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Threading.Tasks;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Models.Api;
|
||||
using Plugin.Connectivity.Abstractions;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Bit.App.Repositories
|
||||
{
|
||||
@@ -84,7 +85,7 @@ namespace Bit.App.Repositories
|
||||
}
|
||||
}
|
||||
|
||||
public virtual async Task<ApiResult<DateTime?>> GetAccountRevisionDate()
|
||||
public virtual async Task<ApiResult<DateTime?>> GetAccountRevisionDateAsync()
|
||||
{
|
||||
if(!Connectivity.IsConnected)
|
||||
{
|
||||
@@ -132,5 +133,85 @@ namespace Bit.App.Repositories
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public virtual async Task<ApiResult<ProfileResponse>> GetProfileAsync()
|
||||
{
|
||||
if(!Connectivity.IsConnected)
|
||||
{
|
||||
return HandledNotConnected<ProfileResponse>();
|
||||
}
|
||||
|
||||
var tokenStateResponse = await HandleTokenStateAsync<ProfileResponse>();
|
||||
if(!tokenStateResponse.Succeeded)
|
||||
{
|
||||
return tokenStateResponse;
|
||||
}
|
||||
|
||||
using(var client = HttpService.Client)
|
||||
{
|
||||
var requestMessage = new TokenHttpRequestMessage()
|
||||
{
|
||||
Method = HttpMethod.Get,
|
||||
RequestUri = new Uri(client.BaseAddress, string.Concat(ApiRoute, "/profile")),
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
var response = await client.SendAsync(requestMessage).ConfigureAwait(false);
|
||||
if(!response.IsSuccessStatusCode)
|
||||
{
|
||||
return await HandleErrorAsync<ProfileResponse>(response).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
var responseObj = JsonConvert.DeserializeObject<ProfileResponse>(responseContent);
|
||||
return ApiResult<ProfileResponse>.Success(responseObj, response.StatusCode);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return HandledWebException<ProfileResponse>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public virtual async Task<ApiResult<KeysResponse>> GetKeys()
|
||||
{
|
||||
if(!Connectivity.IsConnected)
|
||||
{
|
||||
return HandledNotConnected<KeysResponse>();
|
||||
}
|
||||
|
||||
var tokenStateResponse = await HandleTokenStateAsync<KeysResponse>();
|
||||
if(!tokenStateResponse.Succeeded)
|
||||
{
|
||||
return tokenStateResponse;
|
||||
}
|
||||
|
||||
using(var client = HttpService.Client)
|
||||
{
|
||||
var requestMessage = new TokenHttpRequestMessage()
|
||||
{
|
||||
Method = HttpMethod.Get,
|
||||
RequestUri = new Uri(client.BaseAddress, string.Concat(ApiRoute, "/keys")),
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
var response = await client.SendAsync(requestMessage).ConfigureAwait(false);
|
||||
if(!response.IsSuccessStatusCode)
|
||||
{
|
||||
return await HandleErrorAsync<KeysResponse>(response).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
var responseObj = JsonConvert.DeserializeObject<KeysResponse>(responseContent);
|
||||
return ApiResult<KeysResponse>.Success(responseObj, response.StatusCode);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return HandledWebException<KeysResponse>();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,7 +77,8 @@ namespace Bit.App.Repositories
|
||||
var requestMessage = new TokenHttpRequestMessage()
|
||||
{
|
||||
Method = HttpMethod.Get,
|
||||
RequestUri = new Uri(client.BaseAddress, ApiRoute),
|
||||
RequestUri = new Uri(client.BaseAddress,
|
||||
string.Format("{0}?includeFolders=false&includeShared=true", ApiRoute)),
|
||||
};
|
||||
|
||||
try
|
||||
@@ -98,45 +99,5 @@ namespace Bit.App.Repositories
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public virtual async Task<ApiResult<CipherHistoryResponse>> GetByRevisionDateWithHistoryAsync(DateTime since)
|
||||
{
|
||||
if(!Connectivity.IsConnected)
|
||||
{
|
||||
return HandledNotConnected<CipherHistoryResponse>();
|
||||
}
|
||||
|
||||
var tokenStateResponse = await HandleTokenStateAsync<CipherHistoryResponse>();
|
||||
if(!tokenStateResponse.Succeeded)
|
||||
{
|
||||
return tokenStateResponse;
|
||||
}
|
||||
|
||||
using(var client = HttpService.Client)
|
||||
{
|
||||
var requestMessage = new TokenHttpRequestMessage()
|
||||
{
|
||||
Method = HttpMethod.Get,
|
||||
RequestUri = new Uri(client.BaseAddress, string.Concat(ApiRoute, "/history", "?since=", since)),
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
var response = await client.SendAsync(requestMessage).ConfigureAwait(false);
|
||||
if(!response.IsSuccessStatusCode)
|
||||
{
|
||||
return await HandleErrorAsync<CipherHistoryResponse>(response).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
var responseObj = JsonConvert.DeserializeObject<CipherHistoryResponse>(responseContent);
|
||||
return ApiResult<CipherHistoryResponse>.Success(responseObj, response.StatusCode);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return HandledWebException<CipherHistoryResponse>();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Models.Api;
|
||||
using Newtonsoft.Json;
|
||||
using Plugin.Connectivity.Abstractions;
|
||||
using System.Net;
|
||||
|
||||
namespace Bit.App.Repositories
|
||||
{
|
||||
@@ -20,45 +15,5 @@ namespace Bit.App.Repositories
|
||||
{ }
|
||||
|
||||
protected override string ApiRoute => "folders";
|
||||
|
||||
public virtual async Task<ApiResult<ListResponse<FolderResponse>>> GetByRevisionDateAsync(DateTime since)
|
||||
{
|
||||
if(!Connectivity.IsConnected)
|
||||
{
|
||||
return HandledNotConnected<ListResponse<FolderResponse>>();
|
||||
}
|
||||
|
||||
var tokenStateResponse = await HandleTokenStateAsync<ListResponse<FolderResponse>>();
|
||||
if(!tokenStateResponse.Succeeded)
|
||||
{
|
||||
return tokenStateResponse;
|
||||
}
|
||||
|
||||
using(var client = HttpService.Client)
|
||||
{
|
||||
var requestMessage = new TokenHttpRequestMessage()
|
||||
{
|
||||
Method = HttpMethod.Get,
|
||||
RequestUri = new Uri(client.BaseAddress, string.Concat(ApiRoute, "?since=", since)),
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
var response = await client.SendAsync(requestMessage).ConfigureAwait(false);
|
||||
if(!response.IsSuccessStatusCode)
|
||||
{
|
||||
return await HandleErrorAsync<ListResponse<FolderResponse>>(response).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
var responseObj = JsonConvert.DeserializeObject<ListResponse<FolderResponse>>(responseContent);
|
||||
return ApiResult<ListResponse<FolderResponse>>.Success(responseObj, response.StatusCode);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return HandledWebException<ListResponse<FolderResponse>>();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
29
src/App/Resources/AppResources.Designer.cs
generated
@@ -520,6 +520,15 @@ namespace Bit.App.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Disable Analytics.
|
||||
/// </summary>
|
||||
public static string DisableGA {
|
||||
get {
|
||||
return ResourceManager.GetString("DisableGA", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Do you really want to delete? This cannot be undone..
|
||||
/// </summary>
|
||||
@@ -827,7 +836,7 @@ namespace Bit.App.Resources {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to (none).
|
||||
/// Looks up a localized string similar to No Folder.
|
||||
/// </summary>
|
||||
public static string FolderNone {
|
||||
get {
|
||||
@@ -1654,6 +1663,24 @@ namespace Bit.App.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Share Your Vault.
|
||||
/// </summary>
|
||||
public static string ShareVault {
|
||||
get {
|
||||
return ResourceManager.GetString("ShareVault", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Create an organization to securely share your logins with other users..
|
||||
/// </summary>
|
||||
public static string ShareVaultDescription {
|
||||
get {
|
||||
return ResourceManager.GetString("ShareVaultDescription", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Show.
|
||||
/// </summary>
|
||||
|
||||
@@ -222,7 +222,7 @@
|
||||
<value>Kansio poistettu.</value>
|
||||
</data>
|
||||
<data name="FolderNone" xml:space="preserve">
|
||||
<value>(muut)</value>
|
||||
<value>Ei kansioa</value>
|
||||
<comment>Logins that have no folder specified go in this special "catch-all" folder.</comment>
|
||||
</data>
|
||||
<data name="Folders" xml:space="preserve">
|
||||
@@ -792,4 +792,4 @@
|
||||
<data name="Beta" xml:space="preserve">
|
||||
<value>Beeta</value>
|
||||
</data>
|
||||
</root>
|
||||
</root>
|
||||
@@ -222,7 +222,7 @@
|
||||
<value>Dossier supprimé.</value>
|
||||
</data>
|
||||
<data name="FolderNone" xml:space="preserve">
|
||||
<value>(aucun)</value>
|
||||
<value>Pas de dossier</value>
|
||||
<comment>Logins that have no folder specified go in this special "catch-all" folder.</comment>
|
||||
</data>
|
||||
<data name="Folders" xml:space="preserve">
|
||||
@@ -757,7 +757,7 @@
|
||||
<data name="Translations" xml:space="preserve">
|
||||
<value>Traductions</value>
|
||||
</data>
|
||||
<data name="LoginsForUri" xml:space="preserve">
|
||||
<data name="LoginsForUri" xml:space="preserve">
|
||||
<value>Identifiants pour {0}</value>
|
||||
<comment>This is used for the autofill service. ex. "Logins for twitter.com"</comment>
|
||||
</data>
|
||||
@@ -816,4 +816,4 @@
|
||||
<data name="BitwardenAutofillServiceSearch" xml:space="preserve">
|
||||
<value>Vous recherchez un identifiant pour remplir automatiquement "{0}".</value>
|
||||
</data>
|
||||
</root>
|
||||
</root>
|
||||
@@ -222,7 +222,7 @@
|
||||
<value>Folder deleted.</value>
|
||||
</data>
|
||||
<data name="FolderNone" xml:space="preserve">
|
||||
<value>(none)</value>
|
||||
<value>No Folder</value>
|
||||
<comment>Logins that have no folder specified go in this special "catch-all" folder.</comment>
|
||||
</data>
|
||||
<data name="Folders" xml:space="preserve">
|
||||
@@ -302,6 +302,9 @@
|
||||
<value>Ok</value>
|
||||
<comment>Acknowledgement.</comment>
|
||||
</data>
|
||||
<data name="DisableGA" xml:space="preserve">
|
||||
<value>Disable Analytics</value>
|
||||
</data>
|
||||
<data name="Password" xml:space="preserve">
|
||||
<value>Password</value>
|
||||
<comment>Label for a password.</comment>
|
||||
@@ -816,4 +819,10 @@
|
||||
<data name="BitwardenAutofillServiceSearch" xml:space="preserve">
|
||||
<value>You are searching for an auto-fill login for "{0}".</value>
|
||||
</data>
|
||||
<data name="ShareVault" xml:space="preserve">
|
||||
<value>Share Your Vault</value>
|
||||
</data>
|
||||
<data name="ShareVaultDescription" xml:space="preserve">
|
||||
<value>Create an organization to securely share your logins with other users.</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -228,7 +228,7 @@
|
||||
<comment>Folder deleted.</comment>
|
||||
</data>
|
||||
<data name="FolderNone" xml:space="preserve">
|
||||
<value>(ingen)</value>
|
||||
<value>Ingen mapp</value>
|
||||
<comment>Logins that have no folder specified go in this special "catch-all" folder.</comment>
|
||||
</data>
|
||||
<data name="Folders" xml:space="preserve">
|
||||
@@ -868,4 +868,4 @@
|
||||
<data name="BitwardenAutofillServiceAlert" xml:space="preserve">
|
||||
<value>Det lättaste sättet att lägga till nya inloggningar till ditt valv är från bitwardens hjälpmedelsservice för automatisk ifyllnad. Lär dig mer om hur man använder bitwardens hjälpmedelsservice för automatisk ifyllnad genom att navigera till "Verktyg"-skärmen.</value>
|
||||
</data>
|
||||
</root>
|
||||
</root>
|
||||
@@ -222,7 +222,7 @@
|
||||
<value>文件夹已删除。</value>
|
||||
</data>
|
||||
<data name="FolderNone" xml:space="preserve">
|
||||
<value>(无)</value>
|
||||
<value>没有文件夹</value>
|
||||
<comment>Logins that have no folder specified go in this special "catch-all" folder.</comment>
|
||||
</data>
|
||||
<data name="Folders" xml:space="preserve">
|
||||
|
||||
41
src/App/Services/AppSettingsService.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using System;
|
||||
using Bit.App.Abstractions;
|
||||
using Plugin.Settings.Abstractions;
|
||||
|
||||
namespace Bit.App.Services
|
||||
{
|
||||
public class AppSettingsService : IAppSettingsService
|
||||
{
|
||||
private readonly ISettings _settings;
|
||||
|
||||
public AppSettingsService(
|
||||
ISettings settings)
|
||||
{
|
||||
_settings = settings;
|
||||
}
|
||||
|
||||
public bool Locked
|
||||
{
|
||||
get
|
||||
{
|
||||
return _settings.GetValueOrDefault(Constants.Locked, false);
|
||||
}
|
||||
set
|
||||
{
|
||||
_settings.AddOrUpdateValue(Constants.Locked, value);
|
||||
}
|
||||
}
|
||||
|
||||
public DateTime LastActivity
|
||||
{
|
||||
get
|
||||
{
|
||||
return _settings.GetValueOrDefault(Constants.LastActivityDate, DateTime.MinValue);
|
||||
}
|
||||
set
|
||||
{
|
||||
_settings.AddOrUpdateValue(Constants.LastActivityDate, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,10 @@ using System.Threading.Tasks;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Models.Api;
|
||||
using Plugin.Settings.Abstractions;
|
||||
using Bit.App.Models;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Bit.App.Services
|
||||
{
|
||||
@@ -19,6 +23,9 @@ namespace Bit.App.Services
|
||||
private readonly ISettings _settings;
|
||||
private readonly ICryptoService _cryptoService;
|
||||
private readonly IConnectApiRepository _connectApiRepository;
|
||||
private readonly IAccountsApiRepository _accountsApiRepository;
|
||||
private readonly IAppIdService _appIdService;
|
||||
private readonly IDeviceInfoService _deviceInfoService;
|
||||
|
||||
private string _email;
|
||||
private string _userId;
|
||||
@@ -30,13 +37,19 @@ namespace Bit.App.Services
|
||||
ITokenService tokenService,
|
||||
ISettings settings,
|
||||
ICryptoService cryptoService,
|
||||
IConnectApiRepository connectApiRepository)
|
||||
IConnectApiRepository connectApiRepository,
|
||||
IAccountsApiRepository accountsApiRepository,
|
||||
IAppIdService appIdService,
|
||||
IDeviceInfoService deviceInfoService)
|
||||
{
|
||||
_secureStorage = secureStorage;
|
||||
_tokenService = tokenService;
|
||||
_settings = settings;
|
||||
_cryptoService = cryptoService;
|
||||
_connectApiRepository = connectApiRepository;
|
||||
_accountsApiRepository = accountsApiRepository;
|
||||
_appIdService = appIdService;
|
||||
_deviceInfoService = deviceInfoService;
|
||||
}
|
||||
|
||||
public string UserId
|
||||
@@ -178,6 +191,11 @@ namespace Bit.App.Services
|
||||
}
|
||||
}
|
||||
|
||||
public bool BelongsToOrganization(string orgId)
|
||||
{
|
||||
return !string.IsNullOrWhiteSpace(orgId) && (_cryptoService.OrgKeys?.ContainsKey(orgId) ?? false);
|
||||
}
|
||||
|
||||
public void LogOut()
|
||||
{
|
||||
_tokenService.Token = null;
|
||||
@@ -185,16 +203,96 @@ namespace Bit.App.Services
|
||||
_tokenService.AuthBearer = null;
|
||||
UserId = null;
|
||||
Email = null;
|
||||
_cryptoService.Key = null;
|
||||
_cryptoService.ClearKeys();
|
||||
_settings.Remove(Constants.FirstVaultLoad);
|
||||
_settings.Remove(Constants.PushLastRegistrationDate);
|
||||
_settings.Remove(Constants.Locked);
|
||||
}
|
||||
|
||||
public async Task<ApiResult<TokenResponse>> TokenPostAsync(TokenRequest request)
|
||||
public async Task<FullLoginResult> TokenPostAsync(string email, string masterPassword)
|
||||
{
|
||||
// TODO: move more logic in here
|
||||
return await _connectApiRepository.PostTokenAsync(request);
|
||||
var result = new FullLoginResult();
|
||||
|
||||
var normalizedEmail = email.Trim().ToLower();
|
||||
var key = _cryptoService.MakeKeyFromPassword(masterPassword, normalizedEmail);
|
||||
|
||||
var request = new TokenRequest
|
||||
{
|
||||
Email = normalizedEmail,
|
||||
MasterPasswordHash = _cryptoService.HashPasswordBase64(key, masterPassword),
|
||||
Device = new DeviceRequest(_appIdService, _deviceInfoService)
|
||||
};
|
||||
|
||||
var response = await _connectApiRepository.PostTokenAsync(request);
|
||||
if(!response.Succeeded)
|
||||
{
|
||||
result.Success = false;
|
||||
result.ErrorMessage = response.Errors.FirstOrDefault()?.Message;
|
||||
return result;
|
||||
}
|
||||
|
||||
result.Success = true;
|
||||
if(response.Result.TwoFactorProviders != null && response.Result.TwoFactorProviders.Count > 0)
|
||||
{
|
||||
result.Key = key;
|
||||
result.MasterPasswordHash = request.MasterPasswordHash;
|
||||
result.TwoFactorRequired = true;
|
||||
return result;
|
||||
}
|
||||
|
||||
await ProcessLoginSuccessAsync(key, response.Result);
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<LoginResult> TokenPostTwoFactorAsync(string token, string email, string masterPasswordHash,
|
||||
SymmetricCryptoKey key)
|
||||
{
|
||||
var result = new LoginResult();
|
||||
|
||||
var request = new TokenRequest
|
||||
{
|
||||
Email = email.Trim().ToLower(),
|
||||
MasterPasswordHash = masterPasswordHash,
|
||||
Token = token.Trim().Replace(" ", ""),
|
||||
Provider = 0, // Authenticator app (only 1 provider for now, so hard coded)
|
||||
Device = new DeviceRequest(_appIdService, _deviceInfoService)
|
||||
};
|
||||
|
||||
var response = await _connectApiRepository.PostTokenAsync(request);
|
||||
if(!response.Succeeded)
|
||||
{
|
||||
result.Success = false;
|
||||
result.ErrorMessage = response.Errors.FirstOrDefault()?.Message;
|
||||
return result;
|
||||
}
|
||||
|
||||
result.Success = true;
|
||||
await ProcessLoginSuccessAsync(key, response.Result);
|
||||
return result;
|
||||
}
|
||||
|
||||
private async Task ProcessLoginSuccessAsync(SymmetricCryptoKey key, TokenResponse response)
|
||||
{
|
||||
if(response.PrivateKey != null)
|
||||
{
|
||||
_cryptoService.SetPrivateKey(new CipherString(response.PrivateKey));
|
||||
}
|
||||
|
||||
_cryptoService.Key = key;
|
||||
_tokenService.Token = response.AccessToken;
|
||||
_tokenService.RefreshToken = response.RefreshToken;
|
||||
UserId = _tokenService.TokenUserId;
|
||||
Email = _tokenService.TokenEmail;
|
||||
_settings.AddOrUpdateValue(Constants.LastLoginEmail, Email);
|
||||
|
||||
if(response.PrivateKey != null)
|
||||
{
|
||||
var profile = await _accountsApiRepository.GetProfileAsync();
|
||||
if(profile.Succeeded)
|
||||
{
|
||||
_cryptoService.SetOrgKeys(profile.Result);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,11 @@ using Bit.App.Abstractions;
|
||||
using Bit.App.Models;
|
||||
using PCLCrypto;
|
||||
using System.Linq;
|
||||
using Xamarin.Forms;
|
||||
using Bit.App.Enums;
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
using Plugin.Settings.Abstractions;
|
||||
using Bit.App.Models.Api;
|
||||
|
||||
namespace Bit.App.Services
|
||||
{
|
||||
@@ -13,28 +17,40 @@ namespace Bit.App.Services
|
||||
{
|
||||
private const string KeyKey = "key";
|
||||
private const string PreviousKeyKey = "previousKey";
|
||||
private const string PrivateKeyKey = "encPrivateKey";
|
||||
private const string OrgKeysKey = "encOrgKeys";
|
||||
private const int InitializationVectorSize = 16;
|
||||
|
||||
private readonly ISettings _settings;
|
||||
private readonly ISecureStorageService _secureStorage;
|
||||
private readonly IKeyDerivationService _keyDerivationService;
|
||||
private byte[] _key;
|
||||
private byte[] _previousKey;
|
||||
private SymmetricCryptoKey _key;
|
||||
private SymmetricCryptoKey _legacyEtmKey;
|
||||
private SymmetricCryptoKey _previousKey;
|
||||
private IDictionary<string, SymmetricCryptoKey> _orgKeys;
|
||||
private byte[] _privateKey;
|
||||
|
||||
public CryptoService(
|
||||
ISettings settings,
|
||||
ISecureStorageService secureStorage,
|
||||
IKeyDerivationService keyDerivationService)
|
||||
{
|
||||
_settings = settings;
|
||||
_secureStorage = secureStorage;
|
||||
_keyDerivationService = keyDerivationService;
|
||||
}
|
||||
|
||||
public byte[] Key
|
||||
public SymmetricCryptoKey Key
|
||||
{
|
||||
get
|
||||
{
|
||||
if(_key == null)
|
||||
if(_key == null && _secureStorage.Contains(KeyKey))
|
||||
{
|
||||
_key = _secureStorage.Retrieve(KeyKey);
|
||||
var keyBytes = _secureStorage.Retrieve(KeyKey);
|
||||
if(keyBytes != null)
|
||||
{
|
||||
_key = new SymmetricCryptoKey(keyBytes);
|
||||
}
|
||||
}
|
||||
|
||||
return _key;
|
||||
@@ -43,37 +59,29 @@ namespace Bit.App.Services
|
||||
{
|
||||
if(value != null)
|
||||
{
|
||||
_secureStorage.Store(KeyKey, value);
|
||||
_secureStorage.Store(KeyKey, value.Key);
|
||||
}
|
||||
else
|
||||
{
|
||||
PreviousKey = _key;
|
||||
_secureStorage.Delete(KeyKey);
|
||||
_key = null;
|
||||
_legacyEtmKey = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string Base64Key
|
||||
public SymmetricCryptoKey PreviousKey
|
||||
{
|
||||
get
|
||||
{
|
||||
if(Key == null)
|
||||
if(_previousKey == null && _secureStorage.Contains(PreviousKeyKey))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return Convert.ToBase64String(Key);
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] PreviousKey
|
||||
{
|
||||
get
|
||||
{
|
||||
if(_previousKey == null)
|
||||
{
|
||||
_previousKey = _secureStorage.Retrieve(PreviousKeyKey);
|
||||
var keyBytes = _secureStorage.Retrieve(PreviousKeyKey);
|
||||
if(keyBytes != null)
|
||||
{
|
||||
_previousKey = new SymmetricCryptoKey(keyBytes);
|
||||
}
|
||||
}
|
||||
|
||||
return _previousKey;
|
||||
@@ -82,7 +90,7 @@ namespace Bit.App.Services
|
||||
{
|
||||
if(value != null)
|
||||
{
|
||||
_secureStorage.Store(PreviousKeyKey, value);
|
||||
_secureStorage.Store(PreviousKeyKey, value.Key);
|
||||
_previousKey = value;
|
||||
}
|
||||
}
|
||||
@@ -102,18 +110,141 @@ namespace Bit.App.Services
|
||||
return Key != null;
|
||||
}
|
||||
|
||||
return !PreviousKey.SequenceEqual(Key);
|
||||
return !PreviousKey.Key.SequenceEqual(Key.Key);
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] EncKey => Key?.Take(16).ToArray();
|
||||
public byte[] MacKey => Key?.Skip(16).Take(16).ToArray();
|
||||
|
||||
public CipherString Encrypt(string plaintextValue)
|
||||
public byte[] PrivateKey
|
||||
{
|
||||
if(Key == null)
|
||||
get
|
||||
{
|
||||
throw new ArgumentNullException(nameof(Key));
|
||||
if(_privateKey == null && _settings.Contains(PrivateKeyKey))
|
||||
{
|
||||
var encPrivateKey = _settings.GetValueOrDefault<string>(PrivateKeyKey);
|
||||
var encPrivateKeyCs = new CipherString(encPrivateKey);
|
||||
try
|
||||
{
|
||||
_privateKey = DecryptToBytes(encPrivateKeyCs);
|
||||
}
|
||||
catch
|
||||
{
|
||||
_privateKey = null;
|
||||
Debug.WriteLine($"Cannot set private key. Decryption failed.");
|
||||
}
|
||||
}
|
||||
|
||||
return _privateKey;
|
||||
}
|
||||
}
|
||||
|
||||
public IDictionary<string, SymmetricCryptoKey> OrgKeys
|
||||
{
|
||||
get
|
||||
{
|
||||
if((!_orgKeys?.Any() ?? true) && _settings.Contains(OrgKeysKey))
|
||||
{
|
||||
var orgKeysEncDictJson = _settings.GetValueOrDefault<string>(OrgKeysKey);
|
||||
if(!string.IsNullOrWhiteSpace(orgKeysEncDictJson))
|
||||
{
|
||||
_orgKeys = new Dictionary<string, SymmetricCryptoKey>();
|
||||
var orgKeysDict = JsonConvert.DeserializeObject<IDictionary<string, string>>(orgKeysEncDictJson);
|
||||
foreach(var item in orgKeysDict)
|
||||
{
|
||||
try
|
||||
{
|
||||
var orgKeyCs = new CipherString(item.Value);
|
||||
var decOrgKeyBytes = RsaDecryptToBytes(orgKeyCs, PrivateKey);
|
||||
_orgKeys.Add(item.Key, new SymmetricCryptoKey(decOrgKeyBytes));
|
||||
}
|
||||
catch
|
||||
{
|
||||
Debug.WriteLine($"Cannot set org key {item.Key}. Decryption failed.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return _orgKeys;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetPrivateKey(CipherString privateKeyEnc)
|
||||
{
|
||||
if(privateKeyEnc != null)
|
||||
{
|
||||
_settings.AddOrUpdateValue(PrivateKeyKey, privateKeyEnc.EncryptedString);
|
||||
}
|
||||
else if(_settings.Contains(PrivateKeyKey))
|
||||
{
|
||||
_settings.Remove(PrivateKeyKey);
|
||||
_privateKey = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
_privateKey = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetOrgKeys(ProfileResponse profile)
|
||||
{
|
||||
var orgKeysEncDict = new Dictionary<string, string>();
|
||||
|
||||
if(profile?.Organizations?.Any() ?? false)
|
||||
{
|
||||
foreach(var org in profile.Organizations)
|
||||
{
|
||||
orgKeysEncDict.Add(org.Id, org.Key);
|
||||
}
|
||||
}
|
||||
|
||||
SetOrgKeys(orgKeysEncDict);
|
||||
}
|
||||
|
||||
public void SetOrgKeys(Dictionary<string, string> orgKeysEncDict)
|
||||
{
|
||||
if(orgKeysEncDict?.Any() ?? false)
|
||||
{
|
||||
var dictJson = JsonConvert.SerializeObject(orgKeysEncDict);
|
||||
_settings.AddOrUpdateValue(OrgKeysKey, dictJson);
|
||||
}
|
||||
else if(_settings.Contains(OrgKeysKey))
|
||||
{
|
||||
_settings.Remove(OrgKeysKey);
|
||||
_orgKeys = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
_orgKeys = null;
|
||||
}
|
||||
}
|
||||
|
||||
public SymmetricCryptoKey GetOrgKey(string orgId)
|
||||
{
|
||||
if(OrgKeys == null || !OrgKeys.ContainsKey(orgId))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return OrgKeys[orgId];
|
||||
}
|
||||
|
||||
public void ClearKeys()
|
||||
{
|
||||
SetOrgKeys((Dictionary<string, string>)null);
|
||||
Key = null;
|
||||
SetPrivateKey(null);
|
||||
}
|
||||
|
||||
public CipherString Encrypt(string plaintextValue, SymmetricCryptoKey key = null)
|
||||
{
|
||||
if(key == null)
|
||||
{
|
||||
key = Key;
|
||||
}
|
||||
|
||||
if(key == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(key));
|
||||
}
|
||||
|
||||
if(plaintextValue == null)
|
||||
@@ -124,44 +255,21 @@ namespace Bit.App.Services
|
||||
var plaintextBytes = Encoding.UTF8.GetBytes(plaintextValue);
|
||||
|
||||
var provider = WinRTCrypto.SymmetricKeyAlgorithmProvider.OpenAlgorithm(SymmetricAlgorithm.AesCbcPkcs7);
|
||||
// TODO: Turn on whenever ready to support encrypt-then-mac
|
||||
var cryptoKey = provider.CreateSymmetricKey(true ? EncKey : Key);
|
||||
var cryptoKey = provider.CreateSymmetricKey(key.EncKey);
|
||||
var iv = WinRTCrypto.CryptographicBuffer.GenerateRandom(provider.BlockLength);
|
||||
var encryptedBytes = WinRTCrypto.CryptographicEngine.Encrypt(cryptoKey, plaintextBytes, iv);
|
||||
// TODO: Turn on whenever ready to support encrypt-then-mac
|
||||
var mac = true ? ComputeMac(encryptedBytes, iv) : null;
|
||||
var mac = key.MacKey != null ? ComputeMacBase64(encryptedBytes, iv, key.MacKey) : null;
|
||||
|
||||
return new CipherString(Convert.ToBase64String(iv), Convert.ToBase64String(encryptedBytes), mac);
|
||||
return new CipherString(key.EncryptionType, Convert.ToBase64String(iv),
|
||||
Convert.ToBase64String(encryptedBytes), mac);
|
||||
}
|
||||
|
||||
public string Decrypt(CipherString encyptedValue)
|
||||
public string Decrypt(CipherString encyptedValue, SymmetricCryptoKey key = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
if(Key == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(Key));
|
||||
}
|
||||
|
||||
if(encyptedValue == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(encyptedValue));
|
||||
}
|
||||
|
||||
if(encyptedValue.Mac != null)
|
||||
{
|
||||
var computedMac = ComputeMac(encyptedValue.CipherTextBytes, encyptedValue.InitializationVectorBytes);
|
||||
if(computedMac != encyptedValue.Mac)
|
||||
{
|
||||
throw new InvalidOperationException("MAC failed.");
|
||||
}
|
||||
}
|
||||
|
||||
var provider = WinRTCrypto.SymmetricKeyAlgorithmProvider.OpenAlgorithm(SymmetricAlgorithm.AesCbcPkcs7);
|
||||
var cryptoKey = provider.CreateSymmetricKey(encyptedValue.Mac != null ? EncKey : Key);
|
||||
var decryptedBytes = WinRTCrypto.CryptographicEngine.Decrypt(cryptoKey, encyptedValue.CipherTextBytes,
|
||||
encyptedValue.InitializationVectorBytes);
|
||||
return Encoding.UTF8.GetString(decryptedBytes, 0, decryptedBytes.Length).TrimEnd('\0');
|
||||
var bytes = DecryptToBytes(encyptedValue, key);
|
||||
return Encoding.UTF8.GetString(bytes, 0, bytes.Length).TrimEnd('\0');
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
@@ -170,11 +278,98 @@ namespace Bit.App.Services
|
||||
}
|
||||
}
|
||||
|
||||
private string ComputeMac(byte[] ctBytes, byte[] ivBytes)
|
||||
public byte[] DecryptToBytes(CipherString encyptedValue, SymmetricCryptoKey key = null)
|
||||
{
|
||||
if(MacKey == null)
|
||||
if(key == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(MacKey));
|
||||
key = Key;
|
||||
}
|
||||
|
||||
if(key == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(key));
|
||||
}
|
||||
|
||||
if(encyptedValue == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(encyptedValue));
|
||||
}
|
||||
|
||||
if(encyptedValue.EncryptionType == Enums.EncryptionType.AesCbc128_HmacSha256_B64 &&
|
||||
key.EncryptionType == Enums.EncryptionType.AesCbc256_B64)
|
||||
{
|
||||
// Old encrypt-then-mac scheme, swap out the key
|
||||
if(_legacyEtmKey == null)
|
||||
{
|
||||
_legacyEtmKey = new SymmetricCryptoKey(key.Key, Enums.EncryptionType.AesCbc128_HmacSha256_B64);
|
||||
}
|
||||
|
||||
key = _legacyEtmKey;
|
||||
}
|
||||
|
||||
if(encyptedValue.EncryptionType != key.EncryptionType)
|
||||
{
|
||||
throw new ArgumentException("encType unavailable.");
|
||||
}
|
||||
|
||||
if(key.MacKey != null && !string.IsNullOrWhiteSpace(encyptedValue.Mac))
|
||||
{
|
||||
var computedMacBytes = ComputeMac(encyptedValue.CipherTextBytes,
|
||||
encyptedValue.InitializationVectorBytes, key.MacKey);
|
||||
if(!MacsEqual(key.MacKey, computedMacBytes, encyptedValue.MacBytes))
|
||||
{
|
||||
throw new InvalidOperationException("MAC failed.");
|
||||
}
|
||||
}
|
||||
|
||||
var provider = WinRTCrypto.SymmetricKeyAlgorithmProvider.OpenAlgorithm(SymmetricAlgorithm.AesCbcPkcs7);
|
||||
var cryptoKey = provider.CreateSymmetricKey(key.EncKey);
|
||||
var decryptedBytes = WinRTCrypto.CryptographicEngine.Decrypt(cryptoKey, encyptedValue.CipherTextBytes,
|
||||
encyptedValue.InitializationVectorBytes);
|
||||
return decryptedBytes;
|
||||
}
|
||||
|
||||
public byte[] RsaDecryptToBytes(CipherString encyptedValue, byte[] privateKey)
|
||||
{
|
||||
if(privateKey == null)
|
||||
{
|
||||
privateKey = PrivateKey;
|
||||
}
|
||||
|
||||
if(privateKey == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(privateKey));
|
||||
}
|
||||
|
||||
IAsymmetricKeyAlgorithmProvider provider = null;
|
||||
switch(encyptedValue.EncryptionType)
|
||||
{
|
||||
case EncryptionType.Rsa2048_OaepSha256_B64:
|
||||
provider = WinRTCrypto.AsymmetricKeyAlgorithmProvider.OpenAlgorithm(AsymmetricAlgorithm.RsaOaepSha256);
|
||||
break;
|
||||
case EncryptionType.Rsa2048_OaepSha1_B64:
|
||||
provider = WinRTCrypto.AsymmetricKeyAlgorithmProvider.OpenAlgorithm(AsymmetricAlgorithm.RsaOaepSha1);
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentException("EncryptionType unavailable.");
|
||||
}
|
||||
|
||||
var cryptoKey = provider.ImportKeyPair(privateKey, CryptographicPrivateKeyBlobType.Pkcs8RawPrivateKeyInfo);
|
||||
var decryptedBytes = WinRTCrypto.CryptographicEngine.Decrypt(cryptoKey, encyptedValue.CipherTextBytes);
|
||||
return decryptedBytes;
|
||||
}
|
||||
|
||||
private string ComputeMacBase64(byte[] ctBytes, byte[] ivBytes, byte[] macKey)
|
||||
{
|
||||
var mac = ComputeMac(ctBytes, ivBytes, macKey);
|
||||
return Convert.ToBase64String(mac);
|
||||
}
|
||||
|
||||
private byte[] ComputeMac(byte[] ctBytes, byte[] ivBytes, byte[] macKey)
|
||||
{
|
||||
if(macKey == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(macKey));
|
||||
}
|
||||
|
||||
if(ctBytes == null)
|
||||
@@ -188,13 +383,42 @@ namespace Bit.App.Services
|
||||
}
|
||||
|
||||
var algorithm = WinRTCrypto.MacAlgorithmProvider.OpenAlgorithm(MacAlgorithm.HmacSha256);
|
||||
var hasher = algorithm.CreateHash(MacKey);
|
||||
var hasher = algorithm.CreateHash(macKey);
|
||||
hasher.Append(ivBytes.Concat(ctBytes).ToArray());
|
||||
var mac = hasher.GetValueAndReset();
|
||||
return Convert.ToBase64String(mac);
|
||||
return mac;
|
||||
}
|
||||
|
||||
public byte[] MakeKeyFromPassword(string password, string salt)
|
||||
// Safely compare two MACs in a way that protects against timing attacks (Double HMAC Verification).
|
||||
// ref: https://www.nccgroup.trust/us/about-us/newsroom-and-events/blog/2011/february/double-hmac-verification/
|
||||
private bool MacsEqual(byte[] macKey, byte[] mac1, byte[] mac2)
|
||||
{
|
||||
var algorithm = WinRTCrypto.MacAlgorithmProvider.OpenAlgorithm(MacAlgorithm.HmacSha256);
|
||||
var hasher = algorithm.CreateHash(macKey);
|
||||
|
||||
hasher.Append(mac1);
|
||||
mac1 = hasher.GetValueAndReset();
|
||||
|
||||
hasher.Append(mac2);
|
||||
mac2 = hasher.GetValueAndReset();
|
||||
|
||||
if(mac1.Length != mac2.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for(int i = 0; i < mac2.Length; i++)
|
||||
{
|
||||
if(mac1[i] != mac2[i])
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public SymmetricCryptoKey MakeKeyFromPassword(string password, string salt)
|
||||
{
|
||||
if(password == null)
|
||||
{
|
||||
@@ -209,17 +433,17 @@ namespace Bit.App.Services
|
||||
var passwordBytes = Encoding.UTF8.GetBytes(password);
|
||||
var saltBytes = Encoding.UTF8.GetBytes(salt);
|
||||
|
||||
var key = _keyDerivationService.DeriveKey(passwordBytes, saltBytes, 5000);
|
||||
return key;
|
||||
var keyBytes = _keyDerivationService.DeriveKey(passwordBytes, saltBytes, 5000);
|
||||
return new SymmetricCryptoKey(keyBytes);
|
||||
}
|
||||
|
||||
public string MakeKeyFromPasswordBase64(string password, string salt)
|
||||
{
|
||||
var key = MakeKeyFromPassword(password, salt);
|
||||
return Convert.ToBase64String(key);
|
||||
return Convert.ToBase64String(key.Key);
|
||||
}
|
||||
|
||||
public byte[] HashPassword(byte[] key, string password)
|
||||
public byte[] HashPassword(SymmetricCryptoKey key, string password)
|
||||
{
|
||||
if(key == null)
|
||||
{
|
||||
@@ -232,11 +456,11 @@ namespace Bit.App.Services
|
||||
}
|
||||
|
||||
var passwordBytes = Encoding.UTF8.GetBytes(password);
|
||||
var hash = _keyDerivationService.DeriveKey(key, passwordBytes, 1);
|
||||
var hash = _keyDerivationService.DeriveKey(key.Key, passwordBytes, 1);
|
||||
return hash;
|
||||
}
|
||||
|
||||
public string HashPasswordBase64(byte[] key, string password)
|
||||
public string HashPasswordBase64(SymmetricCryptoKey key, string password)
|
||||
{
|
||||
var hash = HashPassword(key, password);
|
||||
return Convert.ToBase64String(hash);
|
||||
|
||||
@@ -9,19 +9,32 @@ namespace Bit.App.Services
|
||||
public class LockService : ILockService
|
||||
{
|
||||
private readonly ISettings _settings;
|
||||
private readonly IAppSettingsService _appSettings;
|
||||
private readonly IAuthService _authService;
|
||||
private readonly IFingerprint _fingerprint;
|
||||
|
||||
public LockService(
|
||||
ISettings settings,
|
||||
IAppSettingsService appSettings,
|
||||
IAuthService authService,
|
||||
IFingerprint fingerprint)
|
||||
{
|
||||
_settings = settings;
|
||||
_appSettings = appSettings;
|
||||
_authService = authService;
|
||||
_fingerprint = fingerprint;
|
||||
}
|
||||
|
||||
public void UpdateLastActivity(DateTime? activityDate = null)
|
||||
{
|
||||
if(_appSettings.Locked)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_appSettings.LastActivity = activityDate.GetValueOrDefault(DateTime.UtcNow);
|
||||
}
|
||||
|
||||
public LockType GetLockType(bool forceLock)
|
||||
{
|
||||
// Only lock if they are logged in
|
||||
@@ -31,7 +44,7 @@ namespace Bit.App.Services
|
||||
}
|
||||
|
||||
// Are we forcing a lock? (i.e. clicking a button to lock the app manually, immediately)
|
||||
if(!forceLock && !_settings.GetValueOrDefault(Constants.Locked, false))
|
||||
if(!forceLock && !_appSettings.Locked)
|
||||
{
|
||||
// Lock seconds tells if they want to lock the app or not
|
||||
var lockSeconds = _settings.GetValueOrDefault(Constants.SettingLockSeconds, 60 * 15);
|
||||
@@ -42,8 +55,7 @@ namespace Bit.App.Services
|
||||
|
||||
// Has it been longer than lockSeconds since the last time the app was used?
|
||||
var now = DateTime.UtcNow;
|
||||
var lastBackground = _settings.GetValueOrDefault(Constants.LastActivityDate, now.AddYears(-1));
|
||||
if((now - lastBackground).TotalSeconds < lockSeconds)
|
||||
if(now > _appSettings.LastActivity && (now - _appSettings.LastActivity).TotalSeconds < lockSeconds)
|
||||
{
|
||||
return LockType.None;
|
||||
}
|
||||
|
||||
@@ -64,17 +64,10 @@ namespace Bit.App.Services
|
||||
|
||||
Uri uri = null;
|
||||
DomainName domainName = null;
|
||||
var androidApp = false;
|
||||
var androidApp = UriIsAndroidApp(uriString);
|
||||
|
||||
if(!Uri.TryCreate(uriString, UriKind.Absolute, out uri) || !DomainName.TryParse(uri.Host, out domainName))
|
||||
{
|
||||
if(domainName == null)
|
||||
{
|
||||
androidApp = UriIsAndroidApp(uriString);
|
||||
}
|
||||
}
|
||||
|
||||
if(!androidApp && domainName == null)
|
||||
if(!androidApp &&
|
||||
(!Uri.TryCreate(uriString, UriKind.Absolute, out uri) || !DomainName.TryParse(uri.Host, out domainName)))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
@@ -91,8 +84,7 @@ namespace Bit.App.Services
|
||||
{
|
||||
matchingDomains.AddRange(eqDomain.Select(d => d).ToList());
|
||||
}
|
||||
|
||||
if(androidAppWebUriString != null && Array.IndexOf(eqDomain, androidAppWebUriString) >= 0)
|
||||
else if(androidAppWebUriString != null && Array.IndexOf(eqDomain, androidAppWebUriString) >= 0)
|
||||
{
|
||||
matchingFuzzyDomains.AddRange(eqDomain.Select(d => d).ToList());
|
||||
}
|
||||
@@ -108,7 +100,8 @@ namespace Bit.App.Services
|
||||
matchingDomains.Add(androidApp ? uriString : domainName.BaseDomain);
|
||||
}
|
||||
|
||||
if(androidApp && androidAppWebUriString != null && !matchingFuzzyDomains.Any())
|
||||
if(androidApp && androidAppWebUriString != null &&
|
||||
!matchingFuzzyDomains.Any() && !matchingDomains.Contains(androidAppWebUriString))
|
||||
{
|
||||
matchingFuzzyDomains.Add(androidAppWebUriString);
|
||||
}
|
||||
@@ -125,7 +118,7 @@ namespace Bit.App.Services
|
||||
continue;
|
||||
}
|
||||
|
||||
var loginUriString = new CipherString(login.Uri).Decrypt();
|
||||
var loginUriString = new CipherString(login.Uri).Decrypt(login.OrganizationId);
|
||||
if(string.IsNullOrWhiteSpace(loginUriString))
|
||||
{
|
||||
continue;
|
||||
|
||||
@@ -59,15 +59,30 @@ namespace Bit.App.Services
|
||||
{
|
||||
case Enums.PushType.SyncCipherUpdate:
|
||||
case Enums.PushType.SyncCipherCreate:
|
||||
var createUpdateMessage = values.ToObject<SyncCipherPushNotification>();
|
||||
if(createUpdateMessage.UserId != _authService.UserId)
|
||||
var cipherCreateUpdateMessage = values.ToObject<SyncCipherPushNotification>();
|
||||
if(cipherCreateUpdateMessage.OrganizationId == null &&
|
||||
cipherCreateUpdateMessage.UserId != _authService.UserId)
|
||||
{
|
||||
break;
|
||||
}
|
||||
_syncService.SyncAsync(createUpdateMessage.Id);
|
||||
else if(cipherCreateUpdateMessage.OrganizationId != null &&
|
||||
!_authService.BelongsToOrganization(cipherCreateUpdateMessage.OrganizationId))
|
||||
{
|
||||
break;
|
||||
}
|
||||
_syncService.SyncCipherAsync(cipherCreateUpdateMessage.Id);
|
||||
break;
|
||||
case Enums.PushType.SyncFolderUpdate:
|
||||
case Enums.PushType.SyncFolderCreate:
|
||||
var folderCreateUpdateMessage = values.ToObject<SyncFolderPushNotification>();
|
||||
if(folderCreateUpdateMessage.UserId != _authService.UserId)
|
||||
{
|
||||
break;
|
||||
}
|
||||
_syncService.SyncFolderAsync(folderCreateUpdateMessage.Id);
|
||||
break;
|
||||
case Enums.PushType.SyncFolderDelete:
|
||||
var folderDeleteMessage = values.ToObject<SyncCipherPushNotification>();
|
||||
var folderDeleteMessage = values.ToObject<SyncFolderPushNotification>();
|
||||
if(folderDeleteMessage.UserId != _authService.UserId)
|
||||
{
|
||||
break;
|
||||
@@ -76,20 +91,43 @@ namespace Bit.App.Services
|
||||
break;
|
||||
case Enums.PushType.SyncLoginDelete:
|
||||
var loginDeleteMessage = values.ToObject<SyncCipherPushNotification>();
|
||||
if(loginDeleteMessage.UserId != _authService.UserId)
|
||||
if(loginDeleteMessage.OrganizationId == null &&
|
||||
loginDeleteMessage.UserId != _authService.UserId)
|
||||
{
|
||||
break;
|
||||
}
|
||||
else if(loginDeleteMessage.OrganizationId != null &&
|
||||
!_authService.BelongsToOrganization(loginDeleteMessage.OrganizationId))
|
||||
{
|
||||
break;
|
||||
}
|
||||
_syncService.SyncDeleteLoginAsync(loginDeleteMessage.Id);
|
||||
break;
|
||||
case Enums.PushType.SyncCiphers:
|
||||
var cipherMessage = values.ToObject<SyncCiphersPushNotification>();
|
||||
case Enums.PushType.SyncVault:
|
||||
var cipherMessage = values.ToObject<SyncUserPushNotification>();
|
||||
if(cipherMessage.UserId != _authService.UserId)
|
||||
{
|
||||
break;
|
||||
}
|
||||
_syncService.FullSyncAsync(true);
|
||||
break;
|
||||
case Enums.PushType.SyncSettings:
|
||||
var domainMessage = values.ToObject<SyncUserPushNotification>();
|
||||
if(domainMessage.UserId != _authService.UserId)
|
||||
{
|
||||
break;
|
||||
}
|
||||
_syncService.SyncSettingsAsync();
|
||||
break;
|
||||
case Enums.PushType.SyncOrgKeys:
|
||||
var orgKeysMessage = values.ToObject<SyncUserPushNotification>();
|
||||
if(orgKeysMessage.UserId != _authService.UserId)
|
||||
{
|
||||
break;
|
||||
}
|
||||
_syncService.SyncProfileAsync();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ using Bit.App.Models.Api;
|
||||
using System.Collections.Generic;
|
||||
using Xamarin.Forms;
|
||||
using Newtonsoft.Json;
|
||||
using Bit.App.Models;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Bit.App.Services
|
||||
{
|
||||
@@ -22,6 +24,7 @@ namespace Bit.App.Services
|
||||
private readonly ILoginRepository _loginRepository;
|
||||
private readonly ISettingsRepository _settingsRepository;
|
||||
private readonly IAuthService _authService;
|
||||
private readonly ICryptoService _cryptoService;
|
||||
private readonly ISettings _settings;
|
||||
|
||||
public SyncService(
|
||||
@@ -34,6 +37,7 @@ namespace Bit.App.Services
|
||||
ILoginRepository loginRepository,
|
||||
ISettingsRepository settingsRepository,
|
||||
IAuthService authService,
|
||||
ICryptoService cryptoService,
|
||||
ISettings settings)
|
||||
{
|
||||
_cipherApiRepository = cipherApiRepository;
|
||||
@@ -45,12 +49,13 @@ namespace Bit.App.Services
|
||||
_loginRepository = loginRepository;
|
||||
_settingsRepository = settingsRepository;
|
||||
_authService = authService;
|
||||
_cryptoService = cryptoService;
|
||||
_settings = settings;
|
||||
}
|
||||
|
||||
public bool SyncInProgress { get; private set; }
|
||||
|
||||
public async Task<bool> SyncAsync(string id)
|
||||
public async Task<bool> SyncCipherAsync(string id)
|
||||
{
|
||||
if(!_authService.IsAuthenticated)
|
||||
{
|
||||
@@ -60,16 +65,8 @@ namespace Bit.App.Services
|
||||
SyncStarted();
|
||||
|
||||
var cipher = await _cipherApiRepository.GetByIdAsync(id).ConfigureAwait(false);
|
||||
if(!cipher.Succeeded)
|
||||
if(!CheckSuccess(cipher))
|
||||
{
|
||||
SyncCompleted(false);
|
||||
|
||||
if(Application.Current != null && (cipher.StatusCode == System.Net.HttpStatusCode.Forbidden
|
||||
|| cipher.StatusCode == System.Net.HttpStatusCode.Unauthorized))
|
||||
{
|
||||
MessagingCenter.Send(Application.Current, "Logout", (string)null);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -77,10 +74,6 @@ namespace Bit.App.Services
|
||||
{
|
||||
switch(cipher.Result.Type)
|
||||
{
|
||||
case Enums.CipherType.Folder:
|
||||
var folderData = new FolderData(cipher.Result, _authService.UserId);
|
||||
await _folderRepository.UpsertAsync(folderData).ConfigureAwait(false);
|
||||
break;
|
||||
case Enums.CipherType.Login:
|
||||
var loginData = new LoginData(cipher.Result, _authService.UserId);
|
||||
await _loginRepository.UpsertAsync(loginData).ConfigureAwait(false);
|
||||
@@ -100,6 +93,36 @@ namespace Bit.App.Services
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> SyncFolderAsync(string id)
|
||||
{
|
||||
if(!_authService.IsAuthenticated)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
SyncStarted();
|
||||
|
||||
var folder = await _folderApiRepository.GetByIdAsync(id).ConfigureAwait(false);
|
||||
if(!CheckSuccess(folder))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var folderData = new FolderData(folder.Result, _authService.UserId);
|
||||
await _folderRepository.UpsertAsync(folderData).ConfigureAwait(false);
|
||||
}
|
||||
catch(SQLite.SQLiteException)
|
||||
{
|
||||
SyncCompleted(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
SyncCompleted(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> SyncDeleteFolderAsync(string id, DateTime revisionDate)
|
||||
{
|
||||
if(!_authService.IsAuthenticated)
|
||||
@@ -144,6 +167,48 @@ namespace Bit.App.Services
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> SyncSettingsAsync()
|
||||
{
|
||||
if(!_authService.IsAuthenticated)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
SyncStarted();
|
||||
|
||||
var domains = await _settingsApiRepository.GetDomains(false).ConfigureAwait(false);
|
||||
if(!CheckSuccess(domains))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
await SyncDomainsAsync(domains.Result);
|
||||
|
||||
SyncCompleted(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> SyncProfileAsync()
|
||||
{
|
||||
if(!_authService.IsAuthenticated)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
SyncStarted();
|
||||
|
||||
var profile = await _accountsApiRepository.GetProfileAsync().ConfigureAwait(false);
|
||||
if(!CheckSuccess(profile))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
await SyncOrgKeysAsync(profile.Result);
|
||||
|
||||
SyncCompleted(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> FullSyncAsync(TimeSpan syncThreshold, bool forceSync = false)
|
||||
{
|
||||
DateTime? lastSync = _settings.GetValueOrDefault<DateTime?>(Constants.LastSync, null);
|
||||
@@ -171,37 +236,33 @@ namespace Bit.App.Services
|
||||
SyncStarted();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
var ciphers = await _cipherApiRepository.GetAsync().ConfigureAwait(false);
|
||||
var domains = await _settingsApiRepository.GetDomains(false).ConfigureAwait(false);
|
||||
|
||||
if(!ciphers.Succeeded || !domains.Succeeded)
|
||||
// Just check profile first to make sure we'll have a success with the API
|
||||
var profile = await _accountsApiRepository.GetProfileAsync().ConfigureAwait(false);
|
||||
if(!CheckSuccess(profile))
|
||||
{
|
||||
SyncCompleted(false);
|
||||
if(Application.Current == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if(ciphers.StatusCode == System.Net.HttpStatusCode.Forbidden ||
|
||||
ciphers.StatusCode == System.Net.HttpStatusCode.Unauthorized ||
|
||||
domains.StatusCode == System.Net.HttpStatusCode.Forbidden ||
|
||||
domains.StatusCode == System.Net.HttpStatusCode.Unauthorized)
|
||||
{
|
||||
MessagingCenter.Send(Application.Current, "Logout", (string)null);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
var logins = ciphers.Result.Data.Where(c => c.Type == Enums.CipherType.Login).ToDictionary(s => s.Id);
|
||||
var folders = ciphers.Result.Data.Where(c => c.Type == Enums.CipherType.Folder).ToDictionary(f => f.Id);
|
||||
var ciphers = await _cipherApiRepository.GetAsync().ConfigureAwait(false);
|
||||
var folders = await _folderApiRepository.GetAsync().ConfigureAwait(false);
|
||||
var domains = await _settingsApiRepository.GetDomains(false).ConfigureAwait(false);
|
||||
if(!CheckSuccess(ciphers) || !CheckSuccess(folders) || !CheckSuccess(domains))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var loginTask = SyncLoginsAsync(logins);
|
||||
var folderTask = SyncFoldersAsync(folders);
|
||||
var loginsDict = ciphers.Result.Data.Where(c => c.Type == Enums.CipherType.Login).ToDictionary(s => s.Id);
|
||||
var foldersDict = folders.Result.Data.ToDictionary(f => f.Id);
|
||||
|
||||
var loginTask = SyncLoginsAsync(loginsDict);
|
||||
var folderTask = SyncFoldersAsync(foldersDict);
|
||||
var domainsTask = SyncDomainsAsync(domains.Result);
|
||||
await Task.WhenAll(loginTask, folderTask, domainsTask).ConfigureAwait(false);
|
||||
var orgKeysTask = SyncOrgKeysAsync(profile.Result);
|
||||
await Task.WhenAll(loginTask, folderTask, domainsTask, orgKeysTask).ConfigureAwait(false);
|
||||
|
||||
if(folderTask.Exception != null || loginTask.Exception != null || domainsTask.Exception != null)
|
||||
if(folderTask.Exception != null || loginTask.Exception != null || domainsTask.Exception != null ||
|
||||
orgKeysTask.Exception != null)
|
||||
{
|
||||
SyncCompleted(false);
|
||||
return false;
|
||||
@@ -220,7 +281,7 @@ namespace Bit.App.Services
|
||||
return true;
|
||||
}
|
||||
|
||||
var accountRevisionDate = await _accountsApiRepository.GetAccountRevisionDate();
|
||||
var accountRevisionDate = await _accountsApiRepository.GetAccountRevisionDateAsync();
|
||||
if(accountRevisionDate.Succeeded && accountRevisionDate.Result.HasValue &&
|
||||
accountRevisionDate.Result.Value > lastSync)
|
||||
{
|
||||
@@ -236,7 +297,7 @@ namespace Bit.App.Services
|
||||
return false;
|
||||
}
|
||||
|
||||
private async Task SyncFoldersAsync(IDictionary<string, CipherResponse> serverFolders)
|
||||
private async Task SyncFoldersAsync(IDictionary<string, FolderResponse> serverFolders)
|
||||
{
|
||||
if(!_authService.IsAuthenticated)
|
||||
{
|
||||
@@ -330,6 +391,25 @@ namespace Bit.App.Services
|
||||
catch(SQLite.SQLiteException) { }
|
||||
}
|
||||
|
||||
private async Task SyncOrgKeysAsync(ProfileResponse profile)
|
||||
{
|
||||
if(_cryptoService.PrivateKey == null && (profile.Organizations?.Any() ?? false))
|
||||
{
|
||||
var keys = await _accountsApiRepository.GetKeys();
|
||||
if(!CheckSuccess(keys))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if(!string.IsNullOrWhiteSpace(keys.Result.PrivateKey))
|
||||
{
|
||||
_cryptoService.SetPrivateKey(new CipherString(keys.Result.PrivateKey));
|
||||
}
|
||||
}
|
||||
|
||||
_cryptoService.SetOrgKeys(profile);
|
||||
}
|
||||
|
||||
private void SyncStarted()
|
||||
{
|
||||
if(Application.Current == null)
|
||||
@@ -351,5 +431,23 @@ namespace Bit.App.Services
|
||||
SyncInProgress = false;
|
||||
MessagingCenter.Send(Application.Current, "SyncCompleted", successfully);
|
||||
}
|
||||
|
||||
private bool CheckSuccess<T>(ApiResult<T> result)
|
||||
{
|
||||
if(!result.Succeeded)
|
||||
{
|
||||
SyncCompleted(false);
|
||||
|
||||
if(Application.Current != null && (result.StatusCode == System.Net.HttpStatusCode.Forbidden
|
||||
|| result.StatusCode == System.Net.HttpStatusCode.Unauthorized))
|
||||
{
|
||||
MessagingCenter.Send(Application.Current, "Logout", (string)null);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,8 +19,10 @@ namespace Bit.App
|
||||
|
||||
private void Init()
|
||||
{
|
||||
//BaseAddress = new Uri("http://192.168.1.3:4000");
|
||||
BaseAddress = new Uri("https://api.bitwarden.com");
|
||||
//BaseAddress = new Uri("http://169.254.80.80:4000"); // Desktop from VS Android Emulator
|
||||
//BaseAddress = new Uri("http://192.168.1.8:4000"); // Desktop
|
||||
//BaseAddress = new Uri("https://preview-api.bitwarden.com"); // Preview
|
||||
BaseAddress = new Uri("https://api.bitwarden.com"); // Production
|
||||
DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace Bit.App
|
||||
{
|
||||
public static class Extentions
|
||||
{
|
||||
public static CipherString Encrypt(this string s)
|
||||
public static CipherString Encrypt(this string s, string orgId = null)
|
||||
{
|
||||
if(s == null)
|
||||
{
|
||||
@@ -18,6 +18,12 @@ namespace Bit.App
|
||||
}
|
||||
|
||||
var cryptoService = Resolver.Resolve<ICryptoService>();
|
||||
|
||||
if(!string.IsNullOrWhiteSpace(orgId))
|
||||
{
|
||||
return cryptoService.Encrypt(s, cryptoService.GetOrgKey(orgId));
|
||||
}
|
||||
|
||||
return cryptoService.Encrypt(s);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="Acr.UserDialogs" version="6.3.3" targetFramework="portable45-net45+win8+wpa81" />
|
||||
<package id="Acr.UserDialogs" version="6.3.10" targetFramework="portable45-net45+win8+wpa81" />
|
||||
<package id="CommonServiceLocator" version="1.3" targetFramework="portable45-net45+win8+wpa81" />
|
||||
<package id="HockeySDK.Xamarin" version="4.1.0" targetFramework="portable45-net45+win8+wpa81" />
|
||||
<package id="HockeySDK.Xamarin" version="4.1.2" targetFramework="portable45-net45+win8+wpa81" />
|
||||
<package id="Newtonsoft.Json" version="9.0.1" targetFramework="portable45-net45+win8+wpa81" />
|
||||
<package id="PCLCrypto" version="2.0.147" targetFramework="portable45-net45+win8+wpa81" />
|
||||
<package id="PInvoke.BCrypt" version="0.3.152" targetFramework="portable45-net45+win8+wpa81" />
|
||||
@@ -18,11 +18,11 @@
|
||||
<package id="SQLitePCLRaw.core" version="1.1.2" targetFramework="portable45-net45+win8+wpa81" />
|
||||
<package id="Unity" version="3.5.1405-prerelease" targetFramework="portable45-net45+win8+wpa81" />
|
||||
<package id="Validation" version="2.3.7" targetFramework="portable45-net45+win8+wpa81" />
|
||||
<package id="Xam.Plugin.Connectivity" version="2.2.12" targetFramework="portable45-net45+win8+wpa81" />
|
||||
<package id="Xam.Plugin.Connectivity" version="2.3.0" targetFramework="portable45-net45+win8+wpa81" />
|
||||
<package id="Xam.Plugin.PushNotification" version="1.2.4" targetFramework="portable45-net45+win8+wpa81" developmentDependency="true" />
|
||||
<package id="Xam.Plugins.Settings" version="2.5.1.0" targetFramework="portable45-net45+win8+wpa81" />
|
||||
<package id="Xamarin.FFImageLoading" version="2.2.8" targetFramework="portable45-net45+win8+wpa81" />
|
||||
<package id="Xamarin.FFImageLoading.Forms" version="2.2.8" targetFramework="portable45-net45+win8+wpa81" />
|
||||
<package id="Xamarin.Forms" version="2.3.3.180" targetFramework="portable45-net45+win8+wpa81" />
|
||||
<package id="Xam.Plugins.Settings" version="2.5.4" targetFramework="portable45-net45+win8+wpa81" />
|
||||
<package id="Xamarin.FFImageLoading" version="2.2.9" targetFramework="portable45-net45+win8+wpa81" />
|
||||
<package id="Xamarin.FFImageLoading.Forms" version="2.2.9" targetFramework="portable45-net45+win8+wpa81" />
|
||||
<package id="Xamarin.Forms" version="2.3.4.231" targetFramework="portable45-net45+win8+wpa81" />
|
||||
<package id="XLabs.IoC" version="2.0.5782" targetFramework="portable45-net45+win8+wpa81" />
|
||||
</packages>
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using Bit.App.Abstractions;
|
||||
using Google.Analytics;
|
||||
using Plugin.Settings.Abstractions;
|
||||
|
||||
namespace Bit.iOS.Core.Services
|
||||
{
|
||||
@@ -8,25 +9,22 @@ namespace Bit.iOS.Core.Services
|
||||
{
|
||||
private readonly ITracker _tracker;
|
||||
private readonly IAuthService _authService;
|
||||
private bool _setUserId = true;
|
||||
|
||||
public GoogleAnalyticsService(
|
||||
IAppIdService appIdService,
|
||||
IAuthService authService)
|
||||
IAuthService authService,
|
||||
ISettings settings)
|
||||
{
|
||||
_authService = authService;
|
||||
|
||||
Gai.SharedInstance.DispatchInterval = 10;
|
||||
Gai.SharedInstance.TrackUncaughtExceptions = true;
|
||||
Gai.SharedInstance.TrackUncaughtExceptions = false;
|
||||
_tracker = Gai.SharedInstance.GetTracker("UA-81915606-1");
|
||||
_tracker.SetAllowIdfaCollection(true);
|
||||
_tracker.Set(GaiConstants.ClientId, appIdService.AnonymousAppId);
|
||||
}
|
||||
|
||||
public void RefreshUserId()
|
||||
{
|
||||
_tracker.Set(GaiConstants.UserId, null);
|
||||
_setUserId = true;
|
||||
var gaOptOut = settings.GetValueOrDefault(App.Constants.SettingGaOptOut, false);
|
||||
SetAppOptOut(gaOptOut);
|
||||
}
|
||||
|
||||
public void TrackAppEvent(string eventName, string label = null)
|
||||
@@ -41,7 +39,6 @@ namespace Bit.iOS.Core.Services
|
||||
|
||||
public void TrackEvent(string category, string eventName, string label = null)
|
||||
{
|
||||
SetUserId();
|
||||
var dict = DictionaryBuilder.CreateEvent(category, eventName, label, null).Build();
|
||||
_tracker.Send(dict);
|
||||
Gai.SharedInstance.Dispatch();
|
||||
@@ -49,14 +46,12 @@ namespace Bit.iOS.Core.Services
|
||||
|
||||
public void TrackException(string message, bool fatal)
|
||||
{
|
||||
SetUserId();
|
||||
var dict = DictionaryBuilder.CreateException(message, fatal).Build();
|
||||
_tracker.Send(dict);
|
||||
}
|
||||
|
||||
public void TrackPage(string pageName)
|
||||
{
|
||||
SetUserId();
|
||||
_tracker.Set(GaiConstants.ScreenName, pageName);
|
||||
var dict = DictionaryBuilder.CreateScreenView().Build();
|
||||
_tracker.Send(dict);
|
||||
@@ -70,13 +65,9 @@ namespace Bit.iOS.Core.Services
|
||||
});
|
||||
}
|
||||
|
||||
private void SetUserId()
|
||||
public void SetAppOptOut(bool optOut)
|
||||
{
|
||||
if(_setUserId && _authService.IsAuthenticated)
|
||||
{
|
||||
_tracker.Set(GaiConstants.UserId, _authService.UserId);
|
||||
_setUserId = false;
|
||||
}
|
||||
Gai.SharedInstance.OptOut = optOut;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="..\..\packages\Xamarin.Build.Download.0.3.0\build\Xamarin.Build.Download.props" Condition="Exists('..\..\packages\Xamarin.Build.Download.0.3.0\build\Xamarin.Build.Download.props')" />
|
||||
<Import Project="..\..\packages\Xamarin.Build.Download.0.4.3\build\Xamarin.Build.Download.props" Condition="Exists('..\..\packages\Xamarin.Build.Download.0.4.3\build\Xamarin.Build.Download.props')" />
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||
@@ -35,28 +35,23 @@
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="Google.Analytics, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\Xamarin.Google.iOS.Analytics.3.17.0\lib\Xamarin.iOS10\Google.Analytics.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
<HintPath>..\..\packages\Xamarin.Google.iOS.Analytics.3.17.0.1\lib\Xamarin.iOS10\Google.Analytics.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="HockeySDK, Version=1.0.6103.22142, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\HockeySDK.Xamarin.4.1.0\lib\Xamarin.iOS10\HockeySDK.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
<Reference Include="HockeySDK, Version=1.0.6288.33981, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\HockeySDK.Xamarin.4.1.2\lib\Xamarin.iOS10\HockeySDK.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="HockeySDK.iOSBindings, Version=0.0.0.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\HockeySDK.Xamarin.4.1.0\lib\Xamarin.iOS10\HockeySDK.iOSBindings.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
<HintPath>..\..\packages\HockeySDK.Xamarin.4.1.2\lib\Xamarin.iOS10\HockeySDK.iOSBindings.dll</HintPath>
|
||||
</Reference>
|
||||
<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>
|
||||
<Private>True</Private>
|
||||
</Reference>
|
||||
<Reference Include="Plugin.Settings, Version=2.5.1.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\Xam.Plugins.Settings.2.5.1.0\lib\Xamarin.iOS10\Plugin.Settings.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
<Reference Include="Plugin.Settings, Version=2.5.4.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\Xam.Plugins.Settings.2.5.4\lib\Xamarin.iOS10\Plugin.Settings.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Plugin.Settings.Abstractions, Version=2.5.1.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\Xam.Plugins.Settings.2.5.1.0\lib\Xamarin.iOS10\Plugin.Settings.Abstractions.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
<Reference Include="Plugin.Settings.Abstractions, Version=2.5.4.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\Xam.Plugins.Settings.2.5.4\lib\Xamarin.iOS10\Plugin.Settings.Abstractions.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="SQLite-net, Version=1.1.0.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\sqlite-net-pcl.1.2.1\lib\portable-net45+wp8+wpa81+win8+MonoAndroid10+MonoTouch10+Xamarin.iOS10\SQLite-net.dll</HintPath>
|
||||
@@ -138,10 +133,10 @@
|
||||
<PropertyGroup>
|
||||
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
|
||||
</PropertyGroup>
|
||||
<Error Condition="!Exists('..\..\packages\Xamarin.Build.Download.0.3.0\build\Xamarin.Build.Download.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\Xamarin.Build.Download.0.3.0\build\Xamarin.Build.Download.props'))" />
|
||||
<Error Condition="!Exists('..\..\packages\Xamarin.Build.Download.0.3.0\build\Xamarin.Build.Download.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\Xamarin.Build.Download.0.3.0\build\Xamarin.Build.Download.targets'))" />
|
||||
<Error Condition="!Exists('..\..\packages\Xamarin.Google.iOS.Analytics.3.17.0\build\Xamarin.Google.iOS.Analytics.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\Xamarin.Google.iOS.Analytics.3.17.0\build\Xamarin.Google.iOS.Analytics.targets'))" />
|
||||
<Error Condition="!Exists('..\..\packages\Xamarin.Build.Download.0.4.3\build\Xamarin.Build.Download.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\Xamarin.Build.Download.0.4.3\build\Xamarin.Build.Download.props'))" />
|
||||
<Error Condition="!Exists('..\..\packages\Xamarin.Build.Download.0.4.3\build\Xamarin.Build.Download.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\Xamarin.Build.Download.0.4.3\build\Xamarin.Build.Download.targets'))" />
|
||||
<Error Condition="!Exists('..\..\packages\Xamarin.Google.iOS.Analytics.3.17.0.1\build\Xamarin.Google.iOS.Analytics.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\Xamarin.Google.iOS.Analytics.3.17.0.1\build\Xamarin.Google.iOS.Analytics.targets'))" />
|
||||
</Target>
|
||||
<Import Project="..\..\packages\Xamarin.Build.Download.0.3.0\build\Xamarin.Build.Download.targets" Condition="Exists('..\..\packages\Xamarin.Build.Download.0.3.0\build\Xamarin.Build.Download.targets')" />
|
||||
<Import Project="..\..\packages\Xamarin.Google.iOS.Analytics.3.17.0\build\Xamarin.Google.iOS.Analytics.targets" Condition="Exists('..\..\packages\Xamarin.Google.iOS.Analytics.3.17.0\build\Xamarin.Google.iOS.Analytics.targets')" />
|
||||
<Import Project="..\..\packages\Xamarin.Build.Download.0.4.3\build\Xamarin.Build.Download.targets" Condition="Exists('..\..\packages\Xamarin.Build.Download.0.4.3\build\Xamarin.Build.Download.targets')" />
|
||||
<Import Project="..\..\packages\Xamarin.Google.iOS.Analytics.3.17.0.1\build\Xamarin.Google.iOS.Analytics.targets" Condition="Exists('..\..\packages\Xamarin.Google.iOS.Analytics.3.17.0.1\build\Xamarin.Google.iOS.Analytics.targets')" />
|
||||
</Project>
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="HockeySDK.Xamarin" version="4.1.0" targetFramework="xamarinios10" />
|
||||
<package id="HockeySDK.Xamarin" version="4.1.2" targetFramework="xamarinios10" />
|
||||
<package id="Newtonsoft.Json" version="9.0.1" targetFramework="xamarinios10" />
|
||||
<package id="sqlite-net-pcl" version="1.2.1" targetFramework="xamarinios10" />
|
||||
<package id="SQLitePCL.bundle_green" version="0.9.3" targetFramework="xamarinios10" />
|
||||
@@ -8,8 +8,8 @@
|
||||
<package id="SQLitePCLRaw.bundle_green" version="1.1.2" targetFramework="xamarinios10" />
|
||||
<package id="SQLitePCLRaw.core" version="1.1.2" targetFramework="xamarinios10" />
|
||||
<package id="SQLitePCLRaw.provider.sqlite3.ios_unified" version="1.1.2" targetFramework="xamarinios10" />
|
||||
<package id="Xam.Plugins.Settings" version="2.5.1.0" targetFramework="xamarinios10" />
|
||||
<package id="Xamarin.Build.Download" version="0.3.0" targetFramework="xamarinios10" />
|
||||
<package id="Xamarin.Google.iOS.Analytics" version="3.17.0" targetFramework="xamarinios10" />
|
||||
<package id="Xam.Plugins.Settings" version="2.5.4" targetFramework="xamarinios10" />
|
||||
<package id="Xamarin.Build.Download" version="0.4.3" targetFramework="xamarinios10" />
|
||||
<package id="Xamarin.Google.iOS.Analytics" version="3.17.0.1" targetFramework="xamarinios10" />
|
||||
<package id="XLabs.IoC" version="2.0.5782" targetFramework="xamarinios10" />
|
||||
</packages>
|
||||
@@ -4,8 +4,6 @@
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>iOS.Extension</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.8bit.bitwarden.find-login-action-extension</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
@@ -13,11 +11,11 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>XPC!</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.4.1</string>
|
||||
<string>1.5.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>11</string>
|
||||
<string>13</string>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionAttributes</key>
|
||||
@@ -74,6 +72,8 @@
|
||||
<string>es</string>
|
||||
<string>zh-Hans</string>
|
||||
<string>sv</string>
|
||||
<string>fi</string>
|
||||
<string>fr</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -30,7 +30,7 @@ namespace Bit.iOS.Extension
|
||||
private readonly JsonSerializerSettings _jsonSettings =
|
||||
new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore };
|
||||
private IGoogleAnalyticsService _googleAnalyticsService;
|
||||
private ISettings _settings;
|
||||
private ILockService _lockService;
|
||||
|
||||
public LoadingViewController(IntPtr handle) : base(handle)
|
||||
{ }
|
||||
@@ -44,7 +44,7 @@ namespace Bit.iOS.Extension
|
||||
View.BackgroundColor = new UIColor(red: 0.94f, green: 0.94f, blue: 0.96f, alpha: 1.0f);
|
||||
_context.ExtContext = ExtensionContext;
|
||||
_googleAnalyticsService = Resolver.Resolve<IGoogleAnalyticsService>();
|
||||
_settings = Resolver.Resolve<ISettings>();
|
||||
_lockService = Resolver.Resolve<ILockService>();
|
||||
|
||||
if(!_setupHockeyApp)
|
||||
{
|
||||
@@ -175,7 +175,7 @@ namespace Bit.iOS.Extension
|
||||
private void ContinueOn()
|
||||
{
|
||||
Debug.WriteLine("BW Log, Segue to setup, login add or list.");
|
||||
_settings.AddOrUpdateValue(App.Constants.LastActivityDate, DateTime.UtcNow);
|
||||
_lockService.UpdateLastActivity();
|
||||
|
||||
if(_context.ProviderType == Constants.UTTypeAppExtensionSaveLoginAction)
|
||||
{
|
||||
@@ -240,7 +240,7 @@ namespace Bit.iOS.Extension
|
||||
|
||||
if(itemData != null)
|
||||
{
|
||||
_settings.AddOrUpdateValue(App.Constants.LastActivityDate, DateTime.UtcNow);
|
||||
_lockService.UpdateLastActivity();
|
||||
_googleAnalyticsService.TrackExtensionEvent("AutoFilled", _context.ProviderType);
|
||||
}
|
||||
else
|
||||
@@ -282,6 +282,8 @@ namespace Bit.iOS.Extension
|
||||
.RegisterType<IHttpService, HttpService>(new ContainerControlledLifetimeManager())
|
||||
.RegisterType<ITokenService, TokenService>(new ContainerControlledLifetimeManager())
|
||||
.RegisterType<ISettingsService, SettingsService>(new ContainerControlledLifetimeManager())
|
||||
.RegisterType<IDeviceInfoService, DeviceInfoService>(new ContainerControlledLifetimeManager())
|
||||
.RegisterType<IAppSettingsService, AppSettingsService>(new ContainerControlledLifetimeManager())
|
||||
// Repositories
|
||||
.RegisterType<IFolderRepository, FolderRepository>(new ContainerControlledLifetimeManager())
|
||||
.RegisterType<IFolderApiRepository, FolderApiRepository>(new ContainerControlledLifetimeManager())
|
||||
@@ -289,6 +291,7 @@ namespace Bit.iOS.Extension
|
||||
.RegisterType<ILoginApiRepository, LoginApiRepository>(new ContainerControlledLifetimeManager())
|
||||
.RegisterType<IConnectApiRepository, ConnectApiRepository>(new ContainerControlledLifetimeManager())
|
||||
.RegisterType<ISettingsRepository, SettingsRepository>(new ContainerControlledLifetimeManager())
|
||||
.RegisterType<IAccountsApiRepository, AccountsApiRepository>(new ContainerControlledLifetimeManager())
|
||||
// Other
|
||||
.RegisterInstance(CrossConnectivity.Current, new ContainerControlledLifetimeManager())
|
||||
.RegisterInstance(CrossFingerprint.Current, new ContainerControlledLifetimeManager());
|
||||
@@ -307,7 +310,8 @@ namespace Bit.iOS.Extension
|
||||
localizeService.SetLocale(ci);
|
||||
}
|
||||
|
||||
private bool ProcessItemProvider(NSItemProvider itemProvider, string type, Action<NSDictionary> action)
|
||||
private bool ProcessItemProvider(NSItemProvider itemProvider, string type, Action<NSDictionary> dictAction,
|
||||
Action<NSUrl> urlAction = null)
|
||||
{
|
||||
if(!itemProvider.HasItemConformingTo(type))
|
||||
{
|
||||
@@ -322,8 +326,21 @@ namespace Bit.iOS.Extension
|
||||
}
|
||||
|
||||
_context.ProviderType = type;
|
||||
|
||||
var dict = list as NSDictionary;
|
||||
action(dict);
|
||||
if(dict != null && dictAction != null)
|
||||
{
|
||||
dictAction(dict);
|
||||
}
|
||||
else if(list is NSUrl && urlAction != null)
|
||||
{
|
||||
var url = list as NSUrl;
|
||||
urlAction(url);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception("Cannot parse list for action.");
|
||||
}
|
||||
|
||||
_googleAnalyticsService.TrackExtensionEvent("ProcessItemProvider", type);
|
||||
|
||||
@@ -391,6 +408,12 @@ namespace Bit.iOS.Extension
|
||||
}
|
||||
|
||||
_context.Details = DeserializeDictionary<PageDetails>(dict[Constants.AppExtensionWebViewPageDetails] as NSDictionary);
|
||||
}, (url) =>
|
||||
{
|
||||
if(url != null)
|
||||
{
|
||||
_context.UrlString = url.AbsoluteString;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||