Skip to content

Commit a0436ef

Browse files
authored
Merge pull request #17 from DEFRA/feature/2831-aes-crypto-transform
Feature/2831 aes crypto transform
2 parents c622f91 + 8d5837a commit a0436ef

File tree

8 files changed

+862
-22
lines changed

8 files changed

+862
-22
lines changed

docker-compose.override.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ services:
1212
- StorageConfiguration__ExternalStorage__BucketName=test-external-bucket
1313
- StorageConfiguration__InternalStorage__BucketName=test-internal-bucket
1414
- ServiceBusSenderConfiguration__DataBridgeEventsTopic__TopicArn=arn:aws:sns:eu-west-2:000000000000:ls-keeper-data-bridge-events
15+
- AesSalt=LD8BB2NNze7qKbVyLutAfGBxhkAApR
1516
ports:
1617
- "8080"
1718
volumes:

src/KeeperData.Bridge/Setup/ServiceCollectionExtensions.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using KeeperData.Application.Setup;
2+
using KeeperData.Infrastructure.Crypto;
23
using KeeperData.Infrastructure.Database.Setup;
34
using KeeperData.Infrastructure.Messaging.Setup;
45
using KeeperData.Infrastructure.Storage.Setup;
@@ -20,6 +21,8 @@ public static void ConfigureApi(this IServiceCollection services, IConfiguration
2021
services.AddMessagingDependencies(configuration);
2122

2223
services.AddStorageDependencies(configuration);
24+
25+
services.AddCrypto(configuration);
2326
}
2427

