Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PM-3089] mTLS - Authenticate with Client Certificate #2629

Closed
wants to merge 19 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
22 changes: 15 additions & 7 deletions src/Android/Android.csproj
Expand Up @@ -30,6 +30,13 @@
<WarningLevel>3</WarningLevel>
<AndroidSupportedAbis />
<JavaMaximumHeapSize>1G</JavaMaximumHeapSize>
<AotAssemblies>false</AotAssemblies>
<EnableLLVM>false</EnableLLVM>
<AndroidEnableProfiledAot>false</AndroidEnableProfiledAot>
<BundleAssemblies>false</BundleAssemblies>
<MandroidI18n />
<AndroidUseAapt2>true</AndroidUseAapt2>
<AndroidLinkMode>None</AndroidLinkMode>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugSymbols>false</DebugSymbols>
Expand Down Expand Up @@ -132,13 +139,16 @@
<Compile Include="Renderers\CustomEntryRenderer.cs" />
<Compile Include="Renderers\CustomSearchBarRenderer.cs" />
<Compile Include="Renderers\HybridWebViewRenderer.cs" />
<Compile Include="Security\AndroidHttpsClientHandler.cs" />
<Compile Include="Security\X509CertificateChainSpec.cs" />
<Compile Include="Services\AndroidPushNotificationService.cs" />
<Compile Include="Services\AndroidLogService.cs" />
<Compile Include="MainApplication.cs" />
<Compile Include="MainActivity.cs" />
<Compile Include="Resources\Resource.designer.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Services\BiometricService.cs" />
<Compile Include="Services\CertificateService.cs" />
<Compile Include="Services\CryptoPrimitiveService.cs" />
<Compile Include="Services\DeviceActionService.cs" />
<Compile Include="Services\LocalizeService.cs" />
Expand Down Expand Up @@ -230,8 +240,10 @@
<AndroidResource Include="Resources\drawable\logo_rounded.xml" />
<AndroidResource Include="Resources\drawable-night-v26\splash_screen_round.xml" />
<AndroidResource Include="Resources\drawable\ic_notification.xml">
<SubType></SubType>
<Generator></Generator>
<SubType>
</SubType>
<Generator>
</Generator>
</AndroidResource>
<AndroidResource Include="Resources\layout\validatable_input_dialog_layout.xml">
<SubType></SubType>
Expand Down Expand Up @@ -310,10 +322,6 @@
<SubType>Designer</SubType>
</AndroidResource>
</ItemGroup>
<ItemGroup>
<Folder Include="Resources\values-v30\" />
<Folder Include="Resources\drawable-v26\" />
<Folder Include="Resources\drawable-night-v26\" />
</ItemGroup>
<ItemGroup />
<Import Project="$(MSBuildExtensionsPath)\Xamarin\Android\Xamarin.Android.CSharp.targets" />
</Project>
26 changes: 25 additions & 1 deletion src/Android/MainActivity.cs
@@ -1,4 +1,5 @@
using System;
using System.Collections.Concurrent;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
Expand All @@ -10,11 +11,11 @@
using Android.OS;
using Android.Runtime;
using Android.Views;
using AndroidX.Activity.Result;
using Bit.App.Abstractions;
using Bit.App.Models;
using Bit.App.Resources;
using Bit.App.Utilities;
using Bit.Core;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Utilities;
Expand All @@ -26,6 +27,7 @@
using Xamarin.Essentials;
using ZXing.Net.Mobile.Android;
using FileProvider = AndroidX.Core.Content.FileProvider;
using Task = System.Threading.Tasks.Task;

