diff --git a/src/Api/Controllers/TwoFactorController.cs b/src/Api/Controllers/TwoFactorController.cs index 07bb067801..405c23cfe7 100644 --- a/src/Api/Controllers/TwoFactorController.cs +++ b/src/Api/Controllers/TwoFactorController.cs @@ -116,8 +116,18 @@ namespace Bit.Api.Controllers public async Task GetU2f([FromBody]TwoFactorRequestModel model) { var user = await CheckPasswordAsync(model.MasterPasswordHash); - var response = new TwoFactorU2fResponseModel(user); - return response; + var provider = user.GetTwoFactorProvider(TwoFactorProviderType.U2f); + if(!provider.Enabled || (provider?.MetaData != null && provider.MetaData.Count > 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; + } } [HttpPut("u2f")] @@ -125,8 +135,7 @@ namespace Bit.Api.Controllers public async Task PutU2f([FromBody]TwoFactorU2fRequestModel model) { var user = await CheckPasswordAsync(model.MasterPasswordHash); - model.ToUser(user); - await _userService.UpdateTwoFactorProviderAsync(user, TwoFactorProviderType.U2f); + await _userService.CompleteU2fRegistrationAsync(user, model.DeviceResponse); var response = new TwoFactorU2fResponseModel(user); return response; } diff --git a/src/Api/settings.json b/src/Api/settings.json index e768d56c9d..b8d65c7412 100644 --- a/src/Api/settings.json +++ b/src/Api/settings.json @@ -45,6 +45,9 @@ }, "duo": { "aKey": "SECRET" + }, + "u2f": { + "appId": "https://bitwarden.com" } }, "IpRateLimitOptions": { diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index e10f533744..2784bfec9e 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -55,6 +55,7 @@ + diff --git a/src/Core/GlobalSettings.cs b/src/Core/GlobalSettings.cs index a410e0a9be..aa22660be9 100644 --- a/src/Core/GlobalSettings.cs +++ b/src/Core/GlobalSettings.cs @@ -17,6 +17,7 @@ public virtual NotificationHubSettings NotificationHub { get; set; } = new NotificationHubSettings(); public virtual YubicoSettings Yubico { get; set; } = new YubicoSettings(); public virtual DuoSettings Duo { get; set; } = new DuoSettings(); + public virtual U2fSettings U2f { get; set; } = new U2fSettings(); public class SqlServerSettings { @@ -91,5 +92,10 @@ { public string AKey { get; set; } } + + public class U2fSettings + { + public string AppId { get; set; } + } } } diff --git a/src/Core/Models/Api/Request/TwoFactorRequestModels.cs b/src/Core/Models/Api/Request/TwoFactorRequestModels.cs index c23c259608..3a48d40bb5 100644 --- a/src/Core/Models/Api/Request/TwoFactorRequestModels.cs +++ b/src/Core/Models/Api/Request/TwoFactorRequestModels.cs @@ -199,33 +199,6 @@ namespace Bit.Core.Models.Api { [Required] public string DeviceResponse { get; set; } - - public User ToUser(User extistingUser) - { - var providers = extistingUser.GetTwoFactorProviders(); - if(providers == null) - { - providers = new Dictionary(); - } - else if(providers.ContainsKey(TwoFactorProviderType.U2f)) - { - providers.Remove(TwoFactorProviderType.U2f); - } - - providers.Add(TwoFactorProviderType.U2f, new TwoFactorProvider - { - MetaData = new Dictionary - { - ["Key1"] = new TwoFactorProvider.U2fMetaData - { - // TODO - } - }, - Enabled = true - }); - extistingUser.SetTwoFactorProviders(providers); - return extistingUser; - } } 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 aa211a830f..9ba0959794 100644 --- a/src/Core/Models/Api/Response/TwoFactor/TwoFactorU2fResponseModel.cs +++ b/src/Core/Models/Api/Response/TwoFactor/TwoFactorU2fResponseModel.cs @@ -1,11 +1,27 @@ using System; -using Bit.Core.Enums; using Bit.Core.Models.Table; +using Bit.Core.Models.Business; +using Bit.Core.Enums; 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; + } + public TwoFactorU2fResponseModel(User user) : base("twoFactorU2f") { @@ -15,18 +31,7 @@ namespace Bit.Core.Models.Api } var provider = user.GetTwoFactorProvider(TwoFactorProviderType.U2f); - if(provider?.MetaData != null && provider.MetaData.Count > 0) - { - Challenge = new ChallengeModel - { - // TODO - }; - Enabled = provider.Enabled; - } - else - { - Enabled = false; - } + Enabled = provider != null && provider.Enabled; } public ChallengeModel Challenge { get; set; } @@ -34,6 +39,14 @@ namespace Bit.Core.Models.Api public class ChallengeModel { + public ChallengeModel(User user, U2fRegistration registration) + { + UserId = user.Id.ToString(); + AppId = registration.AppId; + Challenge = registration.Challenge; + Version = registration.Version; + } + public string UserId { get; set; } public string AppId { get; set; } public string Challenge { get; set; } diff --git a/src/Core/Models/Business/U2fRegistration.cs b/src/Core/Models/Business/U2fRegistration.cs new file mode 100644 index 0000000000..c27afdc40a --- /dev/null +++ b/src/Core/Models/Business/U2fRegistration.cs @@ -0,0 +1,9 @@ +namespace Bit.Core.Models.Business +{ + public class U2fRegistration + { + public string AppId { get; set; } + public string Challenge { get; set; } + public string Version { get; set; } + } +} diff --git a/src/Core/Models/TwoFactorProvider.cs b/src/Core/Models/TwoFactorProvider.cs index e7442350b5..fea8f9c789 100644 --- a/src/Core/Models/TwoFactorProvider.cs +++ b/src/Core/Models/TwoFactorProvider.cs @@ -12,7 +12,7 @@ namespace Bit.Core.Models public string KeyHandle { get; set; } public string PublicKey { get; set; } public string Certificate { get; set; } - public int Counter { get; set; } + public uint Counter { get; set; } public bool Compromised { get; set; } } } diff --git a/src/Core/Services/IUserService.cs b/src/Core/Services/IUserService.cs index 302837ab37..02f3969fc5 100644 --- a/src/Core/Services/IUserService.cs +++ b/src/Core/Services/IUserService.cs @@ -6,6 +6,7 @@ using Bit.Core.Models.Table; using System.Security.Claims; using Bit.Core.Enums; using Bit.Core.Models; +using Bit.Core.Models.Business; namespace Bit.Core.Services { @@ -21,6 +22,8 @@ namespace Bit.Core.Services Task SendMasterPasswordHintAsync(string email); Task SendTwoFactorEmailAsync(User user); Task VerifyTwoFactorEmailAsync(User user, string token); + Task StartU2fRegistrationAsync(User user); + Task CompleteU2fRegistrationAsync(User user, string deviceResponse); Task InitiateEmailChangeAsync(User user, string newEmail); Task ChangeEmailAsync(User user, string masterPassword, string newEmail, string newMasterPassword, string token, string key); diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index b82c8db2a1..c51a1a1f65 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -9,9 +9,11 @@ using Bit.Core.Repositories; using System.Linq; using Microsoft.AspNetCore.Builder; using Bit.Core.Enums; -using OtpNet; using System.Security.Claims; +using U2fLib = U2F.Core.Crypto.U2F; +using U2F.Core.Models; using Bit.Core.Models; +using Bit.Core.Models.Business; namespace Bit.Core.Services { @@ -20,6 +22,7 @@ namespace Bit.Core.Services private readonly IUserRepository _userRepository; private readonly ICipherRepository _cipherRepository; private readonly IOrganizationUserRepository _organizationUserRepository; + private readonly IU2fRepository _u2fRepository; private readonly IMailService _mailService; private readonly IPushNotificationService _pushService; private readonly IdentityErrorDescriber _identityErrorDescriber; @@ -27,11 +30,13 @@ namespace Bit.Core.Services private readonly IPasswordHasher _passwordHasher; private readonly IEnumerable> _passwordValidators; private readonly CurrentContext _currentContext; + private readonly GlobalSettings _globalSettings; public UserService( IUserRepository userRepository, ICipherRepository cipherRepository, IOrganizationUserRepository organizationUserRepository, + IU2fRepository u2fRepository, IMailService mailService, IPushNotificationService pushService, IUserStore store, @@ -43,7 +48,8 @@ namespace Bit.Core.Services IdentityErrorDescriber errors, IServiceProvider services, ILogger> logger, - CurrentContext currentContext) + CurrentContext currentContext, + GlobalSettings globalSettings) : base( store, optionsAccessor, @@ -58,6 +64,7 @@ namespace Bit.Core.Services _userRepository = userRepository; _cipherRepository = cipherRepository; _organizationUserRepository = organizationUserRepository; + _u2fRepository = u2fRepository; _mailService = mailService; _pushService = pushService; _identityOptions = optionsAccessor?.Value ?? new IdentityOptions(); @@ -65,6 +72,7 @@ namespace Bit.Core.Services _passwordHasher = passwordHasher; _passwordValidators = passwordValidators; _currentContext = currentContext; + _globalSettings = globalSettings; } public Guid? GetProperUserId(ClaimsPrincipal principal) @@ -207,6 +215,79 @@ namespace Bit.Core.Services "2faEmail:" + provider.MetaData["Email"], token); } + public async Task StartU2fRegistrationAsync(User user) + { + await _u2fRepository.DeleteManyByUserIdAsync(user.Id); + var reg = U2fLib.StartRegistration(_globalSettings.U2f.AppId); + await _u2fRepository.CreateAsync(new U2f + { + AppId = reg.AppId, + Challenge = reg.Challenge, + Version = reg.Version, + UserId = user.Id + }); + + return new U2fRegistration + { + AppId = reg.AppId, + Challenge = reg.Challenge, + Version = reg.Version + }; + } + + public async Task CompleteU2fRegistrationAsync(User user, string deviceResponse) + { + if(string.IsNullOrWhiteSpace(deviceResponse)) + { + return false; + } + + var challenges = await _u2fRepository.GetManyByUserIdAsync(user.Id); + if(!challenges?.Any() ?? true) + { + return false; + } + + 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); + + await _u2fRepository.DeleteManyByUserIdAsync(user.Id); + + // Add device + var providers = user.GetTwoFactorProviders(); + if(providers == null) + { + providers = new Dictionary(); + } + else 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 : Convert.ToBase64String(reg.KeyHandle), + PublicKey = reg.PublicKey == null ? null : Convert.ToBase64String(reg.PublicKey), + Certificate = reg.AttestationCert == null ? null : Convert.ToBase64String(reg.AttestationCert), + Compromised = false, + Counter = reg.Counter + } + }, + Enabled = true + }); + user.SetTwoFactorProviders(providers); + await UpdateTwoFactorProviderAsync(user, TwoFactorProviderType.U2f); + + return true; + } + public async Task InitiateEmailChangeAsync(User user, string newEmail) { var existingUser = await _userRepository.GetByEmailAsync(newEmail); diff --git a/src/Core/Utilities/ServiceCollectionExtensions.cs b/src/Core/Utilities/ServiceCollectionExtensions.cs index 2ebe6f5e4a..712cbe364f 100644 --- a/src/Core/Utilities/ServiceCollectionExtensions.cs +++ b/src/Core/Utilities/ServiceCollectionExtensions.cs @@ -32,6 +32,7 @@ namespace Bit.Core.Utilities services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); } public static void AddBaseServices(this IServiceCollection services) diff --git a/src/Identity/settings.json b/src/Identity/settings.json index 9046e7e48b..d15eca2585 100644 --- a/src/Identity/settings.json +++ b/src/Identity/settings.json @@ -41,6 +41,9 @@ }, "duo": { "aKey": "SECRET" + }, + "u2f": { + "appId": "https://bitwarden.com" } } }