Skip to content

Commit

Permalink
[Mono.Android] Add convenience KeyChain APIs (#9047)
Browse files Browse the repository at this point in the history
Context: dotnet/runtime#99874
Context: dotnet/runtime#103337

dotnet/runtime#103337 added the ability for the
[`X509Certificate2(IntPtr)` constructor][0] to accept a
[`java.security.KeyStore.PrivateKeyEntry`][1] instance.

Add convenience methods to [`Android.Security.KeyChain`][2] to make
it easier for developers to access certificates with non-exportable
private keys:

	partial class KeyChain {
	    public static X509Certificate2? GetX509Certificate2WithPrivateKey (
	            Android.Content.Context context,
	            string alias);
	    public static async Task<string?> ChoosePrivateKeyAliasAsync (
	            Android.App.Activity activity,
	            string[]? keyTypes,
	            Java.Security.IPrincipal[]? issuers,
	            Android.Net.Uri? uri,
	            string? alias);
	    public static async Task<string?> ChoosePrivateKeyAliasAsync (
	            Android.App.Activity activity,
	            string[]? keyTypes,
	            Java.Security.IPrincipal[]? issuers,
	            string? host,
	            int port,
	            string? alias);
	    public static async Task<X509Certificate2?> ChooseX509Certificate2WithPrivateKeyAsync (
	            Android.App.Activity activity,
	            string[]? keyTypes,
	            Java.Security.IPrincipal[]? issuers,
	            Android.Net.Uri? uri,
	            string? alias);
	    public static async Task<X509Certificate2?> ChooseX509Certificate2WithPrivateKeyAsync (
	            Android.App.Activity activity,
	            string[]? keyTypes,
	            Java.Security.IPrincipal[]? issuers,
	            string? host,
	            int port,
	            string? alias);
	}

[0]: https://learn.microsoft.com/dotnet/api/system.security.cryptography.x509certificates.x509certificate2.-ctor?view=net-8.0#system-security-cryptography-x509certificates-x509certificate2-ctor(system-intptr)
[1]: https://developer.android.com/reference/java/security/KeyStore.PrivateKeyEntry
[2]: https://learn.microsoft.com/dotnet/api/android.security.keychain?view=net-android-34.0
  • Loading branch information
simonrozsival authored Jul 26, 2024
1 parent 9a27140 commit 16cdbb0
Show file tree
Hide file tree
Showing 3 changed files with 101 additions and 2 deletions.
93 changes: 93 additions & 0 deletions src/Mono.Android/Android.Security/KeyChain.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
#nullable enable
using System;
using System.Runtime.Versioning;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;

namespace Android.Security
{
public partial class KeyChain
{
public static X509Certificate2? GetX509Certificate2WithPrivateKey (Android.Content.Context context, string alias)
{
var privateKey = KeyChain.GetPrivateKey (context, alias);
if (privateKey is null) {
return null;
}

var chain = KeyChain.GetCertificateChain (context, alias);
if (chain is null) {
return null;
}

var privateKeyEntry = new Java.Security.KeyStore.PrivateKeyEntry (privateKey, chain);
var certificate = new X509Certificate2 (privateKeyEntry.Handle);
GC.KeepAlive (privateKeyEntry);
return certificate;
}

[SupportedOSPlatform("android23.0")]
public static async Task<string?> ChoosePrivateKeyAliasAsync (
Android.App.Activity activity,
string[]? keyTypes,
Java.Security.IPrincipal[]? issuers,
Android.Net.Uri? uri,
string? alias)
{
var tcs = new TaskCompletionSource<string?> ();
KeyChain.ChoosePrivateKeyAlias (activity, new KeyChainAliasCallback(tcs), keyTypes, issuers, uri, alias);
return await tcs.Task;
}

public static async Task<string?> ChoosePrivateKeyAliasAsync (
Android.App.Activity activity,
string[]? keyTypes,
Java.Security.IPrincipal[]? issuers,
string? host,
int port,
string? alias)
{
var tcs = new TaskCompletionSource<string?> ();
KeyChain.ChoosePrivateKeyAlias (activity, new KeyChainAliasCallback(tcs), keyTypes, issuers, host, port, alias);
return await tcs.Task;
}

[SupportedOSPlatform("android23.0")]
public static async Task<X509Certificate2?> ChooseX509Certificate2WithPrivateKeyAsync (
Android.App.Activity activity,
string[]? keyTypes,
Java.Security.IPrincipal[]? issuers,
Android.Net.Uri? uri,
string? alias)
{
alias = await ChoosePrivateKeyAliasAsync (activity, keyTypes, issuers, uri, alias);
if (alias is null) {
return null;
}

return GetX509Certificate2WithPrivateKey (activity, alias);
}

public static async Task<X509Certificate2?> ChooseX509Certificate2WithPrivateKeyAsync (
Android.App.Activity activity,
string[]? keyTypes,
Java.Security.IPrincipal[]? issuers,
string? host,
int port,
string? alias)
{
alias = await ChoosePrivateKeyAliasAsync (activity, keyTypes, issuers, host, port, alias);
if (alias is null) {
return null;
}

return GetX509Certificate2WithPrivateKey (activity, alias);
}

private sealed class KeyChainAliasCallback(TaskCompletionSource<string?> tcs)
: Java.Lang.Object, IKeyChainAliasCallback
{
public void Alias (string? alias) => tcs.SetResult (alias);
}
}
}
5 changes: 3 additions & 2 deletions src/Mono.Android/Mono.Android.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
<AdditionalFiles Include="PublicAPI\API-$(AndroidApiLevel)\PublicAPI.Shipped.txt" />
<AdditionalFiles Include="PublicAPI\API-$(AndroidApiLevel)\PublicAPI.Unshipped.txt" />
</ItemGroup>

<PropertyGroup>
<JavaCallableWrapperAbsAssembly>$([System.IO.Path]::GetFullPath ('$(OutputPath)$(AssemblyName).dll'))</JavaCallableWrapperAbsAssembly>
</PropertyGroup>
Expand Down Expand Up @@ -263,6 +263,7 @@
<Compile Include="Android.Runtime\XAPeerMembers.cs" />
<Compile Include="Android.Runtime\XmlPullParserReader.cs" />
<Compile Include="Android.Runtime\XmlReaderPullParser.cs" />
<Compile Include="Android.Security\KeyChain.cs" />
<Compile Include="Android.Telephony\CellInfo.cs" />
<Compile Include="Android.Telephony\PhoneNumberUtils.cs" />
<Compile Include="Android.Telephony\TelephonyManager.cs" />
Expand Down Expand Up @@ -395,7 +396,7 @@
<PropertyGroup Condition=" '$(AndroidApiLevel)' &lt; '$(AndroidDefaultTargetDotnetApiLevel)' ">
<BuildDependsOn></BuildDependsOn>
</PropertyGroup>

<PropertyGroup Condition=" '$(AndroidApiLevel)' &gt;= '$(AndroidDefaultTargetDotnetApiLevel)' ">
<BuildDependsOn>
$(BuildDependsOn);
Expand Down
5 changes: 5 additions & 0 deletions src/Mono.Android/PublicAPI/API-35/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3957,3 +3957,8 @@ virtual Java.Util.Zip.Inflater.SetDictionary(Java.Nio.ByteBuffer? dictionary) ->
virtual Java.Util.Zip.Inflater.SetInput(Java.Nio.ByteBuffer? input) -> void
virtual Java.Util.Zip.ZipEntry.TimeLocal.get -> Java.Time.LocalDateTime?
virtual Java.Util.Zip.ZipEntry.TimeLocal.set -> void
static Android.Security.KeyChain.GetX509Certificate2WithPrivateKey(Android.Content.Context! context, string! alias) -> System.Security.Cryptography.X509Certificates.X509Certificate2?
static Android.Security.KeyChain.ChooseX509Certificate2WithPrivateKeyAsync(Android.App.Activity! activity, string![]? keyTypes, Java.Security.IPrincipal![]? issuers, Android.Net.Uri? uri, string? alias) -> System.Threading.Tasks.Task<System.Security.Cryptography.X509Certificates.X509Certificate2?>!
static Android.Security.KeyChain.ChooseX509Certificate2WithPrivateKeyAsync(Android.App.Activity! activity, string![]? keyTypes, Java.Security.IPrincipal![]? issuers, string? host, int port, string? alias) -> System.Threading.Tasks.Task<System.Security.Cryptography.X509Certificates.X509Certificate2?>!
static Android.Security.KeyChain.ChoosePrivateKeyAliasAsync(Android.App.Activity! activity, string![]? keyTypes, Java.Security.IPrincipal![]? issuers, Android.Net.Uri? uri, string? alias) -> System.Threading.Tasks.Task<string?>!
static Android.Security.KeyChain.ChoosePrivateKeyAliasAsync(Android.App.Activity! activity, string![]? keyTypes, Java.Security.IPrincipal![]? issuers, string? host, int port, string? alias) -> System.Threading.Tasks.Task<string?>!

0 comments on commit 16cdbb0

Please sign in to comment.