2528
private static void ConfigureHealthChecks(this IServiceCollection services)
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
namespace KeeperData.Core.Crypto;
2+
3+
public delegate void ProgressCallback(int progressPercentage, string status);
4+
5+
public interface IAesCryptoTransform
6+
{
7+
Task EncryptFileAsync(string inputFilePath, string outputFilePath, string password, byte[] salt,
8+
ProgressCallback? progressCallback = null, CancellationToken cancellationToken = default);
9+
10+
Task EncryptFileAsync(string inputFilePath, string outputFilePath, string password, string salt,
11+
ProgressCallback? progressCallback = null, CancellationToken cancellationToken = default);
12+
13+
Task DecryptFileAsync(string inputFilePath, string outputFilePath, string password, byte[] salt,
14+
ProgressCallback? progressCallback = null, CancellationToken cancellationToken = default);
15+
16+
Task DecryptFileAsync(string inputFilePath, string outputFilePath, string password, string salt,
17+
ProgressCallback? progressCallback = null, CancellationToken cancellationToken = default);
18+
19+
Task EncryptStreamAsync(Stream inputStream, Stream outputStream, string password, byte[] salt,
20+
long? totalBytes = null, ProgressCallback? progressCallback = null, CancellationToken cancellationToken = default);
21+
22+
Task EncryptStreamAsync(Stream inputStream, Stream outputStream, string password, string salt,
23+
long? totalBytes = null, ProgressCallback? progressCallback = null, CancellationToken cancellationToken = default);
24+
25+
Task DecryptStreamAsync(Stream inputStream, Stream outputStream, string password, byte[] salt,
26+
long? totalBytes = null, ProgressCallback? progressCallback = null, CancellationToken cancellationToken = default);
27+
28+
Task DecryptStreamAsync(Stream inputStream, Stream outputStream, string password, string salt,
29+
long? totalBytes = null, ProgressCallback? progressCallback = null, CancellationToken cancellationToken = default);
30+
}
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
using KeeperData.Core.Crypto;
2+
using System.Security.Cryptography;
3+
using System.Text;
4+
5+
namespace KeeperData.Infrastructure.Crypto;
6+
7+
public class AesCryptoTransform : IAesCryptoTransform
8+
{
9+
private const int PbeKeySpecIterationsDefault = 32;
10+
private const int PbeKeySpecKeyLenDefault = 256;
11+
private const int BufferSize = 64 * 1024;
12+
private const int ProgressReportInterval = 1;
13+
14+
public async Task EncryptFileAsync(string inputFilePath, string outputFilePath, string password, byte[] salt,
15+
ProgressCallback? progressCallback = null, CancellationToken cancellationToken = default)
16+
{
17+
if (!File.Exists(inputFilePath))
18+
{
19+
throw new FileNotFoundException($"Input file not found: {inputFilePath}");
20+
}
21+
22+
var fileInfo = new FileInfo(inputFilePath);
23+
var totalBytes = fileInfo.Length;
24+
25+
using var inputStream = new FileStream(inputFilePath, FileMode.Open, FileAccess.Read);
26+
using var outputStream = new FileStream(outputFilePath, FileMode.Create, FileAccess.Write);
27+
28+
await EncryptStreamAsync(inputStream, outputStream, password, salt, totalBytes, progressCallback, cancellationToken);
29+
}
30+
31+
public async Task DecryptFileAsync(string inputFilePath,
32+
string outputFilePath,
33+
string password,
34+
byte[] salt,
35+
ProgressCallback? progressCallback = null,
36+
CancellationToken cancellationToken = default)
37+
{
38+
if (!File.Exists(inputFilePath))
39+
{
40+
throw new FileNotFoundException($"Input file not found: {inputFilePath}");
41+
}
42+
43+
var fileInfo = new FileInfo(inputFilePath);
44+
var totalBytes = fileInfo.Length;
45+
46+
using var inputStream = new FileStream(inputFilePath, FileMode.Open, FileAccess.Read);
47+
using var outputStream = new FileStream(outputFilePath, FileMode.Create, FileAccess.Write);
48+
49+
await DecryptStreamAsync(inputStream, outputStream, password, salt, totalBytes, progressCallback, cancellationToken);
50+
}
51+
52+
public async Task EncryptStreamAsync(Stream inputStream,
53+
Stream outputStream,
54+
string password,
55+
byte[] salt,
56+
long? totalBytes = null,
57+
ProgressCallback? progressCallback = null,
58+
CancellationToken cancellationToken = default)
59+
{
60+
var key = DeriveKey(password, salt);
61+
62+
using var aes = Aes.Create();
63+
aes.Key = key;
64+
aes.Mode = CipherMode.ECB;
65+
aes.Padding = PaddingMode.PKCS7;
66+
67+
using var encryptor = aes.CreateEncryptor();
68+
using var cryptoStream = new CryptoStream(outputStream, encryptor, CryptoStreamMode.Write, leaveOpen: true);
69+
70+
await ProcessStreamAsync(inputStream, cryptoStream, totalBytes, progressCallback, "Encrypting", cancellationToken);
71+
72+
cryptoStream.FlushFinalBlock();
73+
}
74+
75+
public async Task DecryptStreamAsync(Stream inputStream,
76+
Stream outputStream,
77+
string password,
78+
byte[] salt,
79+
long? totalBytes = null,
80+
ProgressCallback? progressCallback = null,
81+
CancellationToken cancellationToken = default)
82+
{
83+
var key = DeriveKey(password, salt);
84+
85+
using var aes = Aes.Create();
86+
aes.Key = key;
87+
aes.Mode = CipherMode.ECB;
88+
aes.Padding = PaddingMode.PKCS7;
89+
90+
using var decryptor = aes.CreateDecryptor();
91+
using var cryptoStream = new CryptoStream(inputStream, decryptor, CryptoStreamMode.Read, leaveOpen: true);
92+
93+
await ProcessStreamAsync(cryptoStream, outputStream, totalBytes, progressCallback, "Decrypting", cancellationToken);
94+
}
95+
96+
public async Task EncryptFileAsync(string inputFilePath,
97+
string outputFilePath,
98+
string password,
99+
string salt,
100+
ProgressCallback? progressCallback = null,
101+
CancellationToken cancellationToken = default)
102+
{
103+
var saltBytes = string.IsNullOrEmpty(salt) ? new byte[0] : Encoding.UTF8.GetBytes(salt);
104+
await EncryptFileAsync(inputFilePath, outputFilePath, password, saltBytes, progressCallback, cancellationToken);
105+
}
106+
107+
public async Task DecryptFileAsync(string inputFilePath,
108+
string outputFilePath,
109+
string password,
110+
string salt,
111+
ProgressCallback? progressCallback = null,
112+
CancellationToken cancellationToken = default)
113+
{
114+
var saltBytes = string.IsNullOrEmpty(salt) ? new byte[0] : Encoding.UTF8.GetBytes(salt);
115+
await DecryptFileAsync(inputFilePath, outputFilePath, password, saltBytes, progressCallback, cancellationToken);
116+
}
117+
118+
public async Task EncryptStreamAsync(Stream inputStream,
119+
Stream outputStream,
120+
string password,
121+
string salt,
122+
long? totalBytes = null,
123+
ProgressCallback? progressCallback = null,
124+
CancellationToken cancellationToken = default)
125+
{
126+
var saltBytes = string.IsNullOrEmpty(salt) ? new byte[0] : Encoding.UTF8.GetBytes(salt);
127+
await EncryptStreamAsync(inputStream, outputStream, password, saltBytes, totalBytes, progressCallback, cancellationToken);
128+
}
129+
130+
public async Task DecryptStreamAsync(Stream inputStream,
131+
Stream outputStream,
132+
string password,
133+
string salt,
134+
long? totalBytes = null,
135+
ProgressCallback? progressCallback = null,
136+
CancellationToken cancellationToken = default)
137+
{
138+
var saltBytes = string.IsNullOrEmpty(salt) ? new byte[0] : Encoding.UTF8.GetBytes(salt);
139+
await DecryptStreamAsync(inputStream, outputStream, password, saltBytes, totalBytes, progressCallback, cancellationToken);
140+
}
141+
142+
private static byte[] DeriveKey(string password, byte[] salt)
143+
{
144+
var actualSalt = salt;
145+
if (salt.Length == 0)
146+
{
147+
actualSalt = new byte[8];
148+
}
149+
else if (salt.Length < 8)
150+
{
151+
actualSalt = new byte[8];
152+
Array.Copy(salt, actualSalt, salt.Length);
153+
}
154+
155+
using var pbkdf2 = new Rfc2898DeriveBytes(password, actualSalt, PbeKeySpecIterationsDefault, HashAlgorithmName.SHA1);
156+
return pbkdf2.GetBytes(PbeKeySpecKeyLenDefault / 8);
157+
}
158+
159+
private static async Task ProcessStreamAsync(Stream inputStream,
160+
Stream outputStream,
161+
long? totalBytes,
162+
ProgressCallback? progressCallback,
163+
string operation,
164+
CancellationToken cancellationToken = default)
165+
{
166+
var buffer = new byte[BufferSize];
167+
long totalBytesProcessed = 0;
168+
var lastReportedProgress = -1;
169+
170+
progressCallback?.Invoke(0, $"{operation} started");
171+
172+
int bytesRead;
173+
while ((bytesRead = await inputStream.ReadAsync(buffer, cancellationToken)) > 0)
174+
{
175+
await outputStream.WriteAsync(buffer.AsMemory(0, bytesRead), cancellationToken);
176+
totalBytesProcessed += bytesRead;
177+
178+
if (totalBytes.HasValue && totalBytes.Value > 0 && progressCallback != null)
179+
{
180+
var progressPercentage = (int)(totalBytesProcessed * 100 / totalBytes.Value);
181+
182+
if (progressPercentage != lastReportedProgress && progressPercentage % ProgressReportInterval == 0)
183+
{
184+
progressCallback.Invoke(progressPercentage,
185+
$"{operation} {progressPercentage}% - {FormatBytes(totalBytesProcessed)} of {FormatBytes(totalBytes.Value)}");
186+
lastReportedProgress = progressPercentage;
187+
}
188+
}
189+
else if (progressCallback != null)
190+
{
191+
progressCallback.Invoke(0, $"{operation} - {FormatBytes(totalBytesProcessed)} processed");
192+
}
193+
}
194+
195+
if (outputStream is not CryptoStream)
196+
{
197+
await outputStream.FlushAsync(cancellationToken);
198+
}
199+
200+
progressCallback?.Invoke(100, $"{operation} completed - {FormatBytes(totalBytesProcessed)} processed");
201+
}
202+
203+
private static string FormatBytes(long bytes)
204+
{
205+
const long kb = 1024;
206+
const long mb = kb * 1024;
207+
const long gb = mb * 1024;
208+
209+
return bytes switch
210+
{
211+
>= gb => $"{bytes / (double)gb:F2} GB",
212+
>= mb => $"{bytes / (double)mb:F2} MB",
213+
>= kb => $"{bytes / (double)kb:F2} KB",
214+
_ => $"{bytes} bytes"
215+
};
216+
}
217+
}

