1
0
mirror of https://github.com/bitwarden/server.git synced 2025-04-18 03:28:15 -05:00

support for user defined kdf parameters

This commit is contained in:
Kyle Spearrin 2018-08-14 15:30:04 -04:00
parent 20f45ca2de
commit 0932189ccb
18 changed files with 470 additions and 3 deletions

View File

@ -20,6 +20,7 @@ namespace Bit.Api.Controllers
public class AccountsController : Controller public class AccountsController : Controller
{ {
private readonly IUserService _userService; private readonly IUserService _userService;
private readonly IUserRepository _userRepository;
private readonly ICipherService _cipherService; private readonly ICipherService _cipherService;
private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly ILicensingService _licenseService; private readonly ILicensingService _licenseService;
@ -27,18 +28,32 @@ namespace Bit.Api.Controllers
public AccountsController( public AccountsController(
IUserService userService, IUserService userService,
IUserRepository userRepository,
ICipherService cipherService, ICipherService cipherService,
IOrganizationUserRepository organizationUserRepository, IOrganizationUserRepository organizationUserRepository,
ILicensingService licenseService, ILicensingService licenseService,
GlobalSettings globalSettings) GlobalSettings globalSettings)
{ {
_userService = userService; _userService = userService;
_userRepository = userRepository;
_cipherService = cipherService; _cipherService = cipherService;
_organizationUserRepository = organizationUserRepository; _organizationUserRepository = organizationUserRepository;
_licenseService = licenseService; _licenseService = licenseService;
_globalSettings = globalSettings; _globalSettings = globalSettings;
} }
[HttpPost("prelogin")]
[AllowAnonymous]
public async Task<PreloginResponseModel> PostPrelogin([FromBody]PreloginRequestModel model)
{
var kdfInformation = await _userRepository.GetKdfInformationByEmailAsync(model.Email);
if(kdfInformation == null)
{
throw new NotFoundException();
}
return new PreloginResponseModel(kdfInformation);
}
[HttpPost("register")] [HttpPost("register")]
[AllowAnonymous] [AllowAnonymous]
public async Task PostRegister([FromBody]RegisterRequestModel model) public async Task PostRegister([FromBody]RegisterRequestModel model)
@ -170,6 +185,31 @@ namespace Bit.Api.Controllers
throw new BadRequestException(ModelState); throw new BadRequestException(ModelState);
} }
[HttpPost("kdf")]
public async Task PostKdf([FromBody]KdfRequestModel model)
{
var user = await _userService.GetUserByPrincipalAsync(User);
if(user == null)
{
throw new UnauthorizedAccessException();
}
var result = await _userService.ChangeKdfAsync(user, model.MasterPasswordHash,
model.NewMasterPasswordHash, model.Key, model.Kdf.Value, model.KdfIterations.Value);
if(result.Succeeded)
{
return;
}
foreach(var error in result.Errors)
{
ModelState.AddModelError(string.Empty, error.Description);
}
await Task.Delay(2000);
throw new BadRequestException(ModelState);
}
[HttpPost("key")] [HttpPost("key")]
public async Task PostKey([FromBody]UpdateKeyRequestModel model) public async Task PostKey([FromBody]UpdateKeyRequestModel model)
{ {

View File

@ -0,0 +1,7 @@
namespace Bit.Core.Enums
{
public enum KdfType : byte
{
PBKDF2 = 0
}
}

View File

@ -0,0 +1,33 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Bit.Core.Enums;
namespace Bit.Core.Models.Api
{
public class KdfRequestModel : PasswordRequestModel, IValidatableObject
{
[Required]
public KdfType? Kdf { get; set; }
[Required]
public int? KdfIterations { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if(Kdf.HasValue && KdfIterations.HasValue)
{
switch(Kdf.Value)
{
case KdfType.PBKDF2:
if(KdfIterations.Value < 5000 || KdfIterations.Value > 1_000_000)
{
yield return new ValidationResult("KDF iterations must be between 5000 and 1000000.");
}
break;
default:
break;
}
}
}
}
}

View File

@ -0,0 +1,13 @@
using System;
using System.ComponentModel.DataAnnotations;
namespace Bit.Core.Models.Api
{
public class PreloginRequestModel
{
[Required]
[EmailAddress]
[StringLength(50)]
public string Email { get; set; }
}
}

View File

@ -1,10 +1,12 @@
using System; using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Bit.Core.Enums;
using Bit.Core.Models.Table; using Bit.Core.Models.Table;
namespace Bit.Core.Models.Api namespace Bit.Core.Models.Api
{ {
public class RegisterRequestModel public class RegisterRequestModel : IValidatableObject
{ {
[StringLength(50)] [StringLength(50)]
public string Name { get; set; } public string Name { get; set; }
@ -21,6 +23,8 @@ namespace Bit.Core.Models.Api
public KeysRequestModel Keys { get; set; } public KeysRequestModel Keys { get; set; }
public string Token { get; set; } public string Token { get; set; }
public Guid? OrganizationUserId { get; set; } public Guid? OrganizationUserId { get; set; }
public KdfType? Kdf { get; set; }
public int? KdfIterations { get; set; }
public User ToUser() public User ToUser()
{ {
@ -28,7 +32,9 @@ namespace Bit.Core.Models.Api
{ {
Name = Name, Name = Name,
Email = Email, Email = Email,
MasterPasswordHint = MasterPasswordHint MasterPasswordHint = MasterPasswordHint,
Kdf = Kdf.GetValueOrDefault(KdfType.PBKDF2),
KdfIterations = KdfIterations.GetValueOrDefault(5000)
}; };
if(Key != null) if(Key != null)
@ -43,5 +49,23 @@ namespace Bit.Core.Models.Api
return user; return user;
} }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if(Kdf.HasValue && KdfIterations.HasValue)
{
switch(Kdf.Value)
{
case KdfType.PBKDF2:
if(KdfIterations.Value < 5000 || KdfIterations.Value > 1_000_000)
{
yield return new ValidationResult("KDF iterations must be between 5000 and 1000000.");
}
break;
default:
break;
}
}
}
} }
} }

View File

@ -0,0 +1,17 @@
using Bit.Core.Enums;
using Bit.Core.Models.Data;
namespace Bit.Core.Models.Api
{
public class PreloginResponseModel
{
public PreloginResponseModel(UserKdfInformation kdfInformation)
{
Kdf = kdfInformation.Kdf;
KdfIterations = kdfInformation.KdfIterations;
}
public KdfType Kdf { get; set; }
public int KdfIterations { get; set; }
}
}

View File

@ -0,0 +1,11 @@
using System;
using Bit.Core.Enums;
namespace Bit.Core.Models.Data
{
public class UserKdfInformation
{
public KdfType Kdf { get; set; }
public int KdfIterations { get; set; }
}
}

View File

@ -39,6 +39,8 @@ namespace Bit.Core.Models.Table
public string GatewayCustomerId { get; set; } public string GatewayCustomerId { get; set; }
public string GatewaySubscriptionId { get; set; } public string GatewaySubscriptionId { get; set; }
public string LicenseKey { get; set; } public string LicenseKey { get; set; }
public KdfType Kdf { get; set; } = KdfType.PBKDF2;
public int KdfIterations { get; set; } = 5000;
public DateTime CreationDate { get; internal set; } = DateTime.UtcNow; public DateTime CreationDate { get; internal set; } = DateTime.UtcNow;
public DateTime RevisionDate { get; internal set; } = DateTime.UtcNow; public DateTime RevisionDate { get; internal set; } = DateTime.UtcNow;

View File

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Bit.Core.Models.Data;
using Bit.Core.Models.Table; using Bit.Core.Models.Table;
namespace Bit.Core.Repositories namespace Bit.Core.Repositories
@ -8,6 +9,7 @@ namespace Bit.Core.Repositories
public interface IUserRepository : IRepository<User, Guid> public interface IUserRepository : IRepository<User, Guid>
{ {
Task<User> GetByEmailAsync(string email); Task<User> GetByEmailAsync(string email);
Task<UserKdfInformation> GetKdfInformationByEmailAsync(string email);
Task<ICollection<User>> SearchAsync(string email, int skip, int take); Task<ICollection<User>> SearchAsync(string email, int skip, int take);
Task<ICollection<User>> GetManyByPremiumAsync(bool premium); Task<ICollection<User>> GetManyByPremiumAsync(bool premium);
Task<ICollection<User>> GetManyByPremiumRenewalAsync(); Task<ICollection<User>> GetManyByPremiumRenewalAsync();

View File

@ -4,6 +4,7 @@ using System.Data;
using System.Data.SqlClient; using System.Data.SqlClient;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Bit.Core.Models.Data;
using Bit.Core.Models.Table; using Bit.Core.Models.Table;
using Dapper; using Dapper;
@ -37,6 +38,19 @@ namespace Bit.Core.Repositories.SqlServer
} }
} }
public async Task<UserKdfInformation> GetKdfInformationByEmailAsync(string email)
{
using(var connection = new SqlConnection(ConnectionString))
{
var results = await connection.QueryAsync<UserKdfInformation>(
$"[{Schema}].[{Table}_ReadKdfByEmail]",
new { Email = email },
commandType: CommandType.StoredProcedure);
return results.SingleOrDefault();
}
}
public async Task<ICollection<User>> SearchAsync(string email, int skip, int take) public async Task<ICollection<User>> SearchAsync(string email, int skip, int take)
{ {
using(var connection = new SqlConnection(ConnectionString)) using(var connection = new SqlConnection(ConnectionString))

View File

@ -30,6 +30,8 @@ namespace Bit.Core.Services
Task<IdentityResult> ChangeEmailAsync(User user, string masterPassword, string newEmail, string newMasterPassword, Task<IdentityResult> ChangeEmailAsync(User user, string masterPassword, string newEmail, string newMasterPassword,
string token, string key); string token, string key);
Task<IdentityResult> ChangePasswordAsync(User user, string masterPassword, string newMasterPassword, string key); Task<IdentityResult> ChangePasswordAsync(User user, string masterPassword, string newMasterPassword, string key);
Task<IdentityResult> ChangeKdfAsync(User user, string masterPassword, string newMasterPassword, string key,
KdfType kdf, int kdfIterations);
Task<IdentityResult> UpdateKeyAsync(User user, string masterPassword, string key, string privateKey, Task<IdentityResult> UpdateKeyAsync(User user, string masterPassword, string key, string privateKey,
IEnumerable<Cipher> ciphers, IEnumerable<Folder> folders); IEnumerable<Cipher> ciphers, IEnumerable<Folder> folders);
Task<IdentityResult> RefreshSecurityStampAsync(User user, string masterPasswordHash); Task<IdentityResult> RefreshSecurityStampAsync(User user, string masterPasswordHash);

View File

@ -446,6 +446,34 @@ namespace Bit.Core.Services
return IdentityResult.Failed(_identityErrorDescriber.PasswordMismatch()); return IdentityResult.Failed(_identityErrorDescriber.PasswordMismatch());
} }
public async Task<IdentityResult> ChangeKdfAsync(User user, string masterPassword, string newMasterPassword,
string key, KdfType kdf, int kdfIterations)
{
if(user == null)
{
throw new ArgumentNullException(nameof(user));
}
if(await CheckPasswordAsync(user, masterPassword))
{
var result = await UpdatePasswordHash(user, newMasterPassword);
if(!result.Succeeded)
{
return result;
}
user.RevisionDate = user.AccountRevisionDate = DateTime.UtcNow;
user.Key = key;
user.Kdf = kdf;
user.KdfIterations = kdfIterations;
await _userRepository.ReplaceAsync(user);
return IdentityResult.Success;
}
Logger.LogWarning("Change KDF failed for user {userId}.", user.Id);
return IdentityResult.Failed(_identityErrorDescriber.PasswordMismatch());
}
public async Task<IdentityResult> UpdateKeyAsync(User user, string masterPassword, string key, string privateKey, public async Task<IdentityResult> UpdateKeyAsync(User user, string masterPassword, string key, string privateKey,
IEnumerable<Cipher> ciphers, IEnumerable<Folder> folders) IEnumerable<Cipher> ciphers, IEnumerable<Folder> folders)
{ {
@ -477,7 +505,7 @@ namespace Bit.Core.Services
return IdentityResult.Success; return IdentityResult.Success;
} }
Logger.LogWarning("Update key for user {userId}.", user.Id); Logger.LogWarning("Update key failed for user {userId}.", user.Id);
return IdentityResult.Failed(_identityErrorDescriber.PasswordMismatch()); return IdentityResult.Failed(_identityErrorDescriber.PasswordMismatch());
} }