namespace Bit.Droid
{
Expand All @@ -51,8 +53,13 @@ public class MainActivity : Xamarin.Forms.Platform.Android.FormsAppCompatActivit
private Java.Util.Regex.Pattern _otpPattern =
Java.Util.Regex.Pattern.Compile("^.*?([cbdefghijklnrtuv]{32,64})$");

private string stamp = "";
private static readonly ConcurrentDictionary<int, TaskCompletionSource<ActivityResult>> _oneTimeActivityListeners = new ConcurrentDictionary<int, TaskCompletionSource<ActivityResult>>();

protected override void OnCreate(Bundle savedInstanceState)
{
stamp = DateTime.UtcNow.ToString();

var eventUploadIntent = new Intent(this, typeof(EventUploadReceiver));
_eventUploadPendingIntent = PendingIntent.GetBroadcast(this, 0, eventUploadIntent,
AndroidHelpers.AddPendingIntentMutabilityFlag(PendingIntentFlags.UpdateCurrent, false));
Expand Down Expand Up @@ -232,6 +239,15 @@ protected override void OnNewIntent(Intent intent)

protected override void OnActivityResult(int requestCode, Result resultCode, Intent data)
{
if (_oneTimeActivityListeners.ContainsKey(requestCode))
{
if (_oneTimeActivityListeners.TryRemove(requestCode, out var listener))
{
listener.SetResult(new ActivityResult((int)resultCode, data));
}
return;
}

if (resultCode == Result.Ok &&
(requestCode == Core.Constants.SelectFileRequestCode || requestCode == Core.Constants.SaveFileRequestCode))
{
Expand Down Expand Up @@ -285,6 +301,14 @@ protected override void OnDestroy()
_broadcasterService.Unsubscribe(_activityKey);
}

public void StartActivityForResult<T>(Intent intent, TaskCompletionSource<T> taskCompletionSource)
{
int requestCode = Math.Abs((int)DateTime.UtcNow.Ticks);
_oneTimeActivityListeners[requestCode] = taskCompletionSource as TaskCompletionSource<ActivityResult>;

StartActivityForResult(intent, requestCode);
}

private void ListenYubiKey(bool listen)
{
if (!_deviceActionService.SupportsNfc())
Expand Down
4 changes: 3 additions & 1 deletion src/Android/MainApplication.cs
Expand Up @@ -7,7 +7,6 @@
using Android.Runtime;
using Bit.App.Abstractions;
using Bit.App.Services;
using Bit.Core;
using Bit.Core.Abstractions;
using Bit.Core.Services;
using Bit.Core.Utilities;
Expand All @@ -22,6 +21,7 @@
using Bit.App.Utilities.AccountManagement;
using Bit.App.Controls;
using Bit.Core.Enums;
using Bit.Droid.Security;
#if !FDROID
using Android.Gms.Security;
#endif
Expand Down Expand Up @@ -182,6 +182,8 @@ private void RegisterLocalServices()
ServiceContainer.Register<ICryptoService>("cryptoService", cryptoService);
ServiceContainer.Register<IPasswordRepromptService>("passwordRepromptService", passwordRepromptService);
ServiceContainer.Register<IAvatarImageSourcePool>("avatarImageSourcePool", new AvatarImageSourcePool());
ServiceContainer.Register<ICertificateService>("certificateService", new CertificateService());
ServiceContainer.Register<IHttpClientHandler>("httpClientHandler", new AndroidHttpsClientHandler());

// Push
#if FDROID
Expand Down
76 changes: 76 additions & 0 deletions src/Android/Security/AndroidHttpsClientHandler.cs
@@ -0,0 +1,76 @@
using System.Linq;
using System.Net.Http;
using System.Security.Authentication;
using System.Threading;
using System.Threading.Tasks;
using Bit.Core.Abstractions;
using Bit.Core.Models;
using Java.Security;
using Java.Security.Cert;
using Javax.Net.Ssl;
using Xamarin.Android.Net;

namespace Bit.Droid.Security
{
public class AndroidHttpsClientHandler : AndroidClientHandler, IHttpClientHandler
{
private X509CertificateChainSpec ClientCertificate;

public AndroidHttpsClientHandler() : base()
{
ClientCertificate = null;
}

public HttpClientHandler AsClientHandler()
{
return this;
}

public void UseClientCertificate(ICertificateChainSpec clientCertificate)
{
ClientCertificate = clientCertificate as X509CertificateChainSpec;
}

protected override SSLSocketFactory ConfigureCustomSSLSocketFactory(HttpsURLConnection connection)
{
if (ClientCertificate == null) return base.ConfigureCustomSSLSocketFactory(connection);

X509Certificate[] certChain = ClientCertificate.CertificateChain;
var privateKey = ClientCertificate.PrivateKeyRef;

if (privateKey == null || certChain == null || certChain.Length == 0)
return base.ConfigureCustomSSLSocketFactory(connection);

KeyStore keyStore = KeyStore.GetInstance("pkcs12");
keyStore.Load(null, null);
keyStore.SetKeyEntry($"{ClientCertificate.Alias}_TLS", privateKey, null, certChain.Cast<Certificate>().ToArray());

var kmf = KeyManagerFactory.GetInstance("x509");
kmf.Init(keyStore, null);

var keyManagers = kmf.GetKeyManagers();

SSLContext sslContext = SSLContext.GetInstance("TLS");
sslContext.Init(keyManagers, null, null);

SSLSocketFactory socketFactory = sslContext.SocketFactory;
if (connection != null)
{
connection.SSLSocketFactory = socketFactory;
}
return socketFactory;
}

protected async override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
try
{
return await base.SendAsync(request, cancellationToken);
}
catch (Javax.Net.Ssl.SSLProtocolException ex) when (ex.Message.Contains("SSLV3_ALERT_BAD_CERTIFICATE"))
{
throw new HttpRequestException(ex.Message, new AuthenticationException());
}
}
}
}
42 changes: 42 additions & 0 deletions src/Android/Security/X509CertificateChainSpec.cs
@@ -0,0 +1,42 @@
using System;
using System.Text;
using Bit.Core.Models;
using Java.Security;
using Java.Security.Cert;

namespace Bit.Droid.Security
{
public class X509CertificateChainSpec : ICertificateChainSpec<Java.Security.Cert.X509Certificate, IKey>
{
public string Alias { get; set; }

public IKey PrivateKeyRef { get; internal set; }

public X509Certificate RootCertificate
{
get => CertificateChain?[0];
}
public X509Certificate[] CertificateChain { get; set; }

public X509Certificate LeafCertificate {
get => CertificateChain?[CertificateChain.Length-1];
}

public string ToString(string format, IFormatProvider formatProvider)
{
if (LeafCertificate == null) {
return string.Empty;
}

StringBuilder sb = new StringBuilder();
sb.AppendLine($"Subject: {LeafCertificate.SubjectDN}");
sb.AppendLine($"Issuer: {LeafCertificate.IssuerDN}");
sb.AppendLine($"Valid From: {LeafCertificate.NotBefore}");
sb.AppendLine($"Valid Until: {LeafCertificate.NotAfter}");

return sb.ToString();
}

public override string ToString() => this.ToString(null, System.Globalization.CultureInfo.CurrentCulture);
}
}