mirror of
https://github.com/bitwarden/server
synced 2026-02-11 14:03:24 +00:00
[PM-31684] Remove email hashing for send access (#6945)
* [PM-31684] Remove email hashing for send access * [PM-31684] switching the order of migration files * [PM-31684] adding more migrations * [PM-31684] Removing anon access emails field and reusing emails field * [PM-31684] cleanup before adding migrations back * [PM-31684] restore original snapshots * [PM-31684] restore original postgres snapshots * [PM-31684] adding migrations * [PM-31684] removing encryption attributes from emails request model * [PM-31684] adding missing stored proc alters * [PM-31684] Improved formatting for stored proc defs * [PM-31684] adding necessary comment back * [PM-31684] adding case-insensitive check on the server for send auth
This commit is contained in:
@@ -102,16 +102,8 @@ public class SendRequestModel
|
||||
/// Comma-separated list of emails that may access the send using OTP
|
||||
/// authentication. Mutually exclusive with <see cref="Password"/>.
|
||||
/// </summary>
|
||||
[EncryptedString]
|
||||
[EncryptedStringLength(4000)]
|
||||
public string Emails { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Comma-separated list of email **hashes** that may access the send using OTP
|
||||
/// authentication. Mutually exclusive with <see cref="Password"/>.
|
||||
/// </summary>
|
||||
[StringLength(4000)]
|
||||
public string EmailHashes { get; set; }
|
||||
public string Emails { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When <see langword="true"/>, send access is disabled.
|
||||
@@ -261,7 +253,6 @@ public class SendRequestModel
|
||||
// normalize encoding
|
||||
var emails = Emails.Split(',', RemoveEmptyEntries | TrimEntries);
|
||||
existingSend.Emails = string.Join(",", emails);
|
||||
existingSend.EmailHashes = EmailHashes;
|
||||
existingSend.Password = null;
|
||||
existingSend.AuthType = Core.Tools.Enums.AuthType.Email;
|
||||
}
|
||||
|
||||
@@ -81,15 +81,6 @@ public class Send : ITableObject<Guid>
|
||||
[MaxLength(4000)]
|
||||
public string? Emails { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Comma-separated list of email **hashes** for OTP authentication.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This field is mutually exclusive with <see cref="Password" />
|
||||
/// </remarks>
|
||||
[MaxLength(4000)]
|
||||
public string? EmailHashes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The send becomes unavailable to API callers when
|
||||
/// <see cref="AccessCount"/> >= <see cref="MaxAccessCount"/>.
|
||||
|
||||
@@ -44,7 +44,7 @@ public record ResourcePassword(string Hash) : SendAuthenticationMethod;
|
||||
/// <summary>
|
||||
/// Create a send claim by requesting a one time password (OTP) confirmation code.
|
||||
/// </summary>
|
||||
/// <param name="EmailHashes">
|
||||
/// The list of email address **hashes** permitted access to the send.
|
||||
/// <param name="emails">
|
||||
/// The list of email addresses permitted access to the send.
|
||||
/// </param>
|
||||
public record EmailOtp(string[] EmailHashes) : SendAuthenticationMethod;
|
||||
public record EmailOtp(string[] emails) : SendAuthenticationMethod;
|
||||
|
||||
@@ -41,7 +41,7 @@ public class SendAuthenticationQuery : ISendAuthenticationQuery
|
||||
var s when s.AccessCount >= s.MaxAccessCount.GetValueOrDefault(int.MaxValue) => NEVER_AUTHENTICATE,
|
||||
var s when s.ExpirationDate.GetValueOrDefault(DateTime.MaxValue) < DateTime.UtcNow => NEVER_AUTHENTICATE,
|
||||
var s when s.DeletionDate <= DateTime.UtcNow => NEVER_AUTHENTICATE,
|
||||
var s when s.AuthType == AuthType.Email && s.EmailHashes is not null => EmailOtp(s.EmailHashes),
|
||||
var s when s.AuthType == AuthType.Email && s.Emails is not null => EmailOtp(s.Emails),
|
||||
var s when s.AuthType == AuthType.Password && s.Password is not null => new ResourcePassword(s.Password),
|
||||
_ => NOT_AUTHENTICATED
|
||||
};
|
||||
@@ -49,13 +49,13 @@ public class SendAuthenticationQuery : ISendAuthenticationQuery
|
||||
return method;
|
||||
}
|
||||
|
||||
private static EmailOtp EmailOtp(string? emailHashes)
|
||||
private static EmailOtp EmailOtp(string? emails)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(emailHashes))
|
||||
if (string.IsNullOrWhiteSpace(emails))
|
||||
{
|
||||
return new EmailOtp([]);
|
||||
}
|
||||
var list = emailHashes.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
var list = emails.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
return new EmailOtp(list);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
using System.Security.Claims;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Bit.Core.Auth.Identity;
|
||||
using Bit.Core.Auth.Identity.TokenProviders;
|
||||
using Bit.Core.Services;
|
||||
@@ -39,17 +37,14 @@ public class SendEmailOtpRequestValidator(
|
||||
return BuildErrorResult(SendAccessConstants.EmailOtpValidatorResults.EmailRequired);
|
||||
}
|
||||
|
||||
// email hash must be in the list of email hashes in the EmailOtp array
|
||||
byte[] hashBytes = SHA256.HashData(Encoding.UTF8.GetBytes(email));
|
||||
string hashEmailHex = Convert.ToHexString(hashBytes).ToUpperInvariant();
|
||||
/*
|
||||
* This is somewhat contradictory to our process where a poor shape means invalid_request and invalid
|
||||
* data is invalid_grant.
|
||||
* In this case the shape is correct and the data is invalid but to protect against enumeration we treat incorrect emails
|
||||
* as invalid requests. The response for a request with a correct email which needs an OTP and a request
|
||||
* that has an invalid email need to be the same otherwise an attacker could enumerate until a valid email is found.
|
||||
*/
|
||||
if (!authMethod.EmailHashes.Contains(hashEmailHex))
|
||||
*/
|
||||
if (!authMethod.emails.Contains(email, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
return BuildErrorResult(SendAccessConstants.EmailOtpValidatorResults.EmailAndOtpRequired);
|
||||
}
|
||||
|
||||
@@ -154,7 +154,7 @@ public class SendRepository : Repository<Send, Guid>, ISendRepository
|
||||
}
|
||||
|
||||
// Capture original value
|
||||
var originalEmailHashes = send.EmailHashes;
|
||||
var emails = send.Emails;
|
||||
|
||||
// Protect value
|
||||
ProtectData(send);
|
||||
@@ -163,15 +163,15 @@ public class SendRepository : Repository<Send, Guid>, ISendRepository
|
||||
await saveTask();
|
||||
|
||||
// Restore original value
|
||||
send.EmailHashes = originalEmailHashes;
|
||||
send.Emails = emails;
|
||||
}
|
||||
|
||||
private void ProtectData(Send send)
|
||||
{
|
||||
if (!send.EmailHashes?.StartsWith(Constants.DatabaseFieldProtectedPrefix) ?? false)
|
||||
if (!send.Emails?.StartsWith(Constants.DatabaseFieldProtectedPrefix) ?? false)
|
||||
{
|
||||
send.EmailHashes = string.Concat(Constants.DatabaseFieldProtectedPrefix,
|
||||
_dataProtector.Protect(send.EmailHashes!));
|
||||
send.Emails = string.Concat(Constants.DatabaseFieldProtectedPrefix,
|
||||
_dataProtector.Protect(send.Emails!));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,10 +182,10 @@ public class SendRepository : Repository<Send, Guid>, ISendRepository
|
||||
return;
|
||||
}
|
||||
|
||||
if (send.EmailHashes?.StartsWith(Constants.DatabaseFieldProtectedPrefix) ?? false)
|
||||
if (send.Emails?.StartsWith(Constants.DatabaseFieldProtectedPrefix) ?? false)
|
||||
{
|
||||
send.EmailHashes = _dataProtector.Unprotect(
|
||||
send.EmailHashes.Substring(Constants.DatabaseFieldProtectedPrefix.Length));
|
||||
send.Emails = _dataProtector.Unprotect(
|
||||
send.Emails.Substring(Constants.DatabaseFieldProtectedPrefix.Length));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -150,7 +150,7 @@ public class DatabaseContext : DbContext
|
||||
var dataProtectionConverter = new DataProtectionConverter(dataProtector);
|
||||
eUser.Property(c => c.Key).HasConversion(dataProtectionConverter);
|
||||
eUser.Property(c => c.MasterPassword).HasConversion(dataProtectionConverter);
|
||||
eSend.Property(c => c.EmailHashes).HasConversion(dataProtectionConverter);
|
||||
eSend.Property(c => c.Emails).HasConversion(dataProtectionConverter);
|
||||
|
||||
if (Database.IsNpgsql())
|
||||
{
|
||||
|
||||
@@ -18,8 +18,7 @@
|
||||
-- FIXME: remove null default value once this argument has been
|
||||
-- in 2 server releases
|
||||
@Emails NVARCHAR(4000) = NULL,
|
||||
@AuthType TINYINT = NULL,
|
||||
@EmailHashes NVARCHAR(4000) = NULL
|
||||
@AuthType TINYINT = NULL
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
@@ -43,8 +42,7 @@ BEGIN
|
||||
[HideEmail],
|
||||
[CipherId],
|
||||
[Emails],
|
||||
[AuthType],
|
||||
[EmailHashes]
|
||||
[AuthType]
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
@@ -65,8 +63,7 @@ BEGIN
|
||||
@HideEmail,
|
||||
@CipherId,
|
||||
@Emails,
|
||||
@AuthType,
|
||||
@EmailHashes
|
||||
@AuthType
|
||||
)
|
||||
|
||||
IF @UserId IS NOT NULL
|
||||
|
||||
@@ -16,8 +16,7 @@
|
||||
@HideEmail BIT,
|
||||
@CipherId UNIQUEIDENTIFIER = NULL,
|
||||
@Emails NVARCHAR(4000) = NULL,
|
||||
@AuthType TINYINT = NULL,
|
||||
@EmailHashes NVARCHAR(4000) = NULL
|
||||
@AuthType TINYINT = NULL
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
@@ -41,8 +40,7 @@ BEGIN
|
||||
[HideEmail] = @HideEmail,
|
||||
[CipherId] = @CipherId,
|
||||
[Emails] = @Emails,
|
||||
[AuthType] = @AuthType,
|
||||
[EmailHashes] = @EmailHashes
|
||||
[AuthType] = @AuthType
|
||||
WHERE
|
||||
[Id] = @Id
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
[HideEmail] BIT NULL,
|
||||
[CipherId] UNIQUEIDENTIFIER NULL,
|
||||
[AuthType] TINYINT NULL,
|
||||
[EmailHashes] NVARCHAR(4000) NULL,
|
||||
CONSTRAINT [PK_Send] PRIMARY KEY CLUSTERED ([Id] ASC),
|
||||
CONSTRAINT [FK_Send_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id]),
|
||||
CONSTRAINT [FK_Send_User] FOREIGN KEY ([UserId]) REFERENCES [dbo].[User] ([Id]),
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace Bit.Test.Common.Helpers;
|
||||
|
||||
public class CryptographyHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns a hex-encoded, SHA256 hash for the given string
|
||||
/// </summary>
|
||||
public static string HashAndEncode(string text)
|
||||
{
|
||||
var hashBytes = SHA256.HashData(Encoding.UTF8.GetBytes(text));
|
||||
var hashEncoded = Convert.ToHexString(hashBytes).ToUpperInvariant();
|
||||
return hashEncoded;
|
||||
}
|
||||
}
|
||||
@@ -43,12 +43,12 @@ public class SendAuthenticationQueryTests
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(EmailHashesParsingTestCases))]
|
||||
public async Task GetAuthenticationMethod_WithEmailHashes_ParsesEmailHashesCorrectly(string emailHashString, string[] expectedEmailHashes)
|
||||
[MemberData(nameof(EmailsParsingTestCases))]
|
||||
public async Task GetAuthenticationMethod_WithEmails_ParsesEmailsCorrectly(string emailString, string[] expectedEmails)
|
||||
{
|
||||
// Arrange
|
||||
var sendId = Guid.NewGuid();
|
||||
var send = CreateSend(accessCount: 0, maxAccessCount: 10, emailHashes: emailHashString, password: null, AuthType.Email);
|
||||
var send = CreateSend(accessCount: 0, maxAccessCount: 10, emails: emailString, password: null, AuthType.Email);
|
||||
_sendRepository.GetByIdAsync(sendId).Returns(send);
|
||||
|
||||
// Act
|
||||
@@ -56,15 +56,15 @@ public class SendAuthenticationQueryTests
|
||||
|
||||
// Assert
|
||||
var emailOtp = Assert.IsType<EmailOtp>(result);
|
||||
Assert.Equal(expectedEmailHashes, emailOtp.EmailHashes);
|
||||
Assert.Equal(expectedEmails, emailOtp.emails);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetAuthenticationMethod_WithBothEmailHashesAndPassword_ReturnsEmailOtp()
|
||||
public async Task GetAuthenticationMethod_WithBothEmailsAndPassword_ReturnsEmailOtp()
|
||||
{
|
||||
// Arrange
|
||||
var sendId = Guid.NewGuid();
|
||||
var send = CreateSend(accessCount: 0, maxAccessCount: 10, emailHashes: "hashedemail", password: "hashedpassword", AuthType.Email);
|
||||
var send = CreateSend(accessCount: 0, maxAccessCount: 10, emails: "person@company.com", password: "hashedpassword", AuthType.Email);
|
||||
_sendRepository.GetByIdAsync(sendId).Returns(send);
|
||||
|
||||
// Act
|
||||
@@ -79,7 +79,7 @@ public class SendAuthenticationQueryTests
|
||||
{
|
||||
// Arrange
|
||||
var sendId = Guid.NewGuid();
|
||||
var send = CreateSend(accessCount: 0, maxAccessCount: 10, emailHashes: null, password: null, AuthType.None);
|
||||
var send = CreateSend(accessCount: 0, maxAccessCount: 10, emails: null, password: null, AuthType.None);
|
||||
_sendRepository.GetByIdAsync(sendId).Returns(send);
|
||||
|
||||
// Act
|
||||
@@ -106,11 +106,11 @@ public class SendAuthenticationQueryTests
|
||||
public static IEnumerable<object[]> AuthenticationMethodTestCases()
|
||||
{
|
||||
yield return new object[] { null, typeof(NeverAuthenticate) };
|
||||
yield return new object[] { CreateSend(accessCount: 5, maxAccessCount: 5, emailHashes: null, password: null, AuthType.None), typeof(NeverAuthenticate) };
|
||||
yield return new object[] { CreateSend(accessCount: 6, maxAccessCount: 5, emailHashes: null, password: null, AuthType.None), typeof(NeverAuthenticate) };
|
||||
yield return new object[] { CreateSend(accessCount: 0, maxAccessCount: 10, emailHashes: "hashedemail", password: null, AuthType.Email), typeof(EmailOtp) };
|
||||
yield return new object[] { CreateSend(accessCount: 0, maxAccessCount: 10, emailHashes: null, password: "hashedpassword", AuthType.Password), typeof(ResourcePassword) };
|
||||
yield return new object[] { CreateSend(accessCount: 0, maxAccessCount: 10, emailHashes: null, password: null, AuthType.None), typeof(NotAuthenticated) };
|
||||
yield return new object[] { CreateSend(accessCount: 5, maxAccessCount: 5, emails: null, password: null, AuthType.None), typeof(NeverAuthenticate) };
|
||||
yield return new object[] { CreateSend(accessCount: 6, maxAccessCount: 5, emails: null, password: null, AuthType.None), typeof(NeverAuthenticate) };
|
||||
yield return new object[] { CreateSend(accessCount: 0, maxAccessCount: 10, emails: "person@company.com", password: null, AuthType.Email), typeof(EmailOtp) };
|
||||
yield return new object[] { CreateSend(accessCount: 0, maxAccessCount: 10, emails: null, password: "hashedpassword", AuthType.Password), typeof(ResourcePassword) };
|
||||
yield return new object[] { CreateSend(accessCount: 0, maxAccessCount: 10, emails: null, password: null, AuthType.None), typeof(NotAuthenticated) };
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -123,7 +123,7 @@ public class SendAuthenticationQueryTests
|
||||
Id = sendId,
|
||||
AccessCount = 0,
|
||||
MaxAccessCount = 10,
|
||||
EmailHashes = "hashedemail",
|
||||
Emails = "person@company.com",
|
||||
Password = null,
|
||||
AuthType = AuthType.Email,
|
||||
Disabled = true,
|
||||
@@ -149,7 +149,7 @@ public class SendAuthenticationQueryTests
|
||||
Id = sendId,
|
||||
AccessCount = 0,
|
||||
MaxAccessCount = 10,
|
||||
EmailHashes = "hashedemail",
|
||||
Emails = "person@company.com",
|
||||
Password = null,
|
||||
AuthType = AuthType.Email,
|
||||
Disabled = false,
|
||||
@@ -175,7 +175,7 @@ public class SendAuthenticationQueryTests
|
||||
Id = sendId,
|
||||
AccessCount = 0,
|
||||
MaxAccessCount = 10,
|
||||
EmailHashes = "hashedemail",
|
||||
Emails = "person@company.com",
|
||||
Password = null,
|
||||
AuthType = AuthType.Email,
|
||||
Disabled = false,
|
||||
@@ -202,7 +202,7 @@ public class SendAuthenticationQueryTests
|
||||
Id = sendId,
|
||||
AccessCount = 0,
|
||||
MaxAccessCount = 10,
|
||||
EmailHashes = "hashedemail",
|
||||
Emails = "person@company.com",
|
||||
Password = null,
|
||||
AuthType = AuthType.Email,
|
||||
Disabled = false,
|
||||
@@ -228,7 +228,7 @@ public class SendAuthenticationQueryTests
|
||||
Id = sendId,
|
||||
AccessCount = 5,
|
||||
MaxAccessCount = 5,
|
||||
EmailHashes = "hashedemail",
|
||||
Emails = "person@company.com",
|
||||
Password = null,
|
||||
AuthType = AuthType.Email,
|
||||
Disabled = false,
|
||||
@@ -254,7 +254,7 @@ public class SendAuthenticationQueryTests
|
||||
Id = sendId,
|
||||
AccessCount = 1000,
|
||||
MaxAccessCount = null, // No limit
|
||||
EmailHashes = "hashedemail",
|
||||
Emails = "person@company.com",
|
||||
Password = null,
|
||||
AuthType = AuthType.Email,
|
||||
Disabled = false,
|
||||
@@ -280,7 +280,7 @@ public class SendAuthenticationQueryTests
|
||||
Id = sendId,
|
||||
AccessCount = 0,
|
||||
MaxAccessCount = 10,
|
||||
EmailHashes = "hashedemail",
|
||||
Emails = "person@company.com",
|
||||
Password = null,
|
||||
AuthType = AuthType.Email,
|
||||
Disabled = false,
|
||||
@@ -296,23 +296,23 @@ public class SendAuthenticationQueryTests
|
||||
Assert.IsType<EmailOtp>(result);
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> EmailHashesParsingTestCases()
|
||||
public static IEnumerable<object[]> EmailsParsingTestCases()
|
||||
{
|
||||
yield return new object[] { "hash1", new[] { "hash1" } };
|
||||
yield return new object[] { "hash1,hash2", new[] { "hash1", "hash2" } };
|
||||
yield return new object[] { " hash1 , hash2 ", new[] { "hash1", "hash2" } };
|
||||
yield return new object[] { "hash1,,hash2", new[] { "hash1", "hash2" } };
|
||||
yield return new object[] { " , hash1, ,hash2, ", new[] { "hash1", "hash2" } };
|
||||
yield return new object[] { "person@company.com", new[] { "person@company.com" } };
|
||||
yield return new object[] { "person1@company.com,person2@company.com", new[] { "person1@company.com", "person2@company.com" } };
|
||||
yield return new object[] { " person1@company.com , person2@company.com ", new[] { "person1@company.com", "person2@company.com" } };
|
||||
yield return new object[] { "person1@company.com,,person2@company.com", new[] { "person1@company.com", "person2@company.com" } };
|
||||
yield return new object[] { " , person1@company.com, ,person2@company.com, ", new[] { "person1@company.com", "person2@company.com" } };
|
||||
}
|
||||
|
||||
private static Send CreateSend(int accessCount, int? maxAccessCount, string? emailHashes, string? password, AuthType? authType)
|
||||
private static Send CreateSend(int accessCount, int? maxAccessCount, string? emails, string? password, AuthType? authType)
|
||||
{
|
||||
return new Send
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
AccessCount = accessCount,
|
||||
MaxAccessCount = maxAccessCount,
|
||||
EmailHashes = emailHashes,
|
||||
Emails = emails,
|
||||
Password = password,
|
||||
AuthType = authType,
|
||||
Disabled = false,
|
||||
|
||||
@@ -3,7 +3,6 @@ using Bit.Core.Services;
|
||||
using Bit.Core.Tools.Models.Data;
|
||||
using Bit.Core.Tools.SendFeatures.Queries.Interfaces;
|
||||
using Bit.IntegrationTestCommon.Factories;
|
||||
using Bit.Test.Common.Helpers;
|
||||
using Duende.IdentityModel;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
@@ -61,7 +60,7 @@ public class SendEmailOtpRequestValidatorIntegrationTests(IdentityApplicationFac
|
||||
|
||||
var sendAuthQuery = Substitute.For<ISendAuthenticationQuery>();
|
||||
sendAuthQuery.GetAuthenticationMethod(sendId)
|
||||
.Returns(new EmailOtp([CryptographyHelper.HashAndEncode(email)]));
|
||||
.Returns(new EmailOtp([email]));
|
||||
services.AddSingleton(sendAuthQuery);
|
||||
|
||||
// Mock OTP token provider
|
||||
@@ -106,7 +105,7 @@ public class SendEmailOtpRequestValidatorIntegrationTests(IdentityApplicationFac
|
||||
|
||||
var sendAuthQuery = Substitute.For<ISendAuthenticationQuery>();
|
||||
sendAuthQuery.GetAuthenticationMethod(sendId)
|
||||
.Returns(new EmailOtp(new[] { CryptographyHelper.HashAndEncode(email) }));
|
||||
.Returns(new EmailOtp(new[] { email }));
|
||||
services.AddSingleton(sendAuthQuery);
|
||||
|
||||
// Mock OTP token provider to validate successfully
|
||||
@@ -150,7 +149,7 @@ public class SendEmailOtpRequestValidatorIntegrationTests(IdentityApplicationFac
|
||||
|
||||
var sendAuthQuery = Substitute.For<ISendAuthenticationQuery>();
|
||||
sendAuthQuery.GetAuthenticationMethod(sendId)
|
||||
.Returns(new EmailOtp(new[] { CryptographyHelper.HashAndEncode(email) }));
|
||||
.Returns(new EmailOtp(new[] { email }));
|
||||
services.AddSingleton(sendAuthQuery);
|
||||
|
||||
// Mock OTP token provider to validate as false
|
||||
@@ -192,7 +191,7 @@ public class SendEmailOtpRequestValidatorIntegrationTests(IdentityApplicationFac
|
||||
|
||||
var sendAuthQuery = Substitute.For<ISendAuthenticationQuery>();
|
||||
sendAuthQuery.GetAuthenticationMethod(sendId)
|
||||
.Returns(new EmailOtp(new[] { CryptographyHelper.HashAndEncode(email) }));
|
||||
.Returns(new EmailOtp(new[] { email }));
|
||||
services.AddSingleton(sendAuthQuery);
|
||||
|
||||
// Mock OTP token provider to fail generation
|
||||
|
||||
@@ -5,7 +5,6 @@ using Bit.Core.Tools.Models.Data;
|
||||
using Bit.Identity.IdentityServer.RequestValidators.SendAccess;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Bit.Test.Common.Helpers;
|
||||
using Duende.IdentityModel;
|
||||
using Duende.IdentityServer.Validation;
|
||||
using NSubstitute;
|
||||
@@ -106,8 +105,7 @@ public class SendEmailOtpRequestValidatorTests
|
||||
expectedUniqueId)
|
||||
.Returns(generatedToken);
|
||||
|
||||
var emailHash = CryptographyHelper.HashAndEncode(email);
|
||||
emailOtp = emailOtp with { EmailHashes = [emailHash] };
|
||||
emailOtp = emailOtp with { emails = [email] };
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateRequestAsync(context, emailOtp, sendId);
|
||||
@@ -146,8 +144,7 @@ public class SendEmailOtpRequestValidatorTests
|
||||
Request = tokenRequest
|
||||
};
|
||||
|
||||
var emailHash = CryptographyHelper.HashAndEncode(email);
|
||||
emailOtp = emailOtp with { EmailHashes = [emailHash] };
|
||||
emailOtp = emailOtp with { emails = [email] };
|
||||
|
||||
sutProvider.GetDependency<IOtpTokenProvider<DefaultOtpTokenProviderOptions>>()
|
||||
.GenerateTokenAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>())
|
||||
@@ -182,8 +179,7 @@ public class SendEmailOtpRequestValidatorTests
|
||||
Request = tokenRequest
|
||||
};
|
||||
|
||||
var emailHash = CryptographyHelper.HashAndEncode(email);
|
||||
emailOtp = emailOtp with { EmailHashes = [emailHash] };
|
||||
emailOtp = emailOtp with { emails = [email] };
|
||||
|
||||
var expectedUniqueId = string.Format(SendAccessConstants.OtpToken.TokenUniqueIdentifier, sendId, email);
|
||||
|
||||
@@ -235,8 +231,7 @@ public class SendEmailOtpRequestValidatorTests
|
||||
Request = tokenRequest
|
||||
};
|
||||
|
||||
var emailHash = CryptographyHelper.HashAndEncode(email);
|
||||
emailOtp = emailOtp with { EmailHashes = [emailHash] };
|
||||
emailOtp = emailOtp with { emails = [email] };
|
||||
|
||||
var expectedUniqueId = string.Format(SendAccessConstants.OtpToken.TokenUniqueIdentifier, sendId, email);
|
||||
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
-- Update Send table to remove EmailHashes Column
|
||||
IF COL_LENGTH('[dbo].[Send]', 'EmailHashes') IS NOT NULL
|
||||
BEGIN
|
||||
ALTER TABLE [dbo].[Send]
|
||||
DROP COLUMN [EmailHashes];
|
||||
END
|
||||
GO
|
||||
|
||||
-- Update Send_Create to remove EmailHashes column
|
||||
CREATE OR ALTER PROCEDURE [dbo].[Send_Create]
|
||||
@Id UNIQUEIDENTIFIER OUTPUT,
|
||||
@UserId UNIQUEIDENTIFIER,
|
||||
@OrganizationId UNIQUEIDENTIFIER,
|
||||
@Type TINYINT,
|
||||
@Data VARCHAR(MAX),
|
||||
@Key VARCHAR(MAX),
|
||||
@Password NVARCHAR(300),
|
||||
@MaxAccessCount INT,
|
||||
@AccessCount INT,
|
||||
@CreationDate DATETIME2(7),
|
||||
@RevisionDate DATETIME2(7),
|
||||
@ExpirationDate DATETIME2(7),
|
||||
@DeletionDate DATETIME2(7),
|
||||
@Disabled BIT,
|
||||
@HideEmail BIT,
|
||||
@CipherId UNIQUEIDENTIFIER = NULL,
|
||||
@Emails NVARCHAR(4000) = NULL,
|
||||
@AuthType TINYINT = NULL
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
INSERT INTO [dbo].[Send]
|
||||
(
|
||||
[Id],
|
||||
[UserId],
|
||||
[OrganizationId],
|
||||
[Type],
|
||||
[Data],
|
||||
[Key],
|
||||
[Password],
|
||||
[MaxAccessCount],
|
||||
[AccessCount],
|
||||
[CreationDate],
|
||||
[RevisionDate],
|
||||
[ExpirationDate],
|
||||
[DeletionDate],
|
||||
[Disabled],
|
||||
[HideEmail],
|
||||
[CipherId],
|
||||
[Emails],
|
||||
[AuthType]
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
@Id,
|
||||
@UserId,
|
||||
@OrganizationId,
|
||||
@Type,
|
||||
@Data,
|
||||
@Key,
|
||||
@Password,
|
||||
@MaxAccessCount,
|
||||
@AccessCount,
|
||||
@CreationDate,
|
||||
@RevisionDate,
|
||||
@ExpirationDate,
|
||||
@DeletionDate,
|
||||
@Disabled,
|
||||
@HideEmail,
|
||||
@CipherId,
|
||||
@Emails,
|
||||
@AuthType
|
||||
)
|
||||
|
||||
IF @UserId IS NOT NULL
|
||||
BEGIN
|
||||
IF @Type = 1 --File
|
||||
BEGIN
|
||||
EXEC [dbo].[User_UpdateStorage] @UserId
|
||||
END
|
||||
EXEC [dbo].[User_BumpAccountRevisionDate] @UserId
|
||||
END
|
||||
-- TODO: OrganizationId bump?
|
||||
END
|
||||
GO
|
||||
|
||||
-- Update Send_Update to remove EmailHashes column
|
||||
CREATE OR ALTER PROCEDURE [dbo].[Send_Update]
|
||||
@Id UNIQUEIDENTIFIER,
|
||||
@UserId UNIQUEIDENTIFIER,
|
||||
@OrganizationId UNIQUEIDENTIFIER,
|
||||
@Type TINYINT,
|
||||
@Data VARCHAR(MAX),
|
||||
@Key VARCHAR(MAX),
|
||||
@Password NVARCHAR(300),
|
||||
@MaxAccessCount INT,
|
||||
@AccessCount INT,
|
||||
@CreationDate DATETIME2(7),
|
||||
@RevisionDate DATETIME2(7),
|
||||
@ExpirationDate DATETIME2(7),
|
||||
@DeletionDate DATETIME2(7),
|
||||
@Disabled BIT,
|
||||
@HideEmail BIT,
|
||||
@CipherId UNIQUEIDENTIFIER = NULL,
|
||||
@Emails NVARCHAR(4000) = NULL,
|
||||
@AuthType TINYINT = NULL
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
UPDATE
|
||||
[dbo].[Send]
|
||||
SET
|
||||
[UserId] = @UserId,
|
||||
[OrganizationId] = @OrganizationId,
|
||||
[Type] = @Type,
|
||||
[Data] = @Data,
|
||||
[Key] = @Key,
|
||||
[Password] = @Password,
|
||||
[MaxAccessCount] = @MaxAccessCount,
|
||||
[AccessCount] = @AccessCount,
|
||||
[CreationDate] = @CreationDate,
|
||||
[RevisionDate] = @RevisionDate,
|
||||
[ExpirationDate] = @ExpirationDate,
|
||||
[DeletionDate] = @DeletionDate,
|
||||
[Disabled] = @Disabled,
|
||||
[HideEmail] = @HideEmail,
|
||||
[CipherId] = @CipherId,
|
||||
[Emails] = @Emails,
|
||||
[AuthType] = @AuthType
|
||||
WHERE
|
||||
[Id] = @Id
|
||||
|
||||
IF @UserId IS NOT NULL
|
||||
BEGIN
|
||||
EXEC [dbo].[User_BumpAccountRevisionDate] @UserId
|
||||
END
|
||||
-- TODO: OrganizationId bump?
|
||||
END
|
||||
GO
|
||||
EXECUTE sp_refreshview N'[dbo].[SendView]'
|
||||
GO
|
||||
3502
util/MySqlMigrations/Migrations/20260204191943_2026-02-03_00_Send_Remove_EmailHashes_Column.Designer.cs
generated
Normal file
3502
util/MySqlMigrations/Migrations/20260204191943_2026-02-03_00_Send_Remove_EmailHashes_Column.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,29 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Bit.MySqlMigrations.Migrations;
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class _20260203_00_Send_Remove_EmailHashes_Column : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "EmailHashes",
|
||||
table: "Send");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "EmailHashes",
|
||||
table: "Send",
|
||||
type: "varchar(4000)",
|
||||
maxLength: 4000,
|
||||
nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
}
|
||||
}
|
||||
@@ -1753,10 +1753,6 @@ namespace Bit.MySqlMigrations.Migrations
|
||||
b.Property<bool>("Disabled")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<string>("EmailHashes")
|
||||
.HasMaxLength(4000)
|
||||
.HasColumnType("varchar(4000)");
|
||||
|
||||
b.Property<string>("Emails")
|
||||
.HasMaxLength(4000)
|
||||
.HasColumnType("varchar(4000)");
|
||||
|
||||
3508
util/PostgresMigrations/Migrations/20260204191814_2026-02-03_00_Send_Remove_EmailHashes_Column.Designer.cs
generated
Normal file
3508
util/PostgresMigrations/Migrations/20260204191814_2026-02-03_00_Send_Remove_EmailHashes_Column.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,28 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Bit.PostgresMigrations.Migrations;
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class _20260203_00_Send_Remove_EmailHashes_Column : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "EmailHashes",
|
||||
table: "Send");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "EmailHashes",
|
||||
table: "Send",
|
||||
type: "character varying(4000)",
|
||||
maxLength: 4000,
|
||||
nullable: true);
|
||||
}
|
||||
}
|
||||
@@ -1758,10 +1758,6 @@ namespace Bit.PostgresMigrations.Migrations
|
||||
b.Property<bool>("Disabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("EmailHashes")
|
||||
.HasMaxLength(4000)
|
||||
.HasColumnType("character varying(4000)");
|
||||
|
||||
b.Property<string>("Emails")
|
||||
.HasMaxLength(4000)
|
||||
.HasColumnType("character varying(4000)");
|
||||
|
||||
3491
util/SqliteMigrations/Migrations/20260204192000_2026-02-03_00_Send_Remove_EmailHashes_Column.Designer.cs
generated
Normal file
3491
util/SqliteMigrations/Migrations/20260204192000_2026-02-03_00_Send_Remove_EmailHashes_Column.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,28 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Bit.SqliteMigrations.Migrations;
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class _20260203_00_Send_Remove_EmailHashes_Column : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "EmailHashes",
|
||||
table: "Send");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "EmailHashes",
|
||||
table: "Send",
|
||||
type: "TEXT",
|
||||
maxLength: 4000,
|
||||
nullable: true);
|
||||
}
|
||||
}
|
||||
@@ -1741,10 +1741,6 @@ namespace Bit.SqliteMigrations.Migrations
|
||||
b.Property<bool>("Disabled")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("EmailHashes")
|
||||
.HasMaxLength(4000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Emails")
|
||||
.HasMaxLength(4000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
Reference in New Issue
Block a user