diff --git a/src/Api/Controllers/AccountsController.cs b/src/Api/Controllers/AccountsController.cs index e253b94d01..f5d6f51dc4 100644 --- a/src/Api/Controllers/AccountsController.cs +++ b/src/Api/Controllers/AccountsController.cs @@ -10,6 +10,7 @@ using Bit.Core.Models.Table; using Bit.Core.Enums; using System.Linq; using Bit.Core.Repositories; +using System.Collections.Generic; namespace Bit.Api.Controllers { @@ -262,12 +263,13 @@ namespace Bit.Api.Controllers throw new BadRequestException("MasterPasswordHash", "Invalid password."); } - await _userService.GetTwoFactorAsync(user, provider); + await _userService.SetupTwoFactorAsync(user, provider); var response = new TwoFactorResponseModel(user); return response; } + [Obsolete] [HttpPut("two-factor")] [HttpPost("two-factor")] public async Task PutTwoFactor([FromBody]UpdateTwoFactorRequestModel model) @@ -290,10 +292,8 @@ namespace Bit.Api.Controllers throw new BadRequestException("Token", "Invalid token."); } - user.TwoFactorProvider = TwoFactorProviderType.Authenticator; user.TwoFactorEnabled = model.Enabled.Value; - user.TwoFactorRecoveryCode = user.TwoFactorEnabled ? Guid.NewGuid().ToString("N") : null; - await _userService.SaveUserAsync(user); + await _userService.UpdateTwoFactorProviderAsync(user, TwoFactorProviderType.Authenticator); var response = new TwoFactorResponseModel(user); return response; @@ -310,38 +310,6 @@ namespace Bit.Api.Controllers } } - [HttpPut("two-factor-regenerate")] - [HttpPost("two-factor-regenerate")] - public async Task PutTwoFactorRegenerate([FromBody]RegenerateTwoFactorRequestModel model) - { - var user = await _userService.GetUserByPrincipalAsync(User); - if(user == null) - { - throw new UnauthorizedAccessException(); - } - - if(!await _userManager.CheckPasswordAsync(user, model.MasterPasswordHash)) - { - await Task.Delay(2000); - throw new BadRequestException("MasterPasswordHash", "Invalid password."); - } - - if(!await _userManager.VerifyTwoFactorTokenAsync(user, TwoFactorProviderType.Authenticator.ToString(), model.Token)) - { - await Task.Delay(2000); - throw new BadRequestException("Token", "Invalid token."); - } - - if(user.TwoFactorEnabled) - { - user.TwoFactorRecoveryCode = Guid.NewGuid().ToString("N"); - await _userService.SaveUserAsync(user); - } - - var response = new TwoFactorResponseModel(user); - return response; - } - [HttpPut("keys")] [HttpPost("keys")] public async Task PutKeys([FromBody]KeysRequestModel model) diff --git a/src/Core/Identity/AuthenticatorTokenProvider.cs b/src/Core/Identity/AuthenticatorTokenProvider.cs index 066aec8999..537c761916 100644 --- a/src/Core/Identity/AuthenticatorTokenProvider.cs +++ b/src/Core/Identity/AuthenticatorTokenProvider.cs @@ -11,10 +11,12 @@ namespace Bit.Core.Identity { public Task CanGenerateTwoFactorTokenAsync(UserManager manager, User user) { - var canGenerate = user.TwoFactorEnabled + var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Authenticator); + + var canGenerate = user.TwoFactorProviderIsEnabled(TwoFactorProviderType.Authenticator) && user.TwoFactorProvider.HasValue && user.TwoFactorProvider.Value == TwoFactorProviderType.Authenticator - && !string.IsNullOrWhiteSpace(user.AuthenticatorKey); + && !string.IsNullOrWhiteSpace(provider.MetaData["Key"]); return Task.FromResult(canGenerate); } @@ -31,7 +33,8 @@ namespace Bit.Core.Identity public Task ValidateAsync(string purpose, string token, UserManager manager, User user) { - var otp = new Totp(Base32Encoding.ToBytes(user.AuthenticatorKey)); + var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Authenticator); + var otp = new Totp(Base32Encoding.ToBytes(provider.MetaData["Key"])); long timeStepMatched; var valid = otp.VerifyTotp(token, out timeStepMatched, new VerificationWindow(1, 1)); diff --git a/src/Core/Identity/UserStore.cs b/src/Core/Identity/UserStore.cs index fe76e59d59..154fe64222 100644 --- a/src/Core/Identity/UserStore.cs +++ b/src/Core/Identity/UserStore.cs @@ -168,7 +168,7 @@ namespace Bit.Core.Identity public Task GetTwoFactorEnabledAsync(User user, CancellationToken cancellationToken) { - return Task.FromResult(user.TwoFactorEnabled && user.TwoFactorProvider.HasValue); + return Task.FromResult(user.TwoFactorIsEnabled()); } public Task SetSecurityStampAsync(User user, string stamp, CancellationToken cancellationToken) diff --git a/src/Core/Models/Api/Response/ProfileResponseModel.cs b/src/Core/Models/Api/Response/ProfileResponseModel.cs index 340ce7ecb0..2a12c23fbb 100644 --- a/src/Core/Models/Api/Response/ProfileResponseModel.cs +++ b/src/Core/Models/Api/Response/ProfileResponseModel.cs @@ -22,7 +22,7 @@ namespace Bit.Core.Models.Api Email = user.Email; MasterPasswordHint = string.IsNullOrWhiteSpace(user.MasterPasswordHint) ? null : user.MasterPasswordHint; Culture = user.Culture; - TwoFactorEnabled = user.TwoFactorEnabled; + TwoFactorEnabled = user.TwoFactorIsEnabled(); Key = user.Key; PrivateKey = user.PrivateKey; SecurityStamp = user.SecurityStamp; diff --git a/src/Core/Models/Api/Response/TwoFactorResponseModel.cs b/src/Core/Models/Api/Response/TwoFactorResponseModel.cs index 2c9b0ad385..698e98e8eb 100644 --- a/src/Core/Models/Api/Response/TwoFactorResponseModel.cs +++ b/src/Core/Models/Api/Response/TwoFactorResponseModel.cs @@ -14,8 +14,25 @@ namespace Bit.Core.Models.Api throw new ArgumentNullException(nameof(user)); } - TwoFactorEnabled = user.TwoFactorEnabled; - AuthenticatorKey = user.AuthenticatorKey; + var providers = user.GetTwoFactorProviders(); + if(user.TwoFactorProvider.HasValue && providers.ContainsKey(user.TwoFactorProvider.Value)) + { + var provider = providers[user.TwoFactorProvider.Value]; + switch(user.TwoFactorProvider.Value) + { + case TwoFactorProviderType.Authenticator: + AuthenticatorKey = provider.MetaData["Key"]; + break; + default: + break; + } + } + else + { + TwoFactorEnabled = false; + } + + TwoFactorEnabled = user.TwoFactorIsEnabled(); TwoFactorProvider = user.TwoFactorProvider; TwoFactorRecoveryCode = user.TwoFactorRecoveryCode; } diff --git a/src/Core/Models/Table/User.cs b/src/Core/Models/Table/User.cs index 9e2d751102..b41e6d69d9 100644 --- a/src/Core/Models/Table/User.cs +++ b/src/Core/Models/Table/User.cs @@ -1,11 +1,15 @@ using System; using Bit.Core.Enums; using Bit.Core.Utilities; +using System.Collections.Generic; +using Newtonsoft.Json; namespace Bit.Core.Models.Table { public class User : IDataObject { + private Dictionary _twoFactorProviders; + public Guid Id { get; set; } public string Name { get; set; } public string Email { get; set; } @@ -32,5 +36,69 @@ namespace Bit.Core.Models.Table { Id = CoreHelpers.GenerateComb(); } + + public Dictionary GetTwoFactorProviders() + { + if(string.IsNullOrWhiteSpace(TwoFactorProviders)) + { + return null; + } + + try + { + if(_twoFactorProviders == null) + { + _twoFactorProviders = + JsonConvert.DeserializeObject>(TwoFactorProviders); + } + + return _twoFactorProviders; + } + catch(JsonSerializationException) + { + return null; + } + } + + public void SetTwoFactorProviders(Dictionary providers) + { + TwoFactorProviders = JsonConvert.SerializeObject(providers, new JsonSerializerSettings + { + ContractResolver = new EnumKeyResolver() + }); + _twoFactorProviders = providers; + } + + public bool TwoFactorProviderIsEnabled(TwoFactorProviderType provider) + { + var providers = GetTwoFactorProviders(); + if(providers == null || !providers.ContainsKey(provider)) + { + return false; + } + + return providers[provider].Enabled; + } + + public bool TwoFactorIsEnabled(TwoFactorProviderType provider) + { + return TwoFactorEnabled && TwoFactorProviderIsEnabled(provider); + } + + public bool TwoFactorIsEnabled() + { + return TwoFactorEnabled && TwoFactorProvider.HasValue && TwoFactorProviderIsEnabled(TwoFactorProvider.Value); + } + + public TwoFactorProvider GetTwoFactorProvider(TwoFactorProviderType provider) + { + var providers = GetTwoFactorProviders(); + if(providers == null || !providers.ContainsKey(provider)) + { + return null; + } + + return providers[provider]; + } } } diff --git a/src/Core/Models/TwoFactorProvider.cs b/src/Core/Models/TwoFactorProvider.cs index 58cbc13c07..638164b613 100644 --- a/src/Core/Models/TwoFactorProvider.cs +++ b/src/Core/Models/TwoFactorProvider.cs @@ -7,6 +7,6 @@ namespace Bit.Core.Models { public bool Enabled { get; set; } public bool Remember { get; set; } - public Dictionary MetaData { get; set; } + public Dictionary MetaData { get; set; } = new Dictionary(); } } diff --git a/src/Core/Services/IUserService.cs b/src/Core/Services/IUserService.cs index 794e97e509..835e5b2b9e 100644 --- a/src/Core/Services/IUserService.cs +++ b/src/Core/Services/IUserService.cs @@ -4,6 +4,8 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; using Bit.Core.Models.Table; using System.Security.Claims; +using Bit.Core.Enums; +using Bit.Core.Models; namespace Bit.Core.Services { @@ -24,7 +26,8 @@ namespace Bit.Core.Services Task UpdateKeyAsync(User user, string masterPassword, string key, string privateKey, IEnumerable ciphers, IEnumerable folders); Task RefreshSecurityStampAsync(User user, string masterPasswordHash); - Task GetTwoFactorAsync(User user, Enums.TwoFactorProviderType provider); + Task SetupTwoFactorAsync(User user, TwoFactorProviderType provider); + Task UpdateTwoFactorProviderAsync(User user, TwoFactorProviderType type); Task RecoverTwoFactorAsync(string email, string masterPassword, string recoveryCode); Task GenerateUserTokenAsync(User user, string tokenProvider, string purpose); Task DeleteAsync(User user); diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index 9420dcb44c..7b2ca069cb 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -11,6 +11,7 @@ using Microsoft.AspNetCore.Builder; using Bit.Core.Enums; using OtpNet; using System.Security.Claims; +using Bit.Core.Models; namespace Bit.Core.Services { @@ -315,14 +316,16 @@ namespace Bit.Core.Services return IdentityResult.Failed(_identityErrorDescriber.PasswordMismatch()); } - public async Task GetTwoFactorAsync(User user, TwoFactorProviderType provider) + public async Task SetupTwoFactorAsync(User user, TwoFactorProviderType provider) { - if(user.TwoFactorEnabled && user.TwoFactorProvider.HasValue && user.TwoFactorProvider.Value == provider) + var providers = user.GetTwoFactorProviders(); + if(providers != null && providers.ContainsKey(provider) && providers[provider].Enabled && + user.TwoFactorProvider.HasValue && user.TwoFactorProvider.Value == provider) { switch(provider) { case TwoFactorProviderType.Authenticator: - if(!string.IsNullOrWhiteSpace(user.AuthenticatorKey)) + if(!string.IsNullOrWhiteSpace(providers[provider].MetaData["Key"])) { return; } @@ -332,20 +335,51 @@ namespace Bit.Core.Services } } - user.TwoFactorProvider = provider; - // Reset authenticator key. - user.AuthenticatorKey = null; + if(providers == null) + { + providers = new Dictionary(); + } + + TwoFactorProvider providerInfo = null; + if(!providers.ContainsKey(provider)) + { + providerInfo = new TwoFactorProvider(); + providers.Add(provider, providerInfo); + } + else + { + providerInfo = providers[provider]; + } switch(provider) { case TwoFactorProviderType.Authenticator: var key = KeyGeneration.GenerateRandomKey(20); - user.AuthenticatorKey = Base32Encoding.ToString(key); + providerInfo.MetaData["Key"] = Base32Encoding.ToString(key); + providerInfo.Remember = true; break; default: throw new ArgumentException(nameof(provider)); } + user.TwoFactorProvider = provider; + user.SetTwoFactorProviders(providers); + await SaveUserAsync(user); + } + + public async Task UpdateTwoFactorProviderAsync(User user, TwoFactorProviderType type) + { + var providers = user.GetTwoFactorProviders(); + if(!providers?.ContainsKey(type) ?? true) + { + return; + } + + providers[type].Enabled = user.TwoFactorEnabled; + user.SetTwoFactorProviders(providers); + + user.TwoFactorProvider = type; + user.TwoFactorRecoveryCode = user.TwoFactorIsEnabled() ? Guid.NewGuid().ToString("N") : null; await SaveUserAsync(user); } @@ -368,7 +402,6 @@ namespace Bit.Core.Services return false; } - user.TwoFactorProvider = TwoFactorProviderType.Authenticator; user.TwoFactorEnabled = false; user.TwoFactorRecoveryCode = null; await SaveUserAsync(user); diff --git a/src/Core/Utilities/EnumKeyResolver.cs b/src/Core/Utilities/EnumKeyResolver.cs new file mode 100644 index 0000000000..1f5671b2a8 --- /dev/null +++ b/src/Core/Utilities/EnumKeyResolver.cs @@ -0,0 +1,21 @@ +using Newtonsoft.Json.Serialization; +using System; + +namespace Bit.Core.Utilities +{ + public class EnumKeyResolver : DefaultContractResolver where T : struct + { + protected override JsonDictionaryContract CreateDictionaryContract(Type objectType) + { + var contract = base.CreateDictionaryContract(objectType); + var keyType = contract.DictionaryKeyType; + + if(keyType.BaseType == typeof(Enum)) + { + contract.DictionaryKeyResolver = propName => ((T)Enum.Parse(keyType, propName)).ToString(); + } + + return contract; + } + } +}