Skip to content

Commit 4f00382

Browse files
committed
feat(security): rotazione chiavi
Aggiunge il supporto al key-ring AES-GCM con id chiave nei token (v2) e nei blob (magic header), mantenendo la decifratura dei dati v1 legacy. Nuovo progetto Accanto.Cli con comandi 'generate-key' e 'rotate-keys' che riscrivono i campi cifrati del DB e i file dei documenti con la chiave attiva.
1 parent 41fb62a commit 4f00382

12 files changed

Lines changed: 573 additions & 50 deletions

File tree

backend/Accanto.slnx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
<Folder Name="/src/">
33
<Project Path="src/Accanto.Api/Accanto.Api.csproj" />
44
<Project Path="src/Accanto.Application/Accanto.Application.csproj" />
5+
<Project Path="src/Accanto.Cli/Accanto.Cli.csproj" />
56
<Project Path="src/Accanto.Domain/Accanto.Domain.csproj" />
67
<Project Path="src/Accanto.Infrastructure/Accanto.Infrastructure.csproj" />
78
</Folder>

backend/src/Accanto.Application/Common/Storage/IFileStorage.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ public interface IFileStorage
55
Task<StoredFile> SaveAsync(Stream content, string originalFileName, string contentType, CancellationToken cancellationToken = default);
66
Task<Stream> OpenReadAsync(string relativePath, CancellationToken cancellationToken = default);
77
Task DeleteAsync(string relativePath, CancellationToken cancellationToken = default);
8+
9+
/// <summary>
10+
/// Decifra il file con il key-ring corrente e lo riscrive in-place cifrato con la chiave attiva.
11+
/// Usato dalla CLI di rotazione: non da invocare dal codice applicativo.
12+
/// </summary>
13+
Task RewriteWithActiveKeyAsync(string relativePath, CancellationToken cancellationToken = default);
814
}
915

1016
public sealed record StoredFile(string InternalFileName, string RelativePath, long SizeInBytes);
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net10.0</TargetFramework>
6+
<Nullable>enable</Nullable>
7+
<ImplicitUsings>enable</ImplicitUsings>
8+
<RootNamespace>Accanto.Cli</RootNamespace>
9+
<AssemblyName>accanto</AssemblyName>
10+
</PropertyGroup>
11+
12+
<ItemGroup>
13+
<PackageReference Include="Microsoft.Extensions.Configuration" Version="10.0.4" />
14+
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="10.0.4" />
15+
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.4" />
16+
<PackageReference Include="Microsoft.Extensions.Configuration.CommandLine" Version="10.0.4" />
17+
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.4" />
18+
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.4" />
19+
</ItemGroup>
20+
21+
<ItemGroup>
22+
<ProjectReference Include="..\Accanto.Infrastructure\Accanto.Infrastructure.csproj" />
23+
</ItemGroup>
24+
25+
</Project>

