using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
namespace PalladiumWallet.Core.Storage;
///
/// Wallet file encryption (blueprint §8/§17): AES-256-GCM with a key derived
/// from the password via PBKDF2-HMAC-SHA512. The container is self-describing JSON
/// (kdf, parameters, salt, nonce) to allow future parameter upgrades.
///
public static class EncryptedFile
{
private const string Format = "plm-wallet-aesgcm-v1";
private const int DefaultIterations = 600_000;
private const int SaltSize = 16;
private const int NonceSize = 12;
private const int TagSize = 16;
private const int KeySize = 32;
private sealed record Container(
string Format, int Iterations, string Salt, string Nonce, string Tag, string Data);
public static bool IsEncrypted(string fileContent)
{
try
{
using var doc = JsonDocument.Parse(fileContent);
return doc.RootElement.TryGetProperty("Format", out var f)
&& f.GetString() == Format;
}
catch (JsonException)
{
return false;
}
}
public static string Encrypt(string plaintext, string password)
{
var salt = RandomNumberGenerator.GetBytes(SaltSize);
var nonce = RandomNumberGenerator.GetBytes(NonceSize);
var key = DeriveKey(password, salt, DefaultIterations);
var plainBytes = Encoding.UTF8.GetBytes(plaintext);
var cipher = new byte[plainBytes.Length];
var tag = new byte[TagSize];
using (var aes = new AesGcm(key, TagSize))
aes.Encrypt(nonce, plainBytes, cipher, tag);
CryptographicOperations.ZeroMemory(key);
return JsonSerializer.Serialize(new Container(
Format, DefaultIterations,
Convert.ToBase64String(salt), Convert.ToBase64String(nonce),
Convert.ToBase64String(tag), Convert.ToBase64String(cipher)));
}
/// Throws if the password is wrong or the file is tampered with.
public static string Decrypt(string fileContent, string password)
{
var container = JsonSerializer.Deserialize(fileContent)
?? throw new InvalidDataException("Contenitore cifrato non valido.");
if (container.Format != Format)
throw new InvalidDataException($"Formato sconosciuto: {container.Format}");
var key = DeriveKey(password, Convert.FromBase64String(container.Salt), container.Iterations);
var cipher = Convert.FromBase64String(container.Data);
var plain = new byte[cipher.Length];
try
{
using var aes = new AesGcm(key, TagSize);
aes.Decrypt(
Convert.FromBase64String(container.Nonce), cipher,
Convert.FromBase64String(container.Tag), plain);
}
catch (AuthenticationTagMismatchException)
{
// The GCM tag authenticates: a wrong password and tampering are indistinguishable.
throw new WrongPasswordException();
}
finally
{
CryptographicOperations.ZeroMemory(key);
}
return Encoding.UTF8.GetString(plain);
}
private static byte[] DeriveKey(string password, byte[] salt, int iterations) =>
Rfc2898DeriveBytes.Pbkdf2(password, salt, iterations, HashAlgorithmName.SHA512, KeySize);
}
/// Wrong password (or tampered wallet file: the GCM tag does not distinguish the two cases).
public sealed class WrongPasswordException : Exception
{
public WrongPasswordException() : base("Password errata o file wallet danneggiato.") { }
}