View File

@ -230,5 +230,6 @@
<Build Include="dbo\Stored Procedures\User_UpdateRenewalReminderDate.sql" /> <Build Include="dbo\Stored Procedures\User_UpdateRenewalReminderDate.sql" />
<Build Include="dbo\Stored Procedures\Grant_DeleteExpired.sql" /> <Build Include="dbo\Stored Procedures\Grant_DeleteExpired.sql" />
<Build Include="dbo\Stored Procedures\U2f_DeleteOld.sql" /> <Build Include="dbo\Stored Procedures\U2f_DeleteOld.sql" />
<Build Include="dbo\Stored Procedures\User_ReadKdfByEmail.sql" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -24,6 +24,8 @@
@GatewayCustomerId VARCHAR(50), @GatewayCustomerId VARCHAR(50),
@GatewaySubscriptionId VARCHAR(50), @GatewaySubscriptionId VARCHAR(50),
@LicenseKey VARCHAR(100), @LicenseKey VARCHAR(100),
@Kdf TINYINT,
@KdfIterations INT,
@CreationDate DATETIME2(7), @CreationDate DATETIME2(7),
@RevisionDate DATETIME2(7) @RevisionDate DATETIME2(7)
AS AS
@ -57,6 +59,8 @@ BEGIN
[GatewayCustomerId], [GatewayCustomerId],
[GatewaySubscriptionId], [GatewaySubscriptionId],
[LicenseKey], [LicenseKey],
[Kdf],
[KdfIterations],
[CreationDate], [CreationDate],
[RevisionDate] [RevisionDate]
) )
@ -87,6 +91,8 @@ BEGIN
@GatewayCustomerId, @GatewayCustomerId,
@GatewaySubscriptionId, @GatewaySubscriptionId,
@LicenseKey, @LicenseKey,
@Kdf,
@KdfIterations,
@CreationDate, @CreationDate,
@RevisionDate @RevisionDate
) )

