Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Backend password hashing and encryption logic changes #44

Merged
merged 30 commits into from
Nov 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
a07ddef
hashing service
radek00 Nov 18, 2023
0fbfad5
SecurePasswordHash value object
radek00 Nov 18, 2023
3784044
invalid password exception
radek00 Nov 18, 2023
deaebc3
update factory for tests
radek00 Nov 18, 2023
0a330e9
add hash to factory and entity
radek00 Nov 18, 2023
70b4101
update commands and handlers
radek00 Nov 18, 2023
a934ae5
install KeyDerivation package
radek00 Nov 18, 2023
b42e2e1
update models and generate migrations
radek00 Nov 18, 2023
7315563
remove isProtected from read model
radek00 Nov 18, 2023
3a609a2
read service GetSecureUpload
radek00 Nov 18, 2023
651af02
VerifyUploadHandler
radek00 Nov 18, 2023
a67f447
verify password
radek00 Nov 19, 2023
c3f020c
VerifyUploadHandler tests
radek00 Nov 19, 2023
1bc310c
PasswordHash domain tests
radek00 Nov 19, 2023
25b2156
ViewSecureUpload tests
radek00 Nov 19, 2023
3f5b5a4
adjust models to api changes
radek00 Nov 20, 2023
c597e54
verify upload service methods
radek00 Nov 20, 2023
4e9b101
change casing
radek00 Nov 20, 2023
224470a
nullable properties in domain model
radek00 Nov 22, 2023
fccad0a
InvalidPassword response error
radek00 Nov 22, 2023
a8e8a82
use suspense component
radek00 Nov 22, 2023
37fbf12
validate password by api call
radek00 Nov 22, 2023
dfb311b
prettier
radek00 Nov 22, 2023
3e7714d
Refactor encryption and decryption logic
radek00 Nov 25, 2023
14ab8d0
rename key fields
radek00 Nov 25, 2023
6430490
rename cryptography service
radek00 Nov 25, 2023
93c7947
nullable expiryDate and passwordHash value objects
radek00 Nov 25, 2023
a2ff1c0
handle null values
radek00 Nov 25, 2023
af4dc9c
Remove encryptionKey import and pbkdfHash utility
radek00 Nov 25, 2023
408c11e
prettier
radek00 Nov 25, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion SecureSend.Application/Commands/CreateSecureUpload.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@