src/KeeperData.Infrastructure/Crypto/PasswordSaltService.cs

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -102,15 +102,6 @@ private string GeneratePassword(FileNameComponents components)
102102

103103
var password = $"{part1}_{part2}";
104104

105-
if (!string.IsNullOrEmpty(components.AfterDate))
106-
{
107-
var afterDateParts = components.AfterDate.Split('_');
108-
Array.Reverse(afterDateParts);
109-
password += "_" + string.Join("_", afterDateParts);
110-
}
111-
112-
password += components.FileExtension;
113-
114105
return password;
115106
}
116107

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using KeeperData.Core.Crypto;
2+
using Microsoft.Extensions.Configuration;
3+
using Microsoft.Extensions.DependencyInjection;
4+
using Microsoft.Extensions.DependencyInjection.Extensions;
5+
using System.Diagnostics.CodeAnalysis;
6+
7+
namespace KeeperData.Infrastructure.Crypto;
8+
9+
[ExcludeFromCodeCoverage]
10+
public static class ServiceCollectionExtensions
11+
{
12+
public static void AddCrypto(this IServiceCollection services, IConfiguration configuration)
13+
{
14+
services.TryAddSingleton(TimeProvider.System);
15+
services.TryAddSingleton<IPasswordSaltService, PasswordSaltService>();
16+
services.TryAddSingleton<IAesCryptoTransform, AesCryptoTransform>();
17+
}
18+
19+
}

0 commit comments

Comments
 (0)