View File

@ -0,0 +1,14 @@
CREATE PROCEDURE [dbo].[User_ReadKdfByEmail]
@Email NVARCHAR(50)
AS
BEGIN
SET NOCOUNT ON
SELECT
[Kdf],
[KdfIterations]
FROM
[dbo].[User]
WHERE
[Email] = @Email
END

View File

@ -24,6 +24,8 @@
@GatewayCustomerId VARCHAR(50), @GatewayCustomerId VARCHAR(50),
@GatewaySubscriptionId VARCHAR(50), @GatewaySubscriptionId VARCHAR(50),
@LicenseKey VARCHAR(100), @LicenseKey VARCHAR(100),
@Kdf TINYINT,
@KdfIterations INT,
@CreationDate DATETIME2(7), @CreationDate DATETIME2(7),
@RevisionDate DATETIME2(7) @RevisionDate DATETIME2(7)
AS AS
@ -57,6 +59,8 @@ BEGIN
[GatewayCustomerId] = @GatewayCustomerId, [GatewayCustomerId] = @GatewayCustomerId,
[GatewaySubscriptionId] = @GatewaySubscriptionId, [GatewaySubscriptionId] = @GatewaySubscriptionId,
[LicenseKey] = @LicenseKey, [LicenseKey] = @LicenseKey,
[Kdf] = @Kdf,
[KdfIterations] = @KdfIterations,
[CreationDate] = @CreationDate, [CreationDate] = @CreationDate,
[RevisionDate] = @RevisionDate [RevisionDate] = @RevisionDate
WHERE WHERE

