diff --git a/src/Core/Identity/U2fTokenProvider.cs b/src/Core/Identity/U2fTokenProvider.cs new file mode 100644 index 0000000000..eba3e0e43d --- /dev/null +++ b/src/Core/Identity/U2fTokenProvider.cs @@ -0,0 +1,167 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Bit.Core.Models.Table; +using Bit.Core.Enums; +using Bit.Core.Models; +using Bit.Core.Services; +using Bit.Core.Repositories; +using U2F.Core.Models; +using U2fLib = U2F.Core.Crypto.U2F; +using Newtonsoft.Json; +using System.Collections.Generic; +using System.Linq; +using U2F.Core.Exceptions; + +namespace Bit.Core.Identity +{ + public class U2fTokenProvider : IUserTwoFactorTokenProvider + { + private readonly IU2fRepository _u2fRepository; + private readonly IUserService _userService; + private readonly GlobalSettings _globalSettings; + + public U2fTokenProvider( + IU2fRepository u2fRepository, + IUserService userService, + GlobalSettings globalSettings) + { + _u2fRepository = u2fRepository; + _userService = userService; + _globalSettings = globalSettings; + } + + public Task CanGenerateTwoFactorTokenAsync(UserManager manager, User user) + { + var provider = user.GetTwoFactorProvider(TwoFactorProviderType.U2f); + var canGenerate = user.TwoFactorProviderIsEnabled(TwoFactorProviderType.U2f) && HasProperMetaData(provider); + return Task.FromResult(canGenerate); + } + + public async Task GenerateAsync(string purpose, UserManager manager, User user) + { + var provider = user.GetTwoFactorProvider(TwoFactorProviderType.U2f); + if(!HasProperMetaData(provider)) + { + return null; + } + + var keys = new List(); + + var key1 = provider.MetaData["Key1"] as TwoFactorProvider.U2fMetaData; + if(!key1?.Compromised ?? false) + { + keys.Add(key1); + } + + if(keys.Count == 0) + { + return null; + } + + await _u2fRepository.DeleteManyByUserIdAsync(user.Id); + + var challenges = new List(); + foreach(var key in keys) + { + var registration = new DeviceRegistration(key.KeyHandleBytes, key.PublicKeyBytes, + key.CertificateBytes, key.Counter); + var auth = U2fLib.StartAuthentication(_globalSettings.U2f.AppId, registration); + + // Maybe move this to a bulk create when we support more than 1 key? + await _u2fRepository.CreateAsync(new U2f + { + AppId = auth.AppId, + Challenge = auth.Challenge, + KeyHandle = auth.KeyHandle, + Version = auth.Version, + UserId = user.Id + }); + + challenges.Add(new + { + appId = auth.AppId, + challenge = auth.Challenge, + keyHandle = auth.KeyHandle, + version = auth.Version + }); + } + + var token = JsonConvert.SerializeObject(challenges); + return token; + } + + public async Task ValidateAsync(string purpose, string token, UserManager manager, User user) + { + if(string.IsNullOrWhiteSpace(token)) + { + return false; + } + + var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Duo); + if(!HasProperMetaData(provider)) + { + return false; + } + + var keys = new List(); + + var key1 = provider.MetaData["Key1"] as TwoFactorProvider.U2fMetaData; + if(!key1?.Compromised ?? false) + { + keys.Add(key1); + } + + if(keys.Count == 0) + { + return false; + } + + var authenticateResponse = BaseModel.FromJson(token); + var key = keys.FirstOrDefault(f => f.KeyHandle == authenticateResponse.KeyHandle); + + if(key == null) + { + return false; + } + + var challenges = await _u2fRepository.GetManyByUserIdAsync(user.Id); + if(challenges.Count == 0) + { + return false; + } + + var success = true; + // User will have a authentication request for each device they have registered so get the one that matches + // the device key handle + var challenge = challenges.First(c => c.KeyHandle == authenticateResponse.KeyHandle); + var registration = new DeviceRegistration(key.KeyHandleBytes, key.PublicKeyBytes, key.CertificateBytes, + key.Counter); + try + { + var auth = new StartedAuthentication(challenge.Challenge, challenge.AppId, challenge.KeyHandle); + U2fLib.FinishAuthentication(auth, authenticateResponse, registration); + } + catch(U2fException) + { + success = false; + } + + // Update database + await _u2fRepository.DeleteManyByUserIdAsync(user.Id); + key.Counter = registration.Counter; + key.Compromised = registration.IsCompromised; + + var providers = user.GetTwoFactorProviders(); + providers[TwoFactorProviderType.U2f].MetaData["Key1"] = key; + user.SetTwoFactorProviders(providers); + await _userService.SaveUserAsync(user); + + return success; + } + + private bool HasProperMetaData(TwoFactorProvider provider) + { + return provider?.MetaData != null && provider.MetaData.ContainsKey("Key1"); + } + } +} diff --git a/src/Core/IdentityServer/ResourceOwnerPasswordValidator.cs b/src/Core/IdentityServer/ResourceOwnerPasswordValidator.cs index 98c538f445..ba7bf0131a 100644 --- a/src/Core/IdentityServer/ResourceOwnerPasswordValidator.cs +++ b/src/Core/IdentityServer/ResourceOwnerPasswordValidator.cs @@ -193,8 +193,10 @@ namespace Bit.Core.IdentityServer } else if(type == TwoFactorProviderType.U2f) { - // TODO: U2F challenge - return new Dictionary { }; + return new Dictionary + { + ["Challenges"] = token + }; } return null; default: diff --git a/src/Core/Models/TwoFactorProvider.cs b/src/Core/Models/TwoFactorProvider.cs index fea8f9c789..9ec16ccc22 100644 --- a/src/Core/Models/TwoFactorProvider.cs +++ b/src/Core/Models/TwoFactorProvider.cs @@ -1,4 +1,6 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using U2F.Core.Utils; namespace Bit.Core.Models { @@ -10,8 +12,14 @@ namespace Bit.Core.Models public class U2fMetaData { public string KeyHandle { get; set; } + public byte[] KeyHandleBytes => + string.IsNullOrWhiteSpace(KeyHandle) ? null : Utils.Base64StringToByteArray(KeyHandle); public string PublicKey { get; set; } + public byte[] PublicKeyBytes => + string.IsNullOrWhiteSpace(PublicKey) ? null : Utils.Base64StringToByteArray(PublicKey); public string Certificate { get; set; } + public byte[] CertificateBytes => + string.IsNullOrWhiteSpace(Certificate) ? null : Utils.Base64StringToByteArray(Certificate); public uint Counter { get; set; } public bool Compromised { get; set; } } diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index c51a1a1f65..5168db085b 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -14,6 +14,7 @@ using U2fLib = U2F.Core.Crypto.U2F; using U2F.Core.Models; using Bit.Core.Models; using Bit.Core.Models.Business; +using U2F.Core.Utils; namespace Bit.Core.Services { @@ -273,9 +274,9 @@ namespace Bit.Core.Services { ["Key1"] = new TwoFactorProvider.U2fMetaData { - KeyHandle = reg.KeyHandle == null ? null : Convert.ToBase64String(reg.KeyHandle), - PublicKey = reg.PublicKey == null ? null : Convert.ToBase64String(reg.PublicKey), - Certificate = reg.AttestationCert == null ? null : Convert.ToBase64String(reg.AttestationCert), + KeyHandle = reg.KeyHandle == null ? null : Utils.ByteArrayToBase64String(reg.KeyHandle), + PublicKey = reg.PublicKey == null ? null : Utils.ByteArrayToBase64String(reg.PublicKey), + Certificate = reg.AttestationCert == null ? null : Utils.ByteArrayToBase64String(reg.AttestationCert), Compromised = false, Counter = reg.Counter }