namespace SecureSend.Application.Commands
{
public record CreateSecureUpload(Guid uploadId, DateTime? expiryDate): ICommand<Unit>;
public record CreateSecureUpload(Guid uploadId, DateTime? expiryDate, string? password): ICommand<Unit>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ public CreateSecureUploadHandler(ISecureSendUploadRepository secureSendUploadRep
public async Task<Unit> Handle (CreateSecureUpload command, CancellationToken cancellationToken)
{
var persisted = await _secureUploadReadService.GetUploadId(command.uploadId, cancellationToken);
if (persisted != Guid.Empty) throw new UploadAlreadyExistsException(persisted.Value);
var secureUpload = _secureSendUploadFactory.CreateSecureSendUpload(command.uploadId, new SecureSendUploadDate(), command.expiryDate, false);
if (persisted is not null && persisted != Guid.Empty) throw new UploadAlreadyExistsException(persisted.Value);
var secureUpload = _secureSendUploadFactory.CreateSecureSendUpload(command.uploadId, new SecureSendUploadDate(), command.expiryDate, false, command.password);
await _secureSendUploadRepository.AddAsync(secureUpload, cancellationToken);
return Unit.Value;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@ public ViewSecureUploadHandler(ISecureSendUploadRepository repository)

public async Task<SecureUploadDto> Handle(ViewSecureUpload request, CancellationToken cancellationToken)
{
var upload = await _repository.GetAsync(request.id, cancellationToken);
if (upload is null) throw new UploadDoesNotExistException(request.id);
if (upload.ExpiryDate is not null && upload.ExpiryDate < DateTime.UtcNow) throw new UploadExpiredException(upload.ExpiryDate);
var upload = await _repository.GetAsync(request.id, cancellationToken) ?? throw new UploadDoesNotExistException(request.id);
if (upload.PasswordHash is not null)
{
upload.PasswordHash.VerifyHash(request.password);
}
upload.MarkAsViewed();
await _repository.SaveChanges(cancellationToken);

Expand Down
4 changes: 1 addition & 3 deletions SecureSend.Application/Commands/ViewSecureUpload.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,5 @@

namespace SecureSend.Application.Commands
{
public record ViewSecureUpload(Guid id) : ICommand<SecureUploadDto>
{
}
public record ViewSecureUpload(Guid id, string? password) : ICommand<SecureUploadDto>;
}
7 changes: 7 additions & 0 deletions SecureSend.Application/DTO/UploadVerifyResponseDTO.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace SecureSend.Application.DTO;

public class UploadVerifyResponseDTO
{
public Guid SecureUploadId { get; set; }
public bool IsProtected { get; set; }
}
31 changes: 31 additions & 0 deletions SecureSend.Application/Queries/Handlers/VerifyUploadHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using SecureSend.Application.DTO;
using SecureSend.Application.Exceptions;
using SecureSend.Application.Services;

namespace SecureSend.Application.Queries.Handlers;

public sealed class VerifyUploadHandler: IQueryHandler<VerifyUpload,UploadVerifyResponseDTO>
{
private readonly ISecureUploadReadService _readService;

public VerifyUploadHandler(ISecureUploadReadService readService)
{
_readService = readService;
}

public async Task<UploadVerifyResponseDTO> Handle(VerifyUpload request, CancellationToken cancellationToken)
{
var upload = await _readService.GetSecureUpload(request.id, cancellationToken);
if (upload is null) throw new UploadDoesNotExistException(request.id);
if (upload.ExpiryDate is not null && upload.ExpiryDate < DateTime.UtcNow) throw new UploadExpiredException(upload.ExpiryDate);

var uploadVerifyDto = new UploadVerifyResponseDTO()
{
SecureUploadId = upload.Id,
IsProtected = upload.PasswordHash is not null,

};

return uploadVerifyDto;
}
}
5 changes: 5 additions & 0 deletions SecureSend.Application/Queries/VerifyUpload.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
using SecureSend.Application.DTO;

namespace SecureSend.Application.Queries;

public record VerifyUpload(Guid id): IQuery<UploadVerifyResponseDTO>;
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@ public interface ISecureUploadReadService
{
Task<UploadedFilesReadModel?> GetUploadedFile(string fileName, Guid id, CancellationToken cancellationToken);
Task<Guid?> GetUploadId(Guid id, CancellationToken cancellationToken);
Task<SecureUploadsReadModel?> GetSecureUpload(Guid id, CancellationToken cancellationToken);
}
}
7 changes: 5 additions & 2 deletions SecureSend.Domain/Entities/SecureSendUpload.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,22 @@
{
public SecureSendUploadId Id {get; private set;}
public SecureSendUploadDate UploadDate { get; private set; }
public SecureSendExpiryDate ExpiryDate { get; private set; }
public SecureSendExpiryDate? ExpiryDate { get; private set; }
public SecureSendIsViewed IsViewed { get; private set; }
public SecureSendPasswordHash? PasswordHash { get; private set; }

public List<SecureSendFile> Files { get; private set; } = new();

public SecureSendUpload(SecureSendUploadId id, SecureSendUploadDate uploadDate, SecureSendExpiryDate expiryDate, SecureSendIsViewed isViewedl)
public SecureSendUpload(SecureSendUploadId id, SecureSendUploadDate uploadDate, SecureSendExpiryDate? expiryDate, SecureSendIsViewed isViewedl, SecureSendPasswordHash? passwordHash)
{
Id = id;
UploadDate = uploadDate;
ExpiryDate = expiryDate;
IsViewed = isViewedl;
PasswordHash = passwordHash;
}

public SecureSendUpload()

Check warning on line 26 in SecureSend.Domain/Entities/SecureSendUpload.cs

View workflow job for this annotation

GitHub Actions / build-ubuntu-latest

Non-nullable property 'Id' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

Check warning on line 26 in SecureSend.Domain/Entities/SecureSendUpload.cs

View workflow job for this annotation

GitHub Actions / build-ubuntu-latest

Non-nullable property 'UploadDate' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

Check warning on line 26 in SecureSend.Domain/Entities/SecureSendUpload.cs

View workflow job for this annotation

GitHub Actions / build-ubuntu-latest

Non-nullable property 'IsViewed' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

Check warning on line 26 in SecureSend.Domain/Entities/SecureSendUpload.cs

View workflow job for this annotation

GitHub Actions / build-windows-latest

Non-nullable property 'Id' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

Check warning on line 26 in SecureSend.Domain/Entities/SecureSendUpload.cs

View workflow job for this annotation

GitHub Actions / build-windows-latest

Non-nullable property 'UploadDate' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

Check warning on line 26 in SecureSend.Domain/Entities/SecureSendUpload.cs

View workflow job for this annotation

GitHub Actions / build-windows-latest

Non-nullable property 'IsViewed' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

Check warning on line 26 in SecureSend.Domain/Entities/SecureSendUpload.cs

View workflow job for this annotation

GitHub Actions / build-macOS-latest

Non-nullable property 'Id' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

Check warning on line 26 in SecureSend.Domain/Entities/SecureSendUpload.cs

View workflow job for this annotation

GitHub Actions / build-macOS-latest

Non-nullable property 'UploadDate' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

Check warning on line 26 in SecureSend.Domain/Entities/SecureSendUpload.cs

View workflow job for this annotation

GitHub Actions / build-macOS-latest

Non-nullable property 'IsViewed' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.
{
}

Expand Down
10 changes: 10 additions & 0 deletions SecureSend.Domain/Exceptions/InvalidPasswordException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using SecureSend.Domain.Exceptions;

namespace SecureSend.Domain.Entities;

public class InvalidPasswordException : SecureSendException
{
public InvalidPasswordException() : base("Provided password is invalid.")
{
}
}
2 changes: 1 addition & 1 deletion SecureSend.Domain/Factories/ISecureSendUploadFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ namespace SecureSend.Domain.Factories
{
public interface ISecureSendUploadFactory
{
SecureSendUpload CreateSecureSendUpload(SecureSendUploadId id, SecureSendUploadDate uploadDate, SecureSendExpiryDate expiryDate, SecureSendIsViewed isViewedl);
SecureSendUpload CreateSecureSendUpload(SecureSendUploadId id, SecureSendUploadDate uploadDate, SecureSendExpiryDate? expiryDate, SecureSendIsViewed isViewedl, SecureSendPasswordHash? password);
}
}
4 changes: 2 additions & 2 deletions SecureSend.Domain/Factories/SecureSendUploadFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ public SecureSendUploadFactory()

}

public SecureSendUpload CreateSecureSendUpload(SecureSendUploadId id, SecureSendUploadDate uploadDate, SecureSendExpiryDate expiryDate, SecureSendIsViewed isViewedl)
public SecureSendUpload CreateSecureSendUpload(SecureSendUploadId id, SecureSendUploadDate uploadDate, SecureSendExpiryDate? expiryDate, SecureSendIsViewed isViewedl, SecureSendPasswordHash? password)
{
return new SecureSendUpload(id, uploadDate, expiryDate, isViewedl);
return new SecureSendUpload(id, uploadDate, expiryDate, isViewedl, password);
}
}
}
1 change: 1 addition & 0 deletions SecureSend.Domain/ReadModels/SecureUploadsReadModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
public DateTime? ExpiryDate { get; set; }
public bool IsViewed { get; set; }
public DateTime UploadDate { get; set; }
public byte[]? PasswordHash { get; set; }
public ICollection<UploadedFilesReadModel> Files { get; set; }

Check warning on line 10 in SecureSend.Domain/ReadModels/SecureUploadsReadModel.cs

View workflow job for this annotation

GitHub Actions / build-ubuntu-latest

Non-nullable property 'Files' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

Check warning on line 10 in SecureSend.Domain/ReadModels/SecureUploadsReadModel.cs

View workflow job for this annotation

GitHub Actions / build-windows-latest

Non-nullable property 'Files' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

Check warning on line 10 in SecureSend.Domain/ReadModels/SecureUploadsReadModel.cs

View workflow job for this annotation

GitHub Actions / build-macOS-latest

Non-nullable property 'Files' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ namespace SecureSend.Domain.Repositories
{
public interface ISecureSendUploadRepository
{
Task<SecureSendUpload> GetAsync(SecureSendUploadId id, CancellationToken cancellationToken);
Task<SecureSendUpload?> GetAsync(SecureSendUploadId id, CancellationToken cancellationToken);
Task AddAsync(SecureSendUpload upload, CancellationToken cancellationToken);
Task DeleteAsync(SecureSendUpload id, CancellationToken cancellationToken);
Task UpdateAsync(SecureSendUpload upload, CancellationToken cancellationToken);
Expand Down
1 change: 1 addition & 0 deletions SecureSend.Domain/SecureSend.Domain.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Cryptography.KeyDerivation" Version="8.0.0" />
<PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="2.2.0" />
</ItemGroup>

Expand Down
165 changes: 165 additions & 0 deletions SecureSend.Domain/Services/HashingService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
using System.Security.Cryptography;
using Microsoft.AspNetCore.Cryptography.KeyDerivation;

namespace SecureSend.Domain.Services;

public class HashingService
{
private const int DefaultIterations = 10000;

/// <summary>
/// Provides Information about a specific Hash Version
/// </summary>
private class HashVersion
{
public short Version { get; set; }
public int SaltSize { get; set; }
public int HashSize { get; set; }
public KeyDerivationPrf KeyDerivation { get; set; }
}

/// <summary>
/// Holds all possible Hash Versions
/// </summary>
private readonly Dictionary<short, HashVersion> _versions = new Dictionary<short, HashVersion>
{
{
1, new HashVersion
{
Version = 1,
KeyDerivation = KeyDerivationPrf.HMACSHA512,
HashSize = 256 / 8,
SaltSize = 128 / 8
}
}
};

/// <summary>
/// The default Hash Version, which should be used, if a new Hash is Created
/// </summary>
private HashVersion DefaultVersion => _versions[1];

/// <summary>
/// Checks if a given hash uses the latest version
/// </summary>
/// <param name="data">The hash</param>
/// <returns>Is the hash of the latest version?</returns>
public bool IsLatestHashVersion(byte[] data)
{
var version = BitConverter.ToInt16(data, 0);
return version == DefaultVersion.Version;
}

/// <summary>
/// Checks if a given hash uses the latest version
/// </summary>
/// <param name="data">The hash</param>
/// <returns>Is the hash of the latest version?</returns>
public bool IsLatestHashVersion(string data)
{
var dataBytes = Convert.FromBase64String(data);
return IsLatestHashVersion(dataBytes);
}

/// <summary>
/// Gets a random byte array
/// </summary>
/// <param name="length">The length of the byte array</param>
/// <returns>The random byte array</returns>
public byte[] GetRandomBytes(int length)
{
var data = new byte[length];
using (var randomNumberGenerator = RandomNumberGenerator.Create())
{
randomNumberGenerator.GetBytes(data);
}
return data;
}

/// <summary>
/// Creates a Hash of a clear text
/// </summary>
/// <param name="clearText">the clear text</param>
/// <param name="iterations">the number of iteration the hash alogrythm should run</param>
/// <returns>the Hash</returns>
public byte[] Hash(string clearText, int iterations = DefaultIterations)
{
//get current version
var currentVersion = DefaultVersion;

//get the byte arrays of the hash and meta information
var saltBytes = GetRandomBytes(currentVersion.SaltSize);
var versionBytes = BitConverter.GetBytes(currentVersion.Version);
var iterationBytes = BitConverter.GetBytes(iterations);
var hashBytes = KeyDerivation.Pbkdf2(clearText, saltBytes, currentVersion.KeyDerivation, iterations, currentVersion.HashSize);

//calculate the indexes for the combined hash
var indexVersion = 0;
var indexIteration = indexVersion + 2;
var indexSalt = indexIteration + 4;
var indexHash = indexSalt + currentVersion.SaltSize;

//combine all data to one result hash
var resultBytes = new byte[2 + 4 + currentVersion.SaltSize + currentVersion.HashSize];
Array.Copy(versionBytes, 0, resultBytes, indexVersion, 2);
Array.Copy(iterationBytes, 0, resultBytes, indexIteration, 4);
Array.Copy(saltBytes, 0, resultBytes, indexSalt, currentVersion.SaltSize);
Array.Copy(hashBytes, 0, resultBytes, indexHash, currentVersion.HashSize);
return resultBytes;
}

/// <summary>
/// Creates a Hash of a clear text and convert it to a Base64 String representation
/// </summary>
/// <param name="clearText">the clear text</param>
/// <param name="iterations">the number of iteration the hash alogrythm should run</param>
/// <returns>the Hash</returns>
public string HashToString(string clearText, int iterations = DefaultIterations)
{
var data = Hash(clearText, iterations);
return Convert.ToBase64String(data);
}

/// <summary>
/// Verifies a given clear Text against a hash
/// </summary>
/// <param name="clearText">The clear text</param>
/// <param name="data">The hash</param>
/// <returns>Is the hash equal to the clear text?</returns>
public bool Verify(string clearText, byte[] data)
{
//Get the current version and number of iterations
var currentVersion = _versions[BitConverter.ToInt16(data, 0)];
var iteration = BitConverter.ToInt32(data, 2);

//Create the byte arrays for the salt and hash
var saltBytes = new byte[currentVersion.SaltSize];
var hashBytes = new byte[currentVersion.HashSize];

//Calculate the indexes of the salt and the hash
var indexSalt = 2 + 4; // Int16 (Version) and Int32 (Iteration)
var indexHash = indexSalt + currentVersion.SaltSize;

//Fill the byte arrays with salt and hash
Array.Copy(data, indexSalt, saltBytes, 0, currentVersion.SaltSize);
Array.Copy(data, indexHash, hashBytes, 0, currentVersion.HashSize);

//Hash the current clearText with the parameters given via the data
var verificationHashBytes = KeyDerivation.Pbkdf2(clearText, saltBytes, currentVersion.KeyDerivation, iteration, currentVersion.HashSize);

//Check if generated hashes are equal
return hashBytes.SequenceEqual(verificationHashBytes);
}

/// <summary>
/// Verifies a given clear Text against a hash
/// </summary>
/// <param name="clearText">The clear text</param>
/// <param name="data">The hash</param>
/// <returns>Is the hash equal to the clear text?</returns>
public bool Verify(string clearText, string data)
{
var dataBytes = Convert.FromBase64String(data);
return Verify(clearText, dataBytes);
}
}
4 changes: 2 additions & 2 deletions SecureSend.Domain/ValueObjects/SecureSendExpiryDate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ public record SecureSendExpiryDate

public SecureSendExpiryDate(DateTime? value = null)
{
Value = value is not null ? value?.ToUniversalTime() : null;
Value = value?.ToUniversalTime();
}


public static implicit operator DateTime?(SecureSendExpiryDate secureSendExpiryDate) => secureSendExpiryDate?.Value;
public static implicit operator DateTime?(SecureSendExpiryDate? secureSendExpiryDate) => secureSendExpiryDate?.Value;

public static implicit operator SecureSendExpiryDate(DateTime? secureSendExpiryDate) => new(secureSendExpiryDate);
}
Expand Down
31 changes: 31 additions & 0 deletions SecureSend.Domain/ValueObjects/SecureSendPasswordHash.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using SecureSend.Domain.Services;

namespace SecureSend.Domain.Entities;

public class SecureSendPasswordHash
{
public byte[]? Value { get;}
private readonly HashingService _hashingService;
public SecureSendPasswordHash(string? password)
{
_hashingService = new HashingService();
Value = String.IsNullOrEmpty(password) ? null : _hashingService.Hash(password);
}

public SecureSendPasswordHash(byte[]? hash)
{
_hashingService = new HashingService();
Value = hash;
}

public void VerifyHash(string? password)
{
if (Value is null || String.IsNullOrEmpty(password) || !_hashingService.Verify(password, Value)) throw new InvalidPasswordException();
}

public static implicit operator byte[]?(SecureSendPasswordHash value) => value.Value;

public static implicit operator SecureSendPasswordHash(byte[] hash) => new(hash);

public static implicit operator SecureSendPasswordHash(string? password) => new(password);
}
Loading
Loading