View File

@ -24,6 +24,8 @@
[GatewayCustomerId] VARCHAR (50) NULL, [GatewayCustomerId] VARCHAR (50) NULL,
[GatewaySubscriptionId] VARCHAR (50) NULL, [GatewaySubscriptionId] VARCHAR (50) NULL,
[LicenseKey] VARCHAR (100) NULL, [LicenseKey] VARCHAR (100) NULL,
[Kdf] TINYINT NOT NULL,
[KdfIterations] INT NOT NULL,
[CreationDate] DATETIME2 (7) NOT NULL, [CreationDate] DATETIME2 (7) NOT NULL,
[RevisionDate] DATETIME2 (7) NOT NULL, [RevisionDate] DATETIME2 (7) NOT NULL,
CONSTRAINT [PK_User] PRIMARY KEY CLUSTERED ([Id] ASC) CONSTRAINT [PK_User] PRIMARY KEY CLUSTERED ([Id] ASC)

View File

@ -0,0 +1,247 @@
IF COL_LENGTH('[dbo].[User]', 'Kdf') IS NULL
BEGIN
ALTER TABLE
[dbo].[User]
ADD
[Kdf] TINYINT NULL,
[KdfIterations] INT NULL
END
GO
UPDATE
[dbo].[User]
SET
[Kdf] = 0,
[KdfIterations] = 5000
GO
ALTER TABLE
[dbo].[User]
ALTER COLUMN
[Kdf] TINYINT NOT NULL
GO
ALTER TABLE
[dbo].[User]
ALTER COLUMN
[KdfIterations] INT NOT NULL
GO
IF EXISTS(SELECT * FROM sys.views WHERE [Name] = 'UserView')
BEGIN
DROP VIEW [dbo].[UserView]
END
GO
CREATE VIEW [dbo].[UserView]
AS
SELECT
*
FROM
[dbo].[User]
GO
IF OBJECT_ID('[dbo].[User_Create]') IS NOT NULL
BEGIN
DROP PROCEDURE [dbo].[User_Create]
END
GO
CREATE PROCEDURE [dbo].[User_Create]
@Id UNIQUEIDENTIFIER,
@Name NVARCHAR(50),
@Email NVARCHAR(50),
@EmailVerified BIT,
@MasterPassword NVARCHAR(300),
@MasterPasswordHint NVARCHAR(50),
@Culture NVARCHAR(10),
@SecurityStamp NVARCHAR(50),
@TwoFactorProviders NVARCHAR(MAX),
@TwoFactorRecoveryCode NVARCHAR(32),
@EquivalentDomains NVARCHAR(MAX),
@ExcludedGlobalEquivalentDomains NVARCHAR(MAX),
@AccountRevisionDate DATETIME2(7),
@Key NVARCHAR(MAX),
@PublicKey NVARCHAR(MAX),
@PrivateKey NVARCHAR(MAX),
@Premium BIT,
@PremiumExpirationDate DATETIME2(7),
@RenewalReminderDate DATETIME2(7),
@Storage BIGINT,
@MaxStorageGb SMALLINT,
@Gateway TINYINT,
@GatewayCustomerId VARCHAR(50),
@GatewaySubscriptionId VARCHAR(50),
@LicenseKey VARCHAR(100),
@Kdf TINYINT,
@KdfIterations INT,
@CreationDate DATETIME2(7),
@RevisionDate DATETIME2(7)
AS
BEGIN
SET NOCOUNT ON
INSERT INTO [dbo].[User]
(
[Id],
[Name],
[Email],
[EmailVerified],
[MasterPassword],
[MasterPasswordHint],
[Culture],
[SecurityStamp],
[TwoFactorProviders],
[TwoFactorRecoveryCode],
[EquivalentDomains],
[ExcludedGlobalEquivalentDomains],
[AccountRevisionDate],
[Key],
[PublicKey],
[PrivateKey],
[Premium],
[PremiumExpirationDate],
[RenewalReminderDate],
[Storage],
[MaxStorageGb],
[Gateway],
[GatewayCustomerId],
[GatewaySubscriptionId],
[LicenseKey],
[Kdf],
[KdfIterations],
[CreationDate],
[RevisionDate]
)
VALUES
(
@Id,
@Name,
@Email,
@EmailVerified,
@MasterPassword,
@MasterPasswordHint,
@Culture,
@SecurityStamp,
@TwoFactorProviders,
@TwoFactorRecoveryCode,
@EquivalentDomains,
@ExcludedGlobalEquivalentDomains,
@AccountRevisionDate,
@Key,
@PublicKey,
@PrivateKey,
@Premium,
@PremiumExpirationDate,
@RenewalReminderDate,
@Storage,
@MaxStorageGb,
@Gateway,
@GatewayCustomerId,
@GatewaySubscriptionId,
@LicenseKey,
@Kdf,
@KdfIterations,
@CreationDate,
@RevisionDate
)
END
GO
IF OBJECT_ID('[dbo].[User_Update]') IS NOT NULL
BEGIN
DROP PROCEDURE [dbo].[User_Update]
END
GO
CREATE PROCEDURE [dbo].[User_Update]
@Id UNIQUEIDENTIFIER,
@Name NVARCHAR(50),
@Email NVARCHAR(50),
@EmailVerified BIT,
@MasterPassword NVARCHAR(300),
@MasterPasswordHint NVARCHAR(50),
@Culture NVARCHAR(10),
@SecurityStamp NVARCHAR(50),
@TwoFactorProviders NVARCHAR(MAX),
@TwoFactorRecoveryCode NVARCHAR(32),
@EquivalentDomains NVARCHAR(MAX),
@ExcludedGlobalEquivalentDomains NVARCHAR(MAX),
@AccountRevisionDate DATETIME2(7),
@Key NVARCHAR(MAX),
@PublicKey NVARCHAR(MAX),
@PrivateKey NVARCHAR(MAX),
@Premium BIT,
@PremiumExpirationDate DATETIME2(7),
@RenewalReminderDate DATETIME2(7),
@Storage BIGINT,
@MaxStorageGb SMALLINT,
@Gateway TINYINT,
@GatewayCustomerId VARCHAR(50),
@GatewaySubscriptionId VARCHAR(50),
@LicenseKey VARCHAR(100),
@Kdf TINYINT,
@KdfIterations INT,
@CreationDate DATETIME2(7),
@RevisionDate DATETIME2(7)
AS
BEGIN
SET NOCOUNT ON
UPDATE
[dbo].[User]
SET
[Name] = @Name,
[Email] = @Email,
[EmailVerified] = @EmailVerified,
[MasterPassword] = @MasterPassword,
[MasterPasswordHint] = @MasterPasswordHint,
[Culture] = @Culture,
[SecurityStamp] = @SecurityStamp,
[TwoFactorProviders] = @TwoFactorProviders,
[TwoFactorRecoveryCode] = @TwoFactorRecoveryCode,
[EquivalentDomains] = @EquivalentDomains,
[ExcludedGlobalEquivalentDomains] = @ExcludedGlobalEquivalentDomains,
[AccountRevisionDate] = @AccountRevisionDate,
[Key] = @Key,
[PublicKey] = @PublicKey,
[PrivateKey] = @PrivateKey,
[Premium] = @Premium,
[PremiumExpirationDate] = @PremiumExpirationDate,
[RenewalReminderDate] = @RenewalReminderDate,
[Storage] = @Storage,
[MaxStorageGb] = @MaxStorageGb,
[Gateway] = @Gateway,
[GatewayCustomerId] = @GatewayCustomerId,
[GatewaySubscriptionId] = @GatewaySubscriptionId,
[LicenseKey] = @LicenseKey,
[Kdf] = @Kdf,
[KdfIterations] = @KdfIterations,
[CreationDate] = @CreationDate,
[RevisionDate] = @RevisionDate
WHERE
[Id] = @Id
END
GO
IF OBJECT_ID('[dbo].[User_ReadKdfByEmail]') IS NOT NULL
BEGIN
DROP PROCEDURE [dbo].[User_ReadKdfByEmail]
END
GO
CREATE PROCEDURE [dbo].[User_ReadKdfByEmail]
@Email NVARCHAR(50)
AS
BEGIN
SET NOCOUNT ON
SELECT
[Kdf],
[KdfIterations]
FROM
[dbo].[User]
WHERE
[Email] = @Email
END
GO