diff --git a/src/Android/Android.csproj b/src/Android/Android.csproj
index 5414e8255..71a1c31fc 100644
--- a/src/Android/Android.csproj
+++ b/src/Android/Android.csproj
@@ -92,6 +92,7 @@
+
117.0.0
@@ -115,6 +116,8 @@
+
+
diff --git a/src/Android/Fido2System/Fido2BuilderObject.cs b/src/Android/Fido2System/Fido2BuilderObject.cs
new file mode 100644
index 000000000..7df6b4ecc
--- /dev/null
+++ b/src/Android/Fido2System/Fido2BuilderObject.cs
@@ -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 ParseCredentialDescriptors(
+ List listData)
+ {
+ if (listData == null || listData.Count == 0)
+ {
+ return new List();
+ }
+
+ var credentials = new List();
+
+ foreach (var data in listData)
+ {
+ string id = null;
+ string type = null;
+ var transports = new List();
+
+ 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
diff --git a/src/Android/Fido2System/Fido2Service.cs b/src/Android/Fido2System/Fido2Service.cs
new file mode 100644
index 000000000..577c287e3
--- /dev/null
+++ b/src/Android/Fido2System/Fido2Service.cs
@@ -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("i18nService") as MobileI18nService;
+ _platformUtilsService = ServiceContainer.Resolve("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(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
diff --git a/src/Android/MainActivity.cs b/src/Android/MainActivity.cs
index 3208295f4..43cf087c0 100644
--- a/src/Android/MainActivity.cs
+++ b/src/Android/MainActivity.cs
@@ -9,6 +9,7 @@ using Bit.Core.Utilities;
using Bit.Core.Abstractions;
using System.IO;
using System;
+using System.Collections.Generic;
using Android.Content;
using Bit.Droid.Utilities;
using Bit.Droid.Receivers;
@@ -17,7 +18,11 @@ using Bit.Core.Enums;
using Android.Nfc;
using Bit.App.Utilities;
using System.Threading.Tasks;
+using Android.Util;
using AndroidX.Core.Content;
+#if !FDROID
+using Bit.Droid.Fido2System;
+#endif
using ZXing.Net.Mobile.Android;
namespace Bit.Droid
@@ -42,7 +47,10 @@ namespace Bit.Droid
@"text/*"
})]
[Register("com.x8bit.bitwarden.MainActivity")]
- public class MainActivity : Xamarin.Forms.Platform.Android.FormsAppCompatActivity
+ public class MainActivity : Xamarin.Forms.Platform.Android.FormsAppCompatActivity,
+ Android.Gms.Tasks.IOnSuccessListener,
+ Android.Gms.Tasks.IOnCompleteListener,
+ Android.Gms.Tasks.IOnFailureListener
{
private IDeviceActionService _deviceActionService;
private IMessagingService _messagingService;
@@ -57,6 +65,7 @@ namespace Bit.Droid
private string _activityKey = $"{nameof(MainActivity)}_{Java.Lang.JavaSystem.CurrentTimeMillis().ToString()}";
private Java.Util.Regex.Pattern _otpPattern =
Java.Util.Regex.Pattern.Compile("^.*?([cbdefghijklnrtuv]{32,64})$");
+ private string _fidoDataJson;
protected override void OnCreate(Bundle savedInstanceState)
{
@@ -91,6 +100,7 @@ namespace Bit.Droid
#if !FDROID
var appCenterHelper = new AppCenterHelper(_appIdService, _userService);
var appCenterTask = appCenterHelper.InitAsync();
+ Fido2Service.INSTANCE.Start(this);
#endif
Xamarin.Essentials.Platform.Init(this, savedInstanceState);
@@ -112,6 +122,14 @@ namespace Bit.Droid
{
Xamarin.Forms.Device.BeginInvokeOnMainThread(() => Finish());
}
+ else if (message.Command == "listenFido2")
+ {
+ ListenFido2((Dictionary)message.Data);
+ }
+ else if (message.Command == "listenFido2TryAgain")
+ {
+ ListenFido2();
+ }
else if (message.Command == "listenYubiKeyOTP")
{
ListenYubiKey((bool)message.Data);
@@ -260,6 +278,34 @@ namespace Bit.Droid
return;
}
}
+ else if (resultCode == Result.Ok &&
+ Enum.IsDefined(typeof(Fido2CodesTypes), requestCode))
+ {
+#if !FDROID
+ Fido2Service.INSTANCE.OnActivityResult(requestCode, resultCode, data);
+#endif
+ }
+ }
+
+ public void OnSuccess(Java.Lang.Object result)
+ {
+#if !FDROID
+ Fido2Service.INSTANCE.OnSuccess(result);
+#endif
+ }
+
+ public void OnComplete(Android.Gms.Tasks.Task task)
+ {
+#if !FDROID
+ Fido2Service.INSTANCE.OnComplete(task);
+#endif
+ }
+
+ public void OnFailure(Java.Lang.Exception e)
+ {
+#if !FDROID
+ Fido2Service.INSTANCE.OnFailure(e);
+#endif
}
protected override void OnDestroy()
@@ -268,6 +314,41 @@ namespace Bit.Droid
_broadcasterService.Unsubscribe(_activityKey);
}
+ private void ListenFido2(Dictionary data = null)
+ {
+ if (!_deviceActionService.SupportsFido2())
+ {
+ return;
+ }
+
+#if !FDROID
+ RunOnUiThread(async () =>
+ {
+ try
+ {
+ if (data != null)
+ {
+ _fidoDataJson = Newtonsoft.Json.JsonConvert.SerializeObject(data);
+ await Fido2Service.INSTANCE.SignInUserRequestAsync(_fidoDataJson);
+ }
+ else
+ {
+ await Fido2Service.INSTANCE.SignInUserRequestAsync(_fidoDataJson);
+ }
+ }
+ catch (Exception e)
+ {
+ Log.Error(Fido2Service._tag_log, e.Message);
+ }
+ });
+#endif
+ }
+
+ public void Fido2Submission(string token)
+ {
+ _messagingService.Send("gotFido2Token", token);
+ }
+
private void ListenYubiKey(bool listen)
{
if (!_deviceActionService.SupportsNfc())
diff --git a/src/Android/Services/DeviceActionService.cs b/src/Android/Services/DeviceActionService.cs
index 45fd32f27..118786278 100644
--- a/src/Android/Services/DeviceActionService.cs
+++ b/src/Android/Services/DeviceActionService.cs
@@ -778,7 +778,13 @@ namespace Bit.Droid.Services
public bool SupportsFido2()
{
- return true;
+#if !FDROID
+ if ((int)Build.VERSION.SdkInt >= 21)
+ {
+ return true;
+ }
+#endif
+ return false;
}
private bool DeleteDir(Java.IO.File dir)
diff --git a/src/App/Pages/Accounts/TwoFactorPage.xaml.cs b/src/App/Pages/Accounts/TwoFactorPage.xaml.cs
index 44307a22e..7d5ec9651 100644
--- a/src/App/Pages/Accounts/TwoFactorPage.xaml.cs
+++ b/src/App/Pages/Accounts/TwoFactorPage.xaml.cs
@@ -75,6 +75,18 @@ namespace Bit.App.Pages
});
}
}
+ else if (message.Command == "gotFido2Token")
+ {
+ var token = (string)message.Data;
+ if (!string.IsNullOrWhiteSpace(token))
+ {
+ Device.BeginInvokeOnMainThread(async () =>
+ {
+ _vm.Token = token;
+ await _vm.SubmitAsync();
+ });
+ }
+ }
else if (message.Command == "resumeYubiKey")
{
if (_vm.YubikeyMethod)
@@ -174,7 +186,14 @@ namespace Bit.App.Pages
{
if (_vm.Fido2Method)
{
- await _vm.Fido2AuthenticateAsync();
+ if (Device.RuntimePlatform == Device.Android)
+ {
+ _messagingService.Send("listenFido2TryAgain", true);
+ }
+ else
+ {
+ await _vm.Fido2AuthenticateAsync();
+ }
}
else if (_vm.YubikeyMethod)
{
diff --git a/src/App/Pages/Accounts/TwoFactorPageViewModel.cs b/src/App/Pages/Accounts/TwoFactorPageViewModel.cs
index 2289e4559..5e45df75f 100644
--- a/src/App/Pages/Accounts/TwoFactorPageViewModel.cs
+++ b/src/App/Pages/Accounts/TwoFactorPageViewModel.cs
@@ -10,6 +10,8 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
+using System.Text;
+using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Bit.App.Utilities;
using Newtonsoft.Json;
@@ -151,7 +153,14 @@ namespace Bit.App.Pages
switch (SelectedProviderType.Value)
{
case TwoFactorProviderType.Fido2WebAuthn:
- Fido2AuthenticateAsync(providerData);
+ if (Device.RuntimePlatform == Device.Android)
+ {
+ _messagingService.Send("listenFido2", providerData);
+ }
+ else
+ {
+ Fido2AuthenticateAsync(providerData);
+ }
break;
case TwoFactorProviderType.YubiKey:
_messagingService.Send("listenYubiKeyOTP", true);
diff --git a/src/App/Resources/AppResources.Designer.cs b/src/App/Resources/AppResources.Designer.cs
index b7dee1148..e4c16c108 100644
--- a/src/App/Resources/AppResources.Designer.cs
+++ b/src/App/Resources/AppResources.Designer.cs
@@ -3590,5 +3590,53 @@ namespace Bit.App.Resources {
return ResourceManager.GetString("Fido2SomethingWentWrong", resourceCulture);
}
}
+
+ public static string Fido2AbortError {
+ get {
+ return ResourceManager.GetString("Fido2AbortError", resourceCulture);
+ }
+ }
+
+ public static string Fido2NetworkFail {
+ get {
+ return ResourceManager.GetString("Fido2NetworkFail", resourceCulture);
+ }
+ }
+
+ public static string Fido2NoPermission {
+ get {
+ return ResourceManager.GetString("Fido2NoPermission", resourceCulture);
+ }
+ }
+
+ public static string Fido2NotSupportedError {
+ get {
+ return ResourceManager.GetString("Fido2NotSupportedError", resourceCulture);
+ }
+ }
+
+ public static string Fido2PrivacyError {
+ get {
+ return ResourceManager.GetString("Fido2PrivacyError", resourceCulture);
+ }
+ }
+
+ public static string Fido2SecurityError {
+ get {
+ return ResourceManager.GetString("Fido2SecurityError", resourceCulture);
+ }
+ }
+
+ public static string Fido2ServerDataFail {
+ get {
+ return ResourceManager.GetString("Fido2ServerDataFail", resourceCulture);
+ }
+ }
+
+ public static string Fido2TimeoutError {
+ get {
+ return ResourceManager.GetString("Fido2TimeoutError", resourceCulture);
+ }
+ }
}
}
diff --git a/src/App/Resources/AppResources.resx b/src/App/Resources/AppResources.resx
index 5301e085d..a512eff80 100644
--- a/src/App/Resources/AppResources.resx
+++ b/src/App/Resources/AppResources.resx
@@ -2031,4 +2031,28 @@
Something Went Wrong. Try again.
+
+ Aborted FIDO2 operation. Try again.
+
+
+ No internet connection. Try again.
+
+
+ Permission was not given. Try again.
+
+
+ Unsupported device.
+
+
+ Privacy issues encountered. Try again.
+
+
+ Security issues encountered.
+
+
+ The server returned invalid data. Try again.
+
+
+ Timeout for FIDO2. Try again
+
diff --git a/src/Core/Enums/Fido2CodesTypes.cs b/src/Core/Enums/Fido2CodesTypes.cs
new file mode 100644
index 000000000..1bae25d5b
--- /dev/null
+++ b/src/Core/Enums/Fido2CodesTypes.cs
@@ -0,0 +1,8 @@
+namespace Bit.Core.Enums
+{
+ public enum Fido2CodesTypes
+ {
+ RequestSignInUser = 994,
+ RequestRegisterNewKey = 995,
+ }
+}
diff --git a/src/Core/Models/Data/Fido2AssertionResponse.cs b/src/Core/Models/Data/Fido2AssertionResponse.cs
new file mode 100644
index 000000000..2bc3f2323
--- /dev/null
+++ b/src/Core/Models/Data/Fido2AssertionResponse.cs
@@ -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; }
+ }
+}
diff --git a/src/Core/Models/Data/Fido2AuthenticatorSelection.cs b/src/Core/Models/Data/Fido2AuthenticatorSelection.cs
new file mode 100644
index 000000000..03a168bea
--- /dev/null
+++ b/src/Core/Models/Data/Fido2AuthenticatorSelection.cs
@@ -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; }
+ }
+}
diff --git a/src/Core/Models/Data/Fido2CredentialDescriptor.cs b/src/Core/Models/Data/Fido2CredentialDescriptor.cs
new file mode 100644
index 000000000..f4f8cb033
--- /dev/null
+++ b/src/Core/Models/Data/Fido2CredentialDescriptor.cs
@@ -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 Transports { get; set; }
+ }
+}
diff --git a/src/Core/Models/Data/Fido2PubKeyCredParam.cs b/src/Core/Models/Data/Fido2PubKeyCredParam.cs
new file mode 100644
index 000000000..0d6b59a2f
--- /dev/null
+++ b/src/Core/Models/Data/Fido2PubKeyCredParam.cs
@@ -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; }
+ }
+}
diff --git a/src/Core/Models/Data/Fido2RP.cs b/src/Core/Models/Data/Fido2RP.cs
new file mode 100644
index 000000000..b219e9e63
--- /dev/null
+++ b/src/Core/Models/Data/Fido2RP.cs
@@ -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; }
+ }
+}
diff --git a/src/Core/Models/Data/Fido2User.cs b/src/Core/Models/Data/Fido2User.cs
new file mode 100644
index 000000000..af1da6a71
--- /dev/null
+++ b/src/Core/Models/Data/Fido2User.cs
@@ -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; }
+ }
+}
diff --git a/src/Core/Models/Request/Fido2AuthenticationChallengeRequest.cs b/src/Core/Models/Request/Fido2AuthenticationChallengeRequest.cs
new file mode 100644
index 000000000..8405e4f03
--- /dev/null
+++ b/src/Core/Models/Request/Fido2AuthenticationChallengeRequest.cs
@@ -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; }
+ }
+}
diff --git a/src/Core/Models/Response/Fido2AuthenticationChallengeResponse.cs b/src/Core/Models/Response/Fido2AuthenticationChallengeResponse.cs
new file mode 100644
index 000000000..ceb1e0657
--- /dev/null
+++ b/src/Core/Models/Response/Fido2AuthenticationChallengeResponse.cs
@@ -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 AllowCredentials { get; set; }
+ [JsonProperty("userVerification")]
+ public string UserVerification { get; set; }
+ [JsonProperty("extensions", NullValueHandling = NullValueHandling.Ignore)]
+ public object Extensions { get; set; }
+ }
+}
diff --git a/src/Core/Models/Response/Fido2RegistrationChallengeResponse.cs b/src/Core/Models/Response/Fido2RegistrationChallengeResponse.cs
new file mode 100644
index 000000000..bbc47cdd6
--- /dev/null
+++ b/src/Core/Models/Response/Fido2RegistrationChallengeResponse.cs
@@ -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 PubKeyCredParams { get; set; }
+ [JsonProperty("excludeCredentials")]
+ public List 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; }
+ }
+}