backend/src/Accanto.Cli/Program.cs

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
using System.Security.Cryptography;
2+
using Accanto.Infrastructure;
3+
using Accanto.Infrastructure.Persistence;
4+
using Accanto.Infrastructure.Security;
5+
using Microsoft.EntityFrameworkCore;
6+
using Microsoft.Extensions.Configuration;
7+
using Microsoft.Extensions.DependencyInjection;
8+
9+
namespace Accanto.Cli;
10+
11+
/// <summary>
12+
/// CLI di amministrazione Accanto. Comandi disponibili:
13+
///
14+
/// accanto generate-key
15+
/// Stampa una nuova chiave AES-256 in base64.
16+
///
17+
/// accanto rotate-keys
18+
/// Riscrive tutti i campi cifrati e tutti i file dei documenti usando la chiave
19+
/// "attiva" (Encryption:ActiveKeyId). Le vecchie chiavi devono restare configurate
20+
/// in Encryption:Keys (oppure Encryption:MasterKey per il formato legacy v1)
21+
/// finche' la rotazione non e' completa.
22+
/// </summary>
23+
public static class Program
24+
{
25+
public static async Task<int> Main(string[] args)
26+
{
27+
if (args.Length == 0)
28+
{
29+
PrintUsage();
30+
return 1;
31+
}
32+
33+
var command = args[0];
34+
var rest = args.Skip(1).ToArray();
35+
36+
try
37+
{
38+
return command switch
39+
{
40+
"generate-key" => GenerateKey(),
41+
"rotate-keys" => await RotateKeysAsync(rest),
42+
"--help" or "-h" or "help" => PrintUsage(),
43+
_ => UnknownCommand(command),
44+
};
45+
}
46+
catch (Exception ex)
47+
{
48+
Console.Error.WriteLine($"Errore: {ex.Message}");
49+
return 2;
50+
}
51+
}
52+
53+
private static int GenerateKey()
54+
{
55+
var key = new byte[32];
56+
RandomNumberGenerator.Fill(key);
57+
Console.WriteLine(Convert.ToBase64String(key));
58+
return 0;
59+
}
60+
61+
private static async Task<int> RotateKeysAsync(string[] args)
62+
{
63+
var config = new ConfigurationBuilder()
64+
.SetBasePath(Directory.GetCurrentDirectory())
65+
.AddJsonFile("appsettings.json", optional: true)
66+
.AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT") ?? "Production"}.json", optional: true)
67+
.AddEnvironmentVariables()
68+
.AddCommandLine(args)
69+
.Build();
70+
71+
var services = new ServiceCollection();
72+
services.AddSingleton<IConfiguration>(config);
73+
services.AddLogging();
74+
services.AddAccantoInfrastructure(config);
75+
services.AddScoped<KeyRotationService>();
76+
77+
await using var provider = services.BuildServiceProvider();
78+
79+
var protector = (AesGcmFieldProtector)provider.GetRequiredService<Accanto.Application.Common.Security.IFieldProtector>();
80+
if (protector.ActiveKeyId is null)
81+
{
82+
Console.Error.WriteLine(
83+
"Nessuna ActiveKeyId configurata: imposta Encryption:ActiveKeyId e Encryption:Keys "
84+
+ "(la chiave legacy va lasciata in Encryption:MasterKey finche' tutti i dati v1 non sono ruotati).");
85+
return 3;
86+
}
87+
88+
Console.WriteLine($"Avvio rotazione verso la chiave attiva '{protector.ActiveKeyId}'.");
89+
using var scope = provider.CreateScope();
90+
var db = scope.ServiceProvider.GetRequiredService<AccantoDbContext>();
91+
await db.Database.MigrateAsync();
92+
93+
var rotator = scope.ServiceProvider.GetRequiredService<KeyRotationService>();
94+
var report = await rotator.RotateAsync();
95+
96+
Console.WriteLine("Rotazione completata:");
97+
Console.WriteLine($" cerchi di cura : {report.CareCircles}");
98+
Console.WriteLine($" voci di diario : {report.TimelineEntries}");
99+
Console.WriteLine($" domande per medico : {report.DoctorQuestions}");
100+
Console.WriteLine($" aggiornamenti : {report.SharedUpdates}");
101+
Console.WriteLine($" documenti (DB) : {report.MedicalDocumentRows}");
102+
Console.WriteLine($" documenti (file) : {report.MedicalDocumentFiles}");
103+
if (report.MissingFiles.Count > 0)
104+
{
105+
Console.WriteLine($" file mancanti : {report.MissingFiles.Count}");
106+
foreach (var f in report.MissingFiles)
107+
Console.WriteLine($" - {f}");
108+
}
109+
return 0;
110+
}
111+
112+
private static int UnknownCommand(string command)
113+
{
114+
Console.Error.WriteLine($"Comando sconosciuto: {command}");
115+
PrintUsage();
116+
return 1;
117+
}
118+
119+
private static int PrintUsage()
120+
{
121+
Console.WriteLine("Uso: accanto <comando>");
122+
Console.WriteLine();
123+
Console.WriteLine("Comandi:");
124+
Console.WriteLine(" generate-key Stampa una nuova chiave AES-256 in base64.");
125+
Console.WriteLine(" rotate-keys Riscrive i dati cifrati usando Encryption:ActiveKeyId.");
126+
Console.WriteLine(" help Mostra questo messaggio.");
127+
return 0;
128+
}
129+
}

0 commit comments

Comments
 (0)