diff --git a/src/Api/Controllers/TwoFactorController.cs b/src/Api/Controllers/TwoFactorController.cs index be005a7f35..9a02e2c937 100644 --- a/src/Api/Controllers/TwoFactorController.cs +++ b/src/Api/Controllers/TwoFactorController.cs @@ -199,18 +199,18 @@ namespace Bit.Api.Controllers public async Task GetU2f([FromBody]TwoFactorRequestModel model) { var user = await CheckAsync(model.MasterPasswordHash, true); - var provider = user.GetTwoFactorProvider(TwoFactorProviderType.U2f); - if(provider == null || !provider.Enabled || (provider.MetaData?.Count ?? 0) > 0) - { - var reg = await _userService.StartU2fRegistrationAsync(user); - var response = new TwoFactorU2fResponseModel(user, provider, reg); - return response; - } - else - { - var response = new TwoFactorU2fResponseModel(user, provider); - return response; - } + var response = new TwoFactorU2fResponseModel(user); + return response; + } + + [HttpPost("get-u2f-challenge")] + public async Task GetU2fChallenge( + [FromBody]TwoFactorRequestModel model) + { + var user = await CheckAsync(model.MasterPasswordHash, true); + var reg = await _userService.StartU2fRegistrationAsync(user); + var challenge = new TwoFactorU2fResponseModel.ChallengeModel(user, reg); + return challenge; } [HttpPut("u2f")] @@ -218,7 +218,16 @@ namespace Bit.Api.Controllers public async Task PutU2f([FromBody]TwoFactorU2fRequestModel model) { var user = await CheckAsync(model.MasterPasswordHash, true); - await _userService.CompleteU2fRegistrationAsync(user, model.DeviceResponse); + await _userService.CompleteU2fRegistrationAsync(user, model.Id.Value, model.Name, model.DeviceResponse); + var response = new TwoFactorU2fResponseModel(user); + return response; + } + + [HttpDelete("u2f")] + public async Task DeleteU2f([FromBody]TwoFactorU2fDeleteRequestModel model) + { + var user = await CheckAsync(model.MasterPasswordHash, true); + await _userService.DeleteU2fKeyAsync(user, model.Id.Value); var response = new TwoFactorU2fResponseModel(user); return response; } @@ -284,7 +293,7 @@ namespace Bit.Api.Controllers var response = new TwoFactorProviderResponseModel(model.Type.Value, user); return response; } - + [HttpPut("~/organizations/{id}/two-factor/disable")] [HttpPost("~/organizations/{id}/two-factor/disable")] public async Task PutOrganizationDisable(string id, diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index bac5392f74..f5659acfca 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -35,10 +35,9 @@ - - + diff --git a/src/Core/Identity/U2fTokenProvider.cs b/src/Core/Identity/U2fTokenProvider.cs index e03efdcf41..5ad3502e23 100644 --- a/src/Core/Identity/U2fTokenProvider.cs +++ b/src/Core/Identity/U2fTokenProvider.cs @@ -10,6 +10,7 @@ using System.Linq; using U2fLib = U2F.Core.Crypto.U2F; using U2F.Core.Models; using U2F.Core.Exceptions; +using U2F.Core.Utils; using System; using Bit.Core.Services; using Microsoft.Extensions.DependencyInjection; @@ -58,19 +59,7 @@ namespace Bit.Core.Identity } var provider = user.GetTwoFactorProvider(TwoFactorProviderType.U2f); - if(!HasProperMetaData(provider)) - { - return null; - } - - var keys = new List(); - - var key1 = new TwoFactorProvider.U2fMetaData((dynamic)provider.MetaData["Key1"]); - if(!key1?.Compromised ?? false) - { - keys.Add(key1); - } - + var keys = LoadKeys(provider); if(keys.Count == 0) { return null; @@ -78,14 +67,16 @@ namespace Bit.Core.Identity await _u2fRepository.DeleteManyByUserIdAsync(user.Id); + var challengeBytes = U2fLib.Crypto.GenerateChallenge(); 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(Utilities.CoreHelpers.U2fAppIdUrl(_globalSettings), registration); + var registration = new DeviceRegistration(key.Item2.KeyHandleBytes, key.Item2.PublicKeyBytes, + key.Item2.CertificateBytes, key.Item2.Counter); + var auth = U2fLib.StartAuthentication(Utilities.CoreHelpers.U2fAppIdUrl(_globalSettings), registration, + challengeBytes); - // Maybe move this to a bulk create when we support more than 1 key? + // TODO: Maybe move this to a bulk create? await _u2fRepository.CreateAsync(new U2f { AppId = auth.AppId, @@ -118,26 +109,14 @@ namespace Bit.Core.Identity } var provider = user.GetTwoFactorProvider(TwoFactorProviderType.U2f); - if(!HasProperMetaData(provider)) - { - return false; - } - - var keys = new List(); - - var key1 = new TwoFactorProvider.U2fMetaData((dynamic)provider.MetaData["Key1"]); - if(!key1?.Compromised ?? false) - { - keys.Add(key1); - } - + var keys = LoadKeys(provider); if(keys.Count == 0) { return false; } var authenticateResponse = BaseModel.FromJson(token); - var key = keys.FirstOrDefault(f => f.KeyHandle == authenticateResponse.KeyHandle); + var key = keys.FirstOrDefault(f => f.Item2.KeyHandle == authenticateResponse.KeyHandle); if(key == null) { @@ -159,8 +138,8 @@ namespace Bit.Core.Identity } var success = true; - var registration = new DeviceRegistration(key.KeyHandleBytes, key.PublicKeyBytes, key.CertificateBytes, - key.Counter); + var registration = new DeviceRegistration(key.Item2.KeyHandleBytes, key.Item2.PublicKeyBytes, + key.Item2.CertificateBytes, key.Item2.Counter); try { var auth = new StartedAuthentication(challenge.Challenge, challenge.AppId, challenge.KeyHandle); @@ -173,14 +152,14 @@ namespace Bit.Core.Identity // Update database await _u2fRepository.DeleteManyByUserIdAsync(user.Id); - key.Counter = registration.Counter; - if(key.Counter > 0) + key.Item2.Counter = registration.Counter; + if(key.Item2.Counter > 0) { - key.Compromised = registration.IsCompromised; + key.Item2.Compromised = registration.IsCompromised; } var providers = user.GetTwoFactorProviders(); - providers[TwoFactorProviderType.U2f].MetaData["Key1"] = key; + providers[TwoFactorProviderType.U2f].MetaData[key.Item1] = key.Item2; user.SetTwoFactorProviders(providers); await manager.UpdateAsync(user); @@ -189,7 +168,32 @@ namespace Bit.Core.Identity private bool HasProperMetaData(TwoFactorProvider provider) { - return provider?.MetaData != null && provider.MetaData.ContainsKey("Key1"); + return (provider?.MetaData?.Count ?? 0) > 0; + } + + private List> LoadKeys(TwoFactorProvider provider) + { + var keys = new List>(); + if(!HasProperMetaData(provider)) + { + return keys; + } + + // Support up to 5 keys + for(var i = 1; i <= 5; i++) + { + var keyName = $"Key{i}"; + if(provider.MetaData.ContainsKey(keyName)) + { + var key = new TwoFactorProvider.U2fMetaData((dynamic)provider.MetaData[keyName]); + if(!key?.Compromised ?? false) + { + keys.Add(new Tuple(keyName, key)); + } + } + } + + return keys; } } } diff --git a/src/Core/Models/Api/Request/TwoFactorRequestModels.cs b/src/Core/Models/Api/Request/TwoFactorRequestModels.cs index 59df738508..6cd1302d3a 100644 --- a/src/Core/Models/Api/Request/TwoFactorRequestModels.cs +++ b/src/Core/Models/Api/Request/TwoFactorRequestModels.cs @@ -223,10 +223,25 @@ namespace Bit.Core.Models.Api } } - public class TwoFactorU2fRequestModel : TwoFactorRequestModel + public class TwoFactorU2fRequestModel : TwoFactorU2fDeleteRequestModel { [Required] public string DeviceResponse { get; set; } + public string Name { get; set; } + } + + public class TwoFactorU2fDeleteRequestModel : TwoFactorRequestModel, IValidatableObject + { + [Required] + public int? Id { get; set; } + + public IEnumerable Validate(ValidationContext validationContext) + { + if(!Id.HasValue || Id < 0 || Id > 5) + { + yield return new ValidationResult("Invalid Key Id", new string[] { nameof(Id) }); + } + } } public class UpdateTwoFactorEmailRequestModel : TwoFactorEmailRequestModel diff --git a/src/Core/Models/Api/Response/TwoFactor/TwoFactorU2fResponseModel.cs b/src/Core/Models/Api/Response/TwoFactor/TwoFactorU2fResponseModel.cs index 1f934089a0..2f37b383ea 100644 --- a/src/Core/Models/Api/Response/TwoFactor/TwoFactorU2fResponseModel.cs +++ b/src/Core/Models/Api/Response/TwoFactor/TwoFactorU2fResponseModel.cs @@ -2,26 +2,13 @@ using Bit.Core.Models.Table; using Bit.Core.Models.Business; using Bit.Core.Enums; +using System.Collections.Generic; +using System.Linq; namespace Bit.Core.Models.Api { public class TwoFactorU2fResponseModel : ResponseModel { - public TwoFactorU2fResponseModel(User user, TwoFactorProvider provider, U2fRegistration registration = null) - : base("twoFactorU2f") - { - if(user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - if(registration != null) - { - Challenge = new ChallengeModel(user, registration); - } - Enabled = provider?.Enabled ?? false; - } - public TwoFactorU2fResponseModel(User user) : base("twoFactorU2f") { @@ -31,11 +18,27 @@ namespace Bit.Core.Models.Api } var provider = user.GetTwoFactorProvider(TwoFactorProviderType.U2f); - Enabled = provider != null && provider.Enabled; + Enabled = provider?.Enabled ?? false; + Keys = provider?.MetaData?.Select(k => new KeyModel(k.Key, + new TwoFactorProvider.U2fMetaData((dynamic)k.Value))); } - public ChallengeModel Challenge { get; set; } public bool Enabled { get; set; } + public IEnumerable Keys { get; set; } + + public class KeyModel + { + public KeyModel(string id, TwoFactorProvider.U2fMetaData data) + { + Name = data.Name; + Id = Convert.ToInt32(id.Replace("Key", string.Empty)); + Compromised = data.Compromised; + } + + public string Name { get; set; } + public int Id { get; set; } + public bool Compromised { get; set; } + } public class ChallengeModel { diff --git a/src/Core/Models/TwoFactorProvider.cs b/src/Core/Models/TwoFactorProvider.cs index 79d23c5226..f017e0ac56 100644 --- a/src/Core/Models/TwoFactorProvider.cs +++ b/src/Core/Models/TwoFactorProvider.cs @@ -17,6 +17,7 @@ namespace Bit.Core.Models public U2fMetaData(dynamic o) { + Name = o.Name; KeyHandle = o.KeyHandle; PublicKey = o.PublicKey; Certificate = o.Certificate; @@ -24,6 +25,7 @@ namespace Bit.Core.Models Compromised = o.Compromised; } + public string Name { get; set; } public string KeyHandle { get; set; } [JsonIgnore] public byte[] KeyHandleBytes => diff --git a/src/Core/Services/IUserService.cs b/src/Core/Services/IUserService.cs index b0bd6fe47d..a69fd10ca1 100644 --- a/src/Core/Services/IUserService.cs +++ b/src/Core/Services/IUserService.cs @@ -23,7 +23,8 @@ namespace Bit.Core.Services Task SendTwoFactorEmailAsync(User user); Task VerifyTwoFactorEmailAsync(User user, string token); Task StartU2fRegistrationAsync(User user); - Task CompleteU2fRegistrationAsync(User user, string deviceResponse); + Task DeleteU2fKeyAsync(User user, int id); + Task CompleteU2fRegistrationAsync(User user, int id, string name, string deviceResponse); Task SendEmailVerificationAsync(User user); Task ConfirmEmailAsync(User user, string token); Task InitiateEmailChangeAsync(User user, string newEmail); diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index 33106e03e9..5de270343e 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -307,7 +307,7 @@ namespace Bit.Core.Services }; } - public async Task CompleteU2fRegistrationAsync(User user, string deviceResponse) + public async Task CompleteU2fRegistrationAsync(User user, int id, string name, string deviceResponse) { if(string.IsNullOrWhiteSpace(deviceResponse)) { @@ -323,8 +323,8 @@ namespace Bit.Core.Services var registerResponse = BaseModel.FromJson(deviceResponse); var challenge = challenges.OrderBy(i => i.Id).Last(i => i.KeyHandle == null); - var statedReg = new StartedRegistration(challenge.Challenge, challenge.AppId); - var reg = U2fLib.FinishRegistration(statedReg, registerResponse); + var startedReg = new StartedRegistration(challenge.Challenge, challenge.AppId); + var reg = U2fLib.FinishRegistration(startedReg, registerResponse); await _u2fRepository.DeleteManyByUserIdAsync(user.Id); @@ -334,29 +334,74 @@ namespace Bit.Core.Services { providers = new Dictionary(); } - else if(providers.ContainsKey(TwoFactorProviderType.U2f)) + var provider = user.GetTwoFactorProvider(TwoFactorProviderType.U2f); + if(provider == null) + { + provider = new TwoFactorProvider(); + } + if(provider.MetaData == null) + { + provider.MetaData = new Dictionary(); + } + + if(provider.MetaData.Count >= 5) + { + // Can only register up to 5 keys + return false; + } + + var keyId = $"Key{id}"; + if(provider.MetaData.ContainsKey(keyId)) + { + provider.MetaData.Remove(keyId); + } + + provider.Enabled = true; + provider.MetaData.Add(keyId, new TwoFactorProvider.U2fMetaData + { + Name = name, + 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 + }); + + if(providers.ContainsKey(TwoFactorProviderType.U2f)) { providers.Remove(TwoFactorProviderType.U2f); } - providers.Add(TwoFactorProviderType.U2f, new TwoFactorProvider - { - MetaData = new Dictionary - { - ["Key1"] = new TwoFactorProvider.U2fMetaData - { - 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 - } - }, - Enabled = true - }); + providers.Add(TwoFactorProviderType.U2f, provider); user.SetTwoFactorProviders(providers); await UpdateTwoFactorProviderAsync(user, TwoFactorProviderType.U2f); + return true; + } + public async Task DeleteU2fKeyAsync(User user, int id) + { + var providers = user.GetTwoFactorProviders(); + if(providers == null) + { + return false; + } + + var keyName = $"Key{id}"; + var provider = user.GetTwoFactorProvider(TwoFactorProviderType.U2f); + if(!provider?.MetaData?.ContainsKey(keyName) ?? true) + { + return false; + } + + if(provider.MetaData.Count < 2) + { + return false; + } + + provider.MetaData.Remove(keyName); + providers[TwoFactorProviderType.U2f] = provider; + user.SetTwoFactorProviders(providers); + await UpdateTwoFactorProviderAsync(user, TwoFactorProviderType.U2f); return true; }