From 0544ec41d50f93fc858ce9d460ceb5cc71e8badc Mon Sep 17 00:00:00 2001 From: Alex Dragovich <46065570+itsadrago@users.noreply.github.com> Date: Thu, 29 Jan 2026 11:48:12 -0800 Subject: [PATCH] [PM-31394] use email address hash for send access email verification (#6921) * [PM-31394] use email address hash for send access email verification * [PM-31394] fixing identity server tests for send access * [PM-31394] fixing more identity server tests for send access --- .../Models/Data/SendAuthenticationTypes.cs | 4 ++-- .../SendAccess/SendEmailOtpRequestValidator.cs | 8 ++++++-- test/Common/Helpers/CryptographyHelper.cs | 17 +++++++++++++++++ .../Services/SendAuthenticationQueryTests.cs | 2 +- ...ndEmailOtpReqestValidatorIntegrationTests.cs | 10 ++++++---- .../SendEmailOtpRequestValidatorTests.cs | 13 +++++++++---- 6 files changed, 41 insertions(+), 13 deletions(-) create mode 100644 test/Common/Helpers/CryptographyHelper.cs diff --git a/src/Core/Tools/Models/Data/SendAuthenticationTypes.cs b/src/Core/Tools/Models/Data/SendAuthenticationTypes.cs index c90dba43a8..769e9df713 100644 --- a/src/Core/Tools/Models/Data/SendAuthenticationTypes.cs +++ b/src/Core/Tools/Models/Data/SendAuthenticationTypes.cs @@ -44,7 +44,7 @@ public record ResourcePassword(string Hash) : SendAuthenticationMethod; /// /// Create a send claim by requesting a one time password (OTP) confirmation code. /// -/// +/// /// The list of email address **hashes** permitted access to the send. /// -public record EmailOtp(string[] Emails) : SendAuthenticationMethod; +public record EmailOtp(string[] EmailHashes) : SendAuthenticationMethod; diff --git a/src/Identity/IdentityServer/RequestValidators/SendAccess/SendEmailOtpRequestValidator.cs b/src/Identity/IdentityServer/RequestValidators/SendAccess/SendEmailOtpRequestValidator.cs index 34a7a6f6e7..f20fdb6f07 100644 --- a/src/Identity/IdentityServer/RequestValidators/SendAccess/SendEmailOtpRequestValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/SendAccess/SendEmailOtpRequestValidator.cs @@ -1,4 +1,6 @@ using System.Security.Claims; +using System.Security.Cryptography; +using System.Text; using Bit.Core; using Bit.Core.Auth.Identity; using Bit.Core.Auth.Identity.TokenProviders; @@ -40,8 +42,10 @@ public class SendEmailOtpRequestValidator( return BuildErrorResult(SendAccessConstants.EmailOtpValidatorResults.EmailRequired); } - // email must be in the list of emails in the EmailOtp array - if (!authMethod.Emails.Contains(email)) + // 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(); + if (!authMethod.EmailHashes.Contains(hashEmailHex)) { return BuildErrorResult(SendAccessConstants.EmailOtpValidatorResults.EmailInvalid); } diff --git a/test/Common/Helpers/CryptographyHelper.cs b/test/Common/Helpers/CryptographyHelper.cs new file mode 100644 index 0000000000..30dfb1a679 --- /dev/null +++ b/test/Common/Helpers/CryptographyHelper.cs @@ -0,0 +1,17 @@ +using System.Security.Cryptography; +using System.Text; + +namespace Bit.Test.Common.Helpers; + +public class CryptographyHelper +{ + /// + /// Returns a hex-encoded, SHA256 hash for the given string + /// + public static string HashAndEncode(string text) + { + var hashBytes = SHA256.HashData(Encoding.UTF8.GetBytes(text)); + var hashEncoded = Convert.ToHexString(hashBytes).ToUpperInvariant(); + return hashEncoded; + } +} diff --git a/test/Core.Test/Tools/Services/SendAuthenticationQueryTests.cs b/test/Core.Test/Tools/Services/SendAuthenticationQueryTests.cs index 56b0f306cb..b4b1ecbc79 100644 --- a/test/Core.Test/Tools/Services/SendAuthenticationQueryTests.cs +++ b/test/Core.Test/Tools/Services/SendAuthenticationQueryTests.cs @@ -56,7 +56,7 @@ public class SendAuthenticationQueryTests // Assert var emailOtp = Assert.IsType(result); - Assert.Equal(expectedEmailHashes, emailOtp.Emails); + Assert.Equal(expectedEmailHashes, emailOtp.EmailHashes); } [Fact] diff --git a/test/Identity.IntegrationTest/RequestValidation/SendAccess/SendEmailOtpReqestValidatorIntegrationTests.cs b/test/Identity.IntegrationTest/RequestValidation/SendAccess/SendEmailOtpReqestValidatorIntegrationTests.cs index 3c4657653b..1c740cd448 100644 --- a/test/Identity.IntegrationTest/RequestValidation/SendAccess/SendEmailOtpReqestValidatorIntegrationTests.cs +++ b/test/Identity.IntegrationTest/RequestValidation/SendAccess/SendEmailOtpReqestValidatorIntegrationTests.cs @@ -3,6 +3,7 @@ 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; @@ -60,7 +61,7 @@ public class SendEmailOtpRequestValidatorIntegrationTests(IdentityApplicationFac var sendAuthQuery = Substitute.For(); sendAuthQuery.GetAuthenticationMethod(sendId) - .Returns(new EmailOtp([email])); + .Returns(new EmailOtp([CryptographyHelper.HashAndEncode(email)])); services.AddSingleton(sendAuthQuery); // Mock OTP token provider @@ -75,6 +76,7 @@ public class SendEmailOtpRequestValidatorIntegrationTests(IdentityApplicationFac }); }).CreateClient(); + var requestBody = SendAccessTestUtilities.CreateTokenRequestBody(sendId, email: email); // Email but no OTP // Act @@ -104,7 +106,7 @@ public class SendEmailOtpRequestValidatorIntegrationTests(IdentityApplicationFac var sendAuthQuery = Substitute.For(); sendAuthQuery.GetAuthenticationMethod(sendId) - .Returns(new EmailOtp(new[] { email })); + .Returns(new EmailOtp(new[] { CryptographyHelper.HashAndEncode(email) })); services.AddSingleton(sendAuthQuery); // Mock OTP token provider to validate successfully @@ -148,7 +150,7 @@ public class SendEmailOtpRequestValidatorIntegrationTests(IdentityApplicationFac var sendAuthQuery = Substitute.For(); sendAuthQuery.GetAuthenticationMethod(sendId) - .Returns(new EmailOtp(new[] { email })); + .Returns(new EmailOtp(new[] { CryptographyHelper.HashAndEncode(email) })); services.AddSingleton(sendAuthQuery); // Mock OTP token provider to validate as false @@ -190,7 +192,7 @@ public class SendEmailOtpRequestValidatorIntegrationTests(IdentityApplicationFac var sendAuthQuery = Substitute.For(); sendAuthQuery.GetAuthenticationMethod(sendId) - .Returns(new EmailOtp(new[] { email })); + .Returns(new EmailOtp(new[] { CryptographyHelper.HashAndEncode(email) })); services.AddSingleton(sendAuthQuery); // Mock OTP token provider to fail generation diff --git a/test/Identity.Test/IdentityServer/SendAccess/SendEmailOtpRequestValidatorTests.cs b/test/Identity.Test/IdentityServer/SendAccess/SendEmailOtpRequestValidatorTests.cs index 7fdfacf428..1815b9207d 100644 --- a/test/Identity.Test/IdentityServer/SendAccess/SendEmailOtpRequestValidatorTests.cs +++ b/test/Identity.Test/IdentityServer/SendAccess/SendEmailOtpRequestValidatorTests.cs @@ -5,6 +5,7 @@ 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; @@ -105,7 +106,8 @@ public class SendEmailOtpRequestValidatorTests expectedUniqueId) .Returns(generatedToken); - emailOtp = emailOtp with { Emails = [email] }; + var emailHash = CryptographyHelper.HashAndEncode(email); + emailOtp = emailOtp with { EmailHashes = [emailHash] }; // Act var result = await sutProvider.Sut.ValidateRequestAsync(context, emailOtp, sendId); @@ -144,7 +146,8 @@ public class SendEmailOtpRequestValidatorTests Request = tokenRequest }; - emailOtp = emailOtp with { Emails = [email] }; + var emailHash = CryptographyHelper.HashAndEncode(email); + emailOtp = emailOtp with { EmailHashes = [emailHash] }; sutProvider.GetDependency>() .GenerateTokenAsync(Arg.Any(), Arg.Any(), Arg.Any()) @@ -179,7 +182,8 @@ public class SendEmailOtpRequestValidatorTests Request = tokenRequest }; - emailOtp = emailOtp with { Emails = [email] }; + var emailHash = CryptographyHelper.HashAndEncode(email); + emailOtp = emailOtp with { EmailHashes = [emailHash] }; var expectedUniqueId = string.Format(SendAccessConstants.OtpToken.TokenUniqueIdentifier, sendId, email); @@ -231,7 +235,8 @@ public class SendEmailOtpRequestValidatorTests Request = tokenRequest }; - emailOtp = emailOtp with { Emails = [email] }; + var emailHash = CryptographyHelper.HashAndEncode(email); + emailOtp = emailOtp with { EmailHashes = [emailHash] }; var expectedUniqueId = string.Format(SendAccessConstants.OtpToken.TokenUniqueIdentifier, sendId, email);