using System.Security.Cryptography; using System.Text; namespace Journal.Core.Services.Vault; public class VaultCryptoService : IVaultCryptoService { public const int SaltSize = 16; public const int KeySize = 32; public const int NonceSize = 12; public const int TagSize = 16; public const int Iterations = 600_000; public byte[] DeriveKey(string password, byte[] salt) { if (string.IsNullOrWhiteSpace(password)) throw new ArgumentException("Password cannot be empty.", nameof(password)); ArgumentNullException.ThrowIfNull(salt); if (salt.Length != SaltSize) throw new ArgumentException($"Salt must be {SaltSize} bytes.", nameof(salt)); return Rfc2898DeriveBytes.Pbkdf2( Encoding.UTF8.GetBytes(password), salt, Iterations, HashAlgorithmName.SHA256, KeySize); } public byte[] EncryptData(byte[] data, string password) { ArgumentNullException.ThrowIfNull(data); var salt = RandomNumberGenerator.GetBytes(SaltSize); var nonce = RandomNumberGenerator.GetBytes(NonceSize); return EncryptData(data, password, salt, nonce); } public byte[] DecryptData(byte[] encryptedData, string password) { ArgumentNullException.ThrowIfNull(encryptedData); var minLength = SaltSize + NonceSize + TagSize; if (encryptedData.Length < minLength) throw new ArgumentException("Encrypted payload is too short.", nameof(encryptedData)); var salt = encryptedData.AsSpan(0, SaltSize).ToArray(); var nonce = encryptedData.AsSpan(SaltSize, NonceSize).ToArray(); var tag = encryptedData.AsSpan(SaltSize + NonceSize, TagSize).ToArray(); var ciphertext = encryptedData.AsSpan(SaltSize + NonceSize + TagSize).ToArray(); var key = DeriveKey(password, salt); var plaintext = new byte[ciphertext.Length]; using var aes = new AesGcm(key, TagSize); aes.Decrypt(nonce, ciphertext, tag, plaintext); return plaintext; } public byte[] EncryptData(byte[] data, string password, byte[] salt, byte[] nonce) { ArgumentNullException.ThrowIfNull(data); ArgumentNullException.ThrowIfNull(salt); ArgumentNullException.ThrowIfNull(nonce); if (salt.Length != SaltSize) throw new ArgumentException($"Salt must be {SaltSize} bytes.", nameof(salt)); if (nonce.Length != NonceSize) throw new ArgumentException($"Nonce must be {NonceSize} bytes.", nameof(nonce)); var key = DeriveKey(password, salt); var ciphertext = new byte[data.Length]; var tag = new byte[TagSize]; using var aes = new AesGcm(key, TagSize); aes.Encrypt(nonce, data, ciphertext, tag); var payload = new byte[SaltSize + NonceSize + TagSize + ciphertext.Length]; Buffer.BlockCopy(salt, 0, payload, 0, SaltSize); Buffer.BlockCopy(nonce, 0, payload, SaltSize, NonceSize); Buffer.BlockCopy(tag, 0, payload, SaltSize + NonceSize, TagSize); Buffer.BlockCopy(ciphertext, 0, payload, SaltSize + NonceSize + TagSize, ciphertext.Length); return payload; } }