19 changed files with 713 additions and 4 deletions
@ -0,0 +1,106 @@
@@ -0,0 +1,106 @@
|
||||
#if !FDROID |
||||
using System.Collections.Generic; |
||||
using Android.Gms.Fido.Common; |
||||
using Android.Gms.Fido.Fido2.Api.Common; |
||||
using Bit.Core.Models.Data; |
||||
using Bit.Core.Models.Response; |
||||
using Bit.Core.Utilities; |
||||
using Java.Lang; |
||||
using Newtonsoft.Json.Linq; |
||||
|
||||
namespace Bit.Droid.Fido2System |
||||
{ |
||||
class Fido2BuilderObject |
||||
{ |
||||
public static PublicKeyCredentialRequestOptions ParsePublicKeyCredentialRequestOptions( |
||||
Fido2AuthenticationChallengeResponse data) |
||||
{ |
||||
if (data == null) |
||||
{ |
||||
return null; |
||||
} |
||||
|
||||
var builder = new PublicKeyCredentialRequestOptions.Builder(); |
||||
|
||||
if (!string.IsNullOrEmpty(data.Challenge)) |
||||
{ |
||||
builder.SetChallenge(CoreHelpers.Base64UrlDecode(data.Challenge)); |
||||
} |
||||
if (data.AllowCredentials != null && data.AllowCredentials.Count > 0) |
||||
{ |
||||
builder.SetAllowList(ParseCredentialDescriptors(data.AllowCredentials)); |
||||
} |
||||
if (!string.IsNullOrEmpty(data.RpId)) |
||||
{ |
||||
builder.SetRpId(data.RpId); |
||||
} |
||||
if (data.Timeout > 0) |
||||
{ |
||||
builder.SetTimeoutSeconds((Double)(data.Timeout / 1000)); |
||||
} |
||||
if (data.Extensions != null) |
||||
{ |
||||
builder.SetAuthenticationExtensions(ParseExtensions((JObject)data.Extensions)); |
||||
} |
||||
return builder.Build(); |
||||
} |
||||
|
||||
private static List<PublicKeyCredentialDescriptor> ParseCredentialDescriptors( |
||||
List<Fido2CredentialDescriptor> listData) |
||||
{ |
||||
if (listData == null || listData.Count == 0) |
||||
{ |
||||
return new List<PublicKeyCredentialDescriptor>(); |
||||
} |
||||
|
||||
var credentials = new List<PublicKeyCredentialDescriptor>(); |
||||
|
||||
foreach (var data in listData) |
||||
{ |
||||
string id = null; |
||||
string type = null; |
||||
var transports = new List<Transport>(); |
||||
|
||||
if (!string.IsNullOrEmpty(data.Id)) |
||||
{ |
||||
id = data.Id; |
||||
} |
||||
if (!string.IsNullOrEmpty(data.Type)) |
||||
{ |
||||
type = data.Type; |
||||
} |
||||
if (data.Transports != null && data.Transports.Count > 0) |
||||
{ |
||||
foreach (var transport in data.Transports) |
||||
{ |
||||
transports.Add(Transport.FromString(transport)); |
||||
} |
||||
} |
||||
|
||||
credentials.Add(new PublicKeyCredentialDescriptor(type, CoreHelpers.Base64UrlDecode(id), transports)); |
||||
} |
||||
|
||||
return credentials; |
||||
} |
||||
|
||||
private static AuthenticationExtensions ParseExtensions(JObject extensions) |
||||
{ |
||||
var builder = new AuthenticationExtensions.Builder(); |
||||
|
||||
if (extensions.ContainsKey("appid")) |
||||
{ |
||||
var appId = new FidoAppIdExtension((string)extensions.GetValue("appid")); |
||||
builder.SetFido2Extension(appId); |
||||
} |
||||
|
||||
if (extensions.ContainsKey("uvm")) |
||||
{ |
||||
var uvm = new UserVerificationMethodExtension((bool)extensions.GetValue("uvm")); |
||||
builder.SetUserVerificationMethodExtension(uvm); |
||||
} |
||||
|
||||
return builder.Build(); |
||||
} |
||||
} |
||||
} |
||||
#endif |
||||
@ -0,0 +1,249 @@
@@ -0,0 +1,249 @@
|
||||
#if !FDROID |
||||
using Android.App; |
||||
using Android.Content; |
||||
using Android.Gms.Fido; |
||||
using Android.Gms.Fido.Fido2; |
||||
using Android.Gms.Fido.Fido2.Api.Common; |
||||
using Android.Gms.Tasks; |
||||
using Android.Util; |
||||
using AndroidX.AppCompat.App; |
||||
using Bit.App.Services; |
||||
using Bit.Core.Abstractions; |
||||
using Bit.Core.Enums; |
||||
using Bit.Core.Models.Data; |
||||
using Bit.Core.Models.Request; |
||||
using Bit.Core.Models.Response; |
||||
using Bit.Core.Utilities; |
||||
using Java.Lang; |
||||
using Newtonsoft.Json; |
||||
using Xamarin.Forms; |
||||
using Enum = System.Enum; |
||||
|
||||
namespace Bit.Droid.Fido2System |
||||
{ |
||||
public class Fido2Service |
||||
{ |
||||
public static readonly string _tag_log = "Fido2Service"; |
||||
|
||||
public static Fido2Service INSTANCE = new Fido2Service(); |
||||
|
||||
private readonly MobileI18nService _i18nService; |
||||
private readonly IPlatformUtilsService _platformUtilsService; |
||||
|
||||
private AppCompatActivity _activity; |
||||
private Fido2ApiClient _fido2ApiClient; |
||||
private Fido2CodesTypes _fido2CodesType; |
||||
|
||||
public Fido2Service() |
||||
{ |
||||
_i18nService = ServiceContainer.Resolve<II18nService>("i18nService") as MobileI18nService; |
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService"); |
||||
} |
||||
|
||||
public void Start(AppCompatActivity activity) |
||||
{ |
||||
_activity = activity; |
||||
_fido2ApiClient = Fido.GetFido2ApiClient(_activity); |
||||
} |
||||
|
||||
public void OnActivityResult(int requestCode, Result resultCode, Intent data) |
||||
{ |
||||
if (resultCode == Result.Ok && Enum.IsDefined(typeof(Fido2CodesTypes), requestCode)) |
||||
{ |
||||
switch ((Fido2CodesTypes)requestCode) |
||||
{ |
||||
case Fido2CodesTypes.RequestSignInUser: |
||||
var errorExtra = data?.GetByteArrayExtra(Fido.Fido2KeyErrorExtra); |
||||
if (errorExtra != null) |
||||
{ |
||||
HandleErrorCode(errorExtra); |
||||
} |
||||
else |
||||
{ |
||||
if (data != null) |
||||
{ |
||||
SignInUserResponse(data); |
||||
} |
||||
} |
||||
break; |
||||
// TODO: Key registration, should we ever choose to implement client-side |
||||
/*case Fido2CodesTypes.RequestRegisterNewKey: |
||||
errorExtra = data?.GetByteArrayExtra(Fido.Fido2KeyErrorExtra); |
||||
if (errorExtra != null) |
||||
{ |
||||
HandleErrorCode(errorExtra); |
||||
} |
||||
else |
||||
{ |
||||
if (data != null) |
||||
{ |
||||
// begin registration flow |
||||
} |
||||
} |
||||
break;*/ |
||||
} |
||||
} |
||||
else if (resultCode == Result.Canceled && Enum.IsDefined(typeof(Fido2CodesTypes), requestCode)) |
||||
{ |
||||
Log.Info(_tag_log, "cancelled"); |
||||
_platformUtilsService.ShowDialogAsync(_i18nService.T("Fido2AbortError"), |
||||
_i18nService.T("Fido2Title")); |
||||
} |
||||
} |
||||
|
||||
public void OnSuccess(Object result) |
||||
{ |
||||
if (result != null && Enum.IsDefined(typeof(Fido2CodesTypes), _fido2CodesType)) |
||||
{ |
||||
try |
||||
{ |
||||
_activity.StartIntentSenderForResult(((PendingIntent)result).IntentSender, (int)_fido2CodesType, |
||||
null, 0, 0, 0); |
||||
} |
||||
catch (System.Exception e) |
||||
{ |
||||
Log.Error(_tag_log, e.Message); |
||||
_platformUtilsService.ShowDialogAsync(_i18nService.T("Fido2SomethingWentWrong"), |
||||
_i18nService.T("Fido2Title")); |
||||
} |
||||
} |
||||
} |
||||
|
||||
public void OnFailure(Exception e) |
||||
{ |
||||
Log.Error(_tag_log, e.Message ?? "OnFailure: No error message returned"); |
||||
_platformUtilsService.ShowDialogAsync(_i18nService.T("Fido2SomethingWentWrong"), |
||||
_i18nService.T("Fido2Title")); |
||||
} |
||||
|
||||
public void OnComplete(Task task) |
||||
{ |
||||
Log.Debug(_tag_log, "OnComplete"); |
||||
} |
||||
|
||||
public async System.Threading.Tasks.Task SignInUserRequestAsync(string dataJson) |
||||
{ |
||||
try |
||||
{ |
||||
var dataObject = JsonConvert.DeserializeObject<Fido2AuthenticationChallengeResponse>(dataJson); |
||||
_fido2CodesType = Fido2CodesTypes.RequestSignInUser; |
||||
var options = Fido2BuilderObject.ParsePublicKeyCredentialRequestOptions(dataObject); |
||||
var task = _fido2ApiClient.GetSignPendingIntent(options); |
||||
task.AddOnSuccessListener((IOnSuccessListener)_activity) |
||||
.AddOnFailureListener((IOnFailureListener)_activity) |
||||
.AddOnCompleteListener((IOnCompleteListener)_activity); |
||||
} |
||||
catch (System.Exception e) |
||||
{ |
||||
Log.Error(_tag_log, e.StackTrace); |
||||
await _platformUtilsService.ShowDialogAsync(_i18nService.T("Fido2SomethingWentWrong"), |
||||
_i18nService.T("Fido2Title")); |
||||
} |
||||
finally |
||||
{ |
||||
Log.Info(_tag_log, "SignInUserRequest() -> finally()"); |
||||
} |
||||
} |
||||
|
||||
private void SignInUserResponse(Intent data) |
||||
{ |
||||
try |
||||
{ |
||||
var response = |
||||
AuthenticatorAssertionResponse.DeserializeFromBytes( |
||||
data.GetByteArrayExtra(Fido.Fido2KeyResponseExtra)); |
||||
var responseJson = JsonConvert.SerializeObject(new Fido2AuthenticationChallengeRequest |
||||
{ |
||||
Id = CoreHelpers.Base64UrlEncode(response.GetKeyHandle()), |
||||
RawId = CoreHelpers.Base64UrlEncode(response.GetKeyHandle()), |
||||
Type = "public-key", |
||||
Response = new Fido2AssertionResponse |
||||
{ |
||||
AuthenticatorData = CoreHelpers.Base64UrlEncode(response.GetAuthenticatorData()), |
||||
ClientDataJson = CoreHelpers.Base64UrlEncode(response.GetClientDataJSON()), |
||||
Signature = CoreHelpers.Base64UrlEncode(response.GetSignature()), |
||||
UserHandle = (response.GetUserHandle() != null |
||||
? CoreHelpers.Base64UrlEncode(response.GetUserHandle()) : null), |
||||
}, |
||||
Extensions = null |
||||
} |
||||
); |
||||
Device.BeginInvokeOnMainThread(() => ((MainActivity)_activity).Fido2Submission(responseJson)); |
||||
} |
||||
catch (System.Exception e) |
||||
{ |
||||
Log.Error(_tag_log, e.Message); |
||||
_platformUtilsService.ShowDialogAsync(_i18nService.T("Fido2SomethingWentWrong"), |
||||
_i18nService.T("Fido2Title")); |
||||
} |
||||
finally |
||||
{ |
||||
Log.Info(_tag_log, "SignInUserResponse() -> finally()"); |
||||
} |
||||
} |
||||
|
||||
public void HandleErrorCode(byte[] errorExtra) |
||||
{ |
||||
var error = AuthenticatorErrorResponse.DeserializeFromBytes(errorExtra); |
||||
if (error.ErrorMessage.Length > 0) |
||||
{ |
||||
Log.Info(_tag_log, error.ErrorMessage); |
||||
} |
||||
string message = ""; |
||||
if (error.ErrorCode == ErrorCode.AbortErr) |
||||
{ |
||||
message = "Fido2AbortError"; |
||||
} |
||||
else if (error.ErrorCode == ErrorCode.TimeoutErr) |
||||
{ |
||||
message = "Fido2TimeoutError"; |
||||
} |
||||
else if (error.ErrorCode == ErrorCode.AttestationNotPrivateErr) |
||||
{ |
||||
message = "Fido2PrivacyError"; |
||||
} |
||||
else if (error.ErrorCode == ErrorCode.ConstraintErr) |
||||
{ |
||||
message = "Fido2SomethingWentWrong"; |
||||
} |
||||
else if (error.ErrorCode == ErrorCode.DataErr) |
||||
{ |
||||
message = "Fido2ServerDataFail"; |
||||
} |
||||
else if (error.ErrorCode == ErrorCode.EncodingErr) |
||||
{ |
||||
message = "Fido2SomethingWentWrong"; |
||||
} |
||||
else if (error.ErrorCode == ErrorCode.InvalidStateErr) |
||||
{ |
||||
message = "Fido2SomethingWentWrong"; |
||||
} |
||||
else if (error.ErrorCode == ErrorCode.NetworkErr) |
||||
{ |
||||
message = "Fido2NetworkFail"; |
||||
} |
||||
else if (error.ErrorCode == ErrorCode.NotAllowedErr) |
||||
{ |
||||
message = "Fido2NoPermission"; |
||||
} |
||||
else if (error.ErrorCode == ErrorCode.NotSupportedErr) |
||||
{ |
||||
message = "Fido2NotSupportedError"; |
||||
} |
||||
else if (error.ErrorCode == ErrorCode.SecurityErr) |
||||
{ |
||||
message = "Fido2SecurityError"; |
||||
} |
||||
else if (error.ErrorCode == ErrorCode.UnknownErr) |
||||
{ |
||||
message = "Fido2SomethingWentWrong"; |
||||
} |
||||
else |
||||
{ |
||||
message = "Fido2SomethingWentWrong"; |
||||
} |
||||
_platformUtilsService.ShowDialogAsync(_i18nService.T(message), _i18nService.T("Fido2Title")); |
||||
} |
||||
} |
||||
} |
||||
#endif |
||||
@ -0,0 +1,8 @@
@@ -0,0 +1,8 @@
|
||||
namespace Bit.Core.Enums |
||||
{ |
||||
public enum Fido2CodesTypes |
||||
{ |
||||
RequestSignInUser = 994, |
||||
RequestRegisterNewKey = 995, |
||||
} |
||||
} |
||||
@ -0,0 +1,16 @@
@@ -0,0 +1,16 @@
|
||||
using Newtonsoft.Json; |
||||
|
||||
namespace Bit.Core.Models.Data |
||||
{ |
||||
public class Fido2AssertionResponse : Data |
||||
{ |
||||
[JsonProperty("authenticatorData")] |
||||
public string AuthenticatorData { get; set; } |
||||
[JsonProperty("signature")] |
||||
public string Signature { get; set; } |
||||
[JsonProperty("clientDataJson")] |
||||
public string ClientDataJson { get; set; } |
||||
[JsonProperty("userHandle")] |
||||
public string UserHandle { get; set; } |
||||
} |
||||
} |
||||
@ -0,0 +1,14 @@
@@ -0,0 +1,14 @@
|
||||
using Newtonsoft.Json; |
||||
|
||||
namespace Bit.Core.Models.Data |
||||
{ |
||||
public class Fido2AuthenticatorSelection : Data |
||||
{ |
||||
[JsonProperty("authenticatorAttachment")] |
||||
public string AuthenticatorAttachment { get; set; } |
||||
[JsonProperty("userVerification")] |
||||
public string UserVerification { get; set; } |
||||
[JsonProperty("requireResidentKey")] |
||||
public string RequireResidentKey { get; set; } |
||||
} |
||||
} |
||||
@ -0,0 +1,15 @@
@@ -0,0 +1,15 @@
|
||||
using System.Collections.Generic; |
||||
using Newtonsoft.Json; |
||||
|
||||
namespace Bit.Core.Models.Data |
||||
{ |
||||
public class Fido2CredentialDescriptor : Data |
||||
{ |
||||
[JsonProperty("type")] |
||||
public string Type { get; set; } |
||||
[JsonProperty("id")] |
||||
public string Id { get; set; } |
||||
[JsonProperty("transports", NullValueHandling = NullValueHandling.Ignore)] |
||||
public List<string> Transports { get; set; } |
||||
} |
||||
} |
||||
@ -0,0 +1,12 @@
@@ -0,0 +1,12 @@
|
||||
using Newtonsoft.Json; |
||||
|
||||
namespace Bit.Core.Models.Data |
||||
{ |
||||
public class Fido2PubKeyCredParam : Data |
||||
{ |
||||
[JsonProperty("type")] |
||||
public string Type { get; set; } |
||||
[JsonProperty("alg")] |
||||
public int Alg { get; set; } |
||||
} |
||||
} |
||||
@ -0,0 +1,14 @@
@@ -0,0 +1,14 @@
|
||||
using Newtonsoft.Json; |
||||
|
||||
namespace Bit.Core.Models.Data |
||||
{ |
||||
public class Fido2RP : Data |
||||
{ |
||||
[JsonProperty("id")] |
||||
public string Id { get; set; } |
||||
[JsonProperty("name")] |
||||
public string Name { get; set; } |
||||
[JsonProperty("icon")] |
||||
public string Icon { get; set; } |
||||
} |
||||
} |
||||
@ -0,0 +1,16 @@
@@ -0,0 +1,16 @@
|
||||
using Newtonsoft.Json; |
||||
|
||||
namespace Bit.Core.Models.Data |
||||
{ |
||||
public class Fido2User : Data |
||||
{ |
||||
[JsonProperty("id")] |
||||
public string Id { get; set; } |
||||
[JsonProperty("name")] |
||||
public string Name { get; set; } |
||||
[JsonProperty("displayName")] |
||||
public string DisplayName { get; set; } |
||||
[JsonProperty("icon")] |
||||
public string Icon { get; set; } |
||||
} |
||||
} |
||||
@ -0,0 +1,19 @@
@@ -0,0 +1,19 @@
|
||||
using Bit.Core.Models.Data; |
||||
using Newtonsoft.Json; |
||||
|
||||
namespace Bit.Core.Models.Request |
||||
{ |
||||
public class Fido2AuthenticationChallengeRequest |
||||
{ |
||||
[JsonProperty("id")] |
||||
public string Id { get; set; } |
||||
[JsonProperty("rawId")] |
||||
public string RawId { get; set; } |
||||
[JsonProperty("response")] |
||||
public Fido2AssertionResponse Response { get; set; } |
||||
[JsonProperty("type")] |
||||
public string Type { get; set; } |
||||
[JsonProperty("extensions", NullValueHandling = NullValueHandling.Ignore)] |
||||
public string Extensions { get; set; } |
||||
} |
||||
} |
||||
@ -0,0 +1,22 @@
@@ -0,0 +1,22 @@
|
||||
using System.Collections.Generic; |
||||
using Bit.Core.Models.Data; |
||||
using Newtonsoft.Json; |
||||
|
||||
namespace Bit.Core.Models.Response |
||||
{ |
||||
public class Fido2AuthenticationChallengeResponse |
||||
{ |
||||
[JsonProperty("challenge")] |
||||
public string Challenge { get; set; } |
||||
[JsonProperty("rpId")] |
||||
public string RpId { get; set; } |
||||
[JsonProperty("timeout")] |
||||
public double Timeout { get; set; } |
||||
[JsonProperty("allowCredentials")] |
||||
public List<Fido2CredentialDescriptor> AllowCredentials { get; set; } |
||||
[JsonProperty("userVerification")] |
||||
public string UserVerification { get; set; } |
||||
[JsonProperty("extensions", NullValueHandling = NullValueHandling.Ignore)] |
||||
public object Extensions { get; set; } |
||||
} |
||||
} |
||||
@ -0,0 +1,28 @@
@@ -0,0 +1,28 @@
|
||||
using System.Collections.Generic; |
||||
using Bit.Core.Models.Data; |
||||
using Newtonsoft.Json; |
||||
|
||||
namespace Bit.Core.Models.Response |
||||
{ |
||||
public class Fido2RegistrationChallengeResponse |
||||
{ |
||||
[JsonProperty("challenge")] |
||||
public string Challenge { get; set; } |
||||
[JsonProperty("timeout")] |
||||
public double Timeout { get; set; } |
||||
[JsonProperty("rp")] |
||||
public Fido2RP Rp { get; set; } |
||||
[JsonProperty("user")] |
||||
public Fido2User User { get; set; } |
||||
[JsonProperty("pubKeyCredParams")] |
||||
public List<Fido2PubKeyCredParam> PubKeyCredParams { get; set; } |
||||
[JsonProperty("excludeCredentials")] |
||||
public List<Fido2CredentialDescriptor> ExcludeCredentials { get; set; } |
||||
[JsonProperty("authenticatorSelection")] |
||||
public Fido2AuthenticatorSelection AuthenticatorSelection { get; set; } |
||||
[JsonProperty("attestation", NullValueHandling = NullValueHandling.Ignore)] |
||||
public object Attestation { get; set; } |
||||
[JsonProperty("extensions", NullValueHandling = NullValueHandling.Ignore)] |
||||
public object Extensions { get; set; } |
||||
} |
||||
} |
||||
Loading…
Reference in new issue