- Move standalone fragment storage from unencrypted SQLite to the existing encrypted SQLCipher database (journal_cache.db) - Add IDatabaseSessionService/DatabaseSessionService for shared encrypted connection management after authentication - Update fragments table schema: nullable entry_id, add guid column - Reorganize flat Services/ directory (28 files) into 9 domain modules: Ai, Config, Database, Entries, Fragments, Logging, Sidecar, Speech, Vault - Update all namespace declarations and using statements across all projects - Update REFACTORING_SUMMARY.md with all changes Co-Authored-By: Warp <agent@warp.dev>
84 lines
3.2 KiB
C#
84 lines
3.2 KiB
C#
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;
|
|
}
|
|
}
|