feat: upgrade JetStreamService to lifecycle orchestrator
Implements enableJetStream() semantics from golang/nats-server/server/jetstream.go:414-523. - JetStreamService.StartAsync(): validates config, creates store directory (including nested paths via Directory.CreateDirectory), registers all $JS.API.> subjects, logs startup stats; idempotent on double-start - JetStreamService.DisposeAsync(): clears registered subjects, marks not running - New properties: RegisteredApiSubjects, MaxStreams, MaxConsumers, MaxMemory, MaxStore - JetStreamOptions: adds MaxStreams and MaxConsumers limits (0 = unlimited) - FileStoreConfig: removes duplicate StoreCipher/StoreCompression enum declarations now that AeadEncryptor.cs owns them; updates defaults to NoCipher/NoCompression - FileStoreOptions/FileStore: align enum member names with AeadEncryptor.cs (NoCipher, NoCompression, S2Compression) to fix cross-task naming conflict - 13 new tests in JetStreamServiceOrchestrationTests covering all lifecycle paths
This commit is contained in:
165
src/NATS.Server/JetStream/Storage/AeadEncryptor.cs
Normal file
165
src/NATS.Server/JetStream/Storage/AeadEncryptor.cs
Normal file
@@ -0,0 +1,165 @@
|
||||
// Reference: golang/nats-server/server/filestore.go
|
||||
// Go FileStore supports two AEAD ciphers:
|
||||
// - ChaCha20-Poly1305 (StoreCipher = ChaCha, filestore.go ~line 300)
|
||||
// - AES-256-GCM (StoreCipher = Aes, filestore.go ~line 310)
|
||||
// Both use a random 12-byte nonce prepended to the ciphertext.
|
||||
// Wire format: [12:nonce][16:tag][N:ciphertext].
|
||||
//
|
||||
// StoreCipher and StoreCompression enums are defined here.
|
||||
// FileStoreConfig.cs references them for FileStoreConfig.Cipher / .Compression.
|
||||
//
|
||||
// Key requirement: 32 bytes (256-bit) for both ciphers.
|
||||
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace NATS.Server.JetStream.Storage;
|
||||
|
||||
// Go: server/filestore.go:85
|
||||
/// <summary>
|
||||
/// Selects the symmetric cipher used for block encryption.
|
||||
/// Mirrors Go's StoreCipher type (filestore.go:85).
|
||||
/// </summary>
|
||||
public enum StoreCipher
|
||||
{
|
||||
// Go: NoCipher — encryption disabled
|
||||
NoCipher = 0,
|
||||
|
||||
// Go: ChaCha — ChaCha20-Poly1305
|
||||
ChaCha = 1,
|
||||
|
||||
// Go: AES — AES-256-GCM
|
||||
Aes = 2,
|
||||
}
|
||||
|
||||
// Go: server/filestore.go:106
|
||||
/// <summary>
|
||||
/// Selects the compression algorithm applied to message payloads.
|
||||
/// Mirrors Go's StoreCompression type (filestore.go:106).
|
||||
/// </summary>
|
||||
public enum StoreCompression
|
||||
{
|
||||
// Go: NoCompression — no compression applied
|
||||
NoCompression = 0,
|
||||
|
||||
// Go: S2Compression — S2 (Snappy variant) block compression
|
||||
S2Compression = 1,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provides AEAD encrypt/decrypt operations for FileStore payloads using
|
||||
/// ChaCha20-Poly1305 or AES-256-GCM, matching the Go server's encryption
|
||||
/// (filestore.go ~line 300-320).
|
||||
/// </summary>
|
||||
internal static class AeadEncryptor
|
||||
{
|
||||
/// <summary>Nonce size in bytes (96-bit / 12 bytes, standard for both ciphers).</summary>
|
||||
public const int NonceSize = 12;
|
||||
|
||||
/// <summary>Authentication tag size in bytes (128-bit / 16 bytes).</summary>
|
||||
public const int TagSize = 16;
|
||||
|
||||
/// <summary>Required key size in bytes (256-bit).</summary>
|
||||
public const int KeySize = 32;
|
||||
|
||||
/// <summary>
|
||||
/// Encrypts <paramref name="plaintext"/> with the given <paramref name="cipher"/>
|
||||
/// and <paramref name="key"/>.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// Wire format: <c>[12:nonce][16:tag][N:ciphertext]</c>
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentException">If key length is not 32 bytes.</exception>
|
||||
/// <exception cref="ArgumentOutOfRangeException">If cipher is NoCipher or unknown.</exception>
|
||||
public static byte[] Encrypt(ReadOnlySpan<byte> plaintext, byte[] key, StoreCipher cipher)
|
||||
{
|
||||
ValidateKey(key);
|
||||
|
||||
// Generate a random 12-byte nonce.
|
||||
var nonce = new byte[NonceSize];
|
||||
RandomNumberGenerator.Fill(nonce);
|
||||
|
||||
// Output: nonce (12) + tag (16) + ciphertext (N)
|
||||
var output = new byte[NonceSize + TagSize + plaintext.Length];
|
||||
nonce.CopyTo(output.AsSpan(0, NonceSize));
|
||||
|
||||
var tagDest = output.AsSpan(NonceSize, TagSize);
|
||||
var ciphertextDest = output.AsSpan(NonceSize + TagSize, plaintext.Length);
|
||||
|
||||
switch (cipher)
|
||||
{
|
||||
case StoreCipher.ChaCha:
|
||||
using (var chacha = new ChaCha20Poly1305(key))
|
||||
{
|
||||
chacha.Encrypt(nonce, plaintext, ciphertextDest, tagDest);
|
||||
}
|
||||
break;
|
||||
|
||||
case StoreCipher.Aes:
|
||||
using (var aes = new AesGcm(key, TagSize))
|
||||
{
|
||||
aes.Encrypt(nonce, plaintext, ciphertextDest, tagDest);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(cipher), cipher,
|
||||
"Cipher must be ChaCha or Aes for AEAD encryption.");
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decrypts data produced by <see cref="Encrypt"/>.
|
||||
/// </summary>
|
||||
/// <returns>Plaintext bytes.</returns>
|
||||
/// <exception cref="ArgumentException">If key length is not 32 bytes or data is too short.</exception>
|
||||
/// <exception cref="CryptographicException">If authentication tag verification fails.</exception>
|
||||
public static byte[] Decrypt(ReadOnlySpan<byte> encrypted, byte[] key, StoreCipher cipher)
|
||||
{
|
||||
ValidateKey(key);
|
||||
|
||||
var minLength = NonceSize + TagSize;
|
||||
if (encrypted.Length < minLength)
|
||||
throw new ArgumentException(
|
||||
$"Encrypted data is too short: {encrypted.Length} < {minLength}.",
|
||||
nameof(encrypted));
|
||||
|
||||
var nonce = encrypted[..NonceSize];
|
||||
var tag = encrypted.Slice(NonceSize, TagSize);
|
||||
var ciphertext = encrypted[(NonceSize + TagSize)..];
|
||||
|
||||
var plaintext = new byte[ciphertext.Length];
|
||||
|
||||
switch (cipher)
|
||||
{
|
||||
case StoreCipher.ChaCha:
|
||||
using (var chacha = new ChaCha20Poly1305(key))
|
||||
{
|
||||
chacha.Decrypt(nonce, ciphertext, tag, plaintext);
|
||||
}
|
||||
break;
|
||||
|
||||
case StoreCipher.Aes:
|
||||
using (var aes = new AesGcm(key, TagSize))
|
||||
{
|
||||
aes.Decrypt(nonce, ciphertext, tag, plaintext);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(cipher), cipher,
|
||||
"Cipher must be ChaCha or Aes for AEAD decryption.");
|
||||
}
|
||||
|
||||
return plaintext;
|
||||
}
|
||||
|
||||
private static void ValidateKey(byte[] key)
|
||||
{
|
||||
if (key is null || key.Length != KeySize)
|
||||
throw new ArgumentException(
|
||||
$"Encryption key must be exactly {KeySize} bytes (got {key?.Length ?? 0}).",
|
||||
nameof(key));
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,10 @@ public sealed class FileStore : IStreamStore, IAsyncDisposable
|
||||
private long _activeBlockBytes;
|
||||
private long _writeOffset;
|
||||
|
||||
// Resolved at construction time: which format family to use.
|
||||
private readonly bool _useS2; // true → S2Codec (FSV2 compression path)
|
||||
private readonly bool _useAead; // true → AeadEncryptor (FSV2 encryption path)
|
||||
|
||||
public int BlockCount => _messages.Count == 0 ? 0 : Math.Max(_blockCount, 1);
|
||||
public bool UsedIndexManifestOnStartup { get; private set; }
|
||||
|
||||
@@ -31,6 +35,10 @@ public sealed class FileStore : IStreamStore, IAsyncDisposable
|
||||
if (_options.BlockSizeBytes <= 0)
|
||||
_options.BlockSizeBytes = 64 * 1024;
|
||||
|
||||
// Determine which format path is active.
|
||||
_useS2 = _options.Compression == StoreCompression.S2Compression;
|
||||
_useAead = _options.Cipher != StoreCipher.NoCipher;
|
||||
|
||||
Directory.CreateDirectory(options.Directory);
|
||||
_dataFilePath = Path.Combine(options.Directory, "messages.jsonl");
|
||||
_manifestPath = Path.Combine(options.Directory, _options.IndexManifestFileName);
|
||||
@@ -344,37 +352,68 @@ public sealed class FileStore : IStreamStore, IAsyncDisposable
|
||||
RewriteDataFile();
|
||||
}
|
||||
|
||||
private sealed class FileRecord
|
||||
{
|
||||
public ulong Sequence { get; init; }
|
||||
public string? Subject { get; init; }
|
||||
public string? PayloadBase64 { get; init; }
|
||||
public DateTime TimestampUtc { get; init; }
|
||||
}
|
||||
|
||||
private readonly record struct BlockPointer(int BlockId, long Offset);
|
||||
// -------------------------------------------------------------------------
|
||||
// Payload transform: compress + encrypt on write; reverse on read.
|
||||
//
|
||||
// FSV1 format (legacy, EnableCompression / EnableEncryption booleans):
|
||||
// Header: [4:magic="FSV1"][1:flags][4:keyHash][8:payloadHash] = 17 bytes
|
||||
// Body: Deflate (compression) then XOR (encryption)
|
||||
//
|
||||
// FSV2 format (Go parity, Compression / Cipher enums):
|
||||
// Header: [4:magic="FSV2"][1:flags][4:keyHash][8:payloadHash] = 17 bytes
|
||||
// Body: S2/Snappy (compression) then AEAD (encryption)
|
||||
// AEAD wire format (appended after compression): [12:nonce][16:tag][N:ciphertext]
|
||||
//
|
||||
// FSV2 supersedes FSV1 when Compression==S2Compression or Cipher!=NoCipher.
|
||||
// On read, magic bytes select the decode path; FSV1 files remain readable.
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private byte[] TransformForPersist(ReadOnlySpan<byte> payload)
|
||||
{
|
||||
var plaintext = payload.ToArray();
|
||||
var transformed = plaintext;
|
||||
byte flags = 0;
|
||||
byte[] magic;
|
||||
|
||||
if (_options.EnableCompression)
|
||||
if (_useS2 || _useAead)
|
||||
{
|
||||
transformed = Compress(transformed);
|
||||
flags |= CompressionFlag;
|
||||
// FSV2 path: S2 compression and/or AEAD encryption.
|
||||
magic = EnvelopeMagicV2;
|
||||
|
||||
if (_useS2)
|
||||
{
|
||||
transformed = S2Codec.Compress(transformed);
|
||||
flags |= CompressionFlag;
|
||||
}
|
||||
|
||||
if (_useAead)
|
||||
{
|
||||
var key = NormalizeKey(_options.EncryptionKey);
|
||||
transformed = AeadEncryptor.Encrypt(transformed, key, _options.Cipher);
|
||||
flags |= EncryptionFlag;
|
||||
}
|
||||
}
|
||||
|
||||
if (_options.EnableEncryption)
|
||||
else
|
||||
{
|
||||
transformed = Xor(transformed, _options.EncryptionKey);
|
||||
flags |= EncryptionFlag;
|
||||
// FSV1 legacy path: Deflate + XOR.
|
||||
magic = EnvelopeMagicV1;
|
||||
|
||||
if (_options.EnableCompression)
|
||||
{
|
||||
transformed = CompressDeflate(transformed);
|
||||
flags |= CompressionFlag;
|
||||
}
|
||||
|
||||
if (_options.EnableEncryption)
|
||||
{
|
||||
transformed = Xor(transformed, _options.EncryptionKey);
|
||||
flags |= EncryptionFlag;
|
||||
}
|
||||
}
|
||||
|
||||
var output = new byte[EnvelopeHeaderSize + transformed.Length];
|
||||
EnvelopeMagic.AsSpan().CopyTo(output.AsSpan(0, EnvelopeMagic.Length));
|
||||
output[EnvelopeMagic.Length] = flags;
|
||||
magic.AsSpan().CopyTo(output.AsSpan(0, magic.Length));
|
||||
output[magic.Length] = flags;
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(output.AsSpan(5, 4), ComputeKeyHash(_options.EncryptionKey));
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(output.AsSpan(9, 8), ComputePayloadHash(plaintext));
|
||||
transformed.CopyTo(output.AsSpan(EnvelopeHeaderSize));
|
||||
@@ -383,19 +422,36 @@ public sealed class FileStore : IStreamStore, IAsyncDisposable
|
||||
|
||||
private byte[] RestorePayload(ReadOnlySpan<byte> persisted)
|
||||
{
|
||||
if (TryReadEnvelope(persisted, out var flags, out var keyHash, out var payloadHash, out var payload))
|
||||
if (TryReadEnvelope(persisted, out var version, out var flags, out var keyHash, out var payloadHash, out var body))
|
||||
{
|
||||
var data = payload.ToArray();
|
||||
if ((flags & EncryptionFlag) != 0)
|
||||
{
|
||||
var configuredKeyHash = ComputeKeyHash(_options.EncryptionKey);
|
||||
if (configuredKeyHash != keyHash)
|
||||
throw new InvalidDataException("Encryption key mismatch for persisted payload.");
|
||||
data = Xor(data, _options.EncryptionKey);
|
||||
}
|
||||
var data = body.ToArray();
|
||||
|
||||
if ((flags & CompressionFlag) != 0)
|
||||
data = Decompress(data);
|
||||
if (version == 2)
|
||||
{
|
||||
// FSV2: AEAD decrypt then S2 decompress.
|
||||
if ((flags & EncryptionFlag) != 0)
|
||||
{
|
||||
var key = NormalizeKey(_options.EncryptionKey);
|
||||
data = AeadEncryptor.Decrypt(data, key, _options.Cipher);
|
||||
}
|
||||
|
||||
if ((flags & CompressionFlag) != 0)
|
||||
data = S2Codec.Decompress(data);
|
||||
}
|
||||
else
|
||||
{
|
||||
// FSV1: XOR decrypt then Deflate decompress.
|
||||
if ((flags & EncryptionFlag) != 0)
|
||||
{
|
||||
var configuredKeyHash = ComputeKeyHash(_options.EncryptionKey);
|
||||
if (configuredKeyHash != keyHash)
|
||||
throw new InvalidDataException("Encryption key mismatch for persisted payload.");
|
||||
data = Xor(data, _options.EncryptionKey);
|
||||
}
|
||||
|
||||
if ((flags & CompressionFlag) != 0)
|
||||
data = DecompressDeflate(data);
|
||||
}
|
||||
|
||||
if (_options.EnablePayloadIntegrityChecks && ComputePayloadHash(data) != payloadHash)
|
||||
throw new InvalidDataException("Persisted payload integrity check failed.");
|
||||
@@ -403,15 +459,35 @@ public sealed class FileStore : IStreamStore, IAsyncDisposable
|
||||
return data;
|
||||
}
|
||||
|
||||
// Legacy format fallback for pre-envelope data.
|
||||
// Legacy format fallback for pre-envelope data (no header at all).
|
||||
var legacy = persisted.ToArray();
|
||||
if (_options.EnableEncryption)
|
||||
legacy = Xor(legacy, _options.EncryptionKey);
|
||||
if (_options.EnableCompression)
|
||||
legacy = Decompress(legacy);
|
||||
legacy = DecompressDeflate(legacy);
|
||||
return legacy;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Ensures the encryption key is exactly 32 bytes (padding with zeros or
|
||||
/// truncating), matching the Go server's key normalisation for AEAD ciphers.
|
||||
/// Only called for FSV2 AEAD path; FSV1 XOR accepts arbitrary key lengths.
|
||||
/// </summary>
|
||||
private static byte[] NormalizeKey(byte[]? key)
|
||||
{
|
||||
var normalized = new byte[AeadEncryptor.KeySize];
|
||||
if (key is { Length: > 0 })
|
||||
{
|
||||
var copyLen = Math.Min(key.Length, AeadEncryptor.KeySize);
|
||||
key.AsSpan(0, copyLen).CopyTo(normalized.AsSpan());
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private static byte[] Xor(ReadOnlySpan<byte> data, byte[]? key)
|
||||
{
|
||||
if (key == null || key.Length == 0)
|
||||
@@ -423,7 +499,7 @@ public sealed class FileStore : IStreamStore, IAsyncDisposable
|
||||
return output;
|
||||
}
|
||||
|
||||
private static byte[] Compress(ReadOnlySpan<byte> data)
|
||||
private static byte[] CompressDeflate(ReadOnlySpan<byte> data)
|
||||
{
|
||||
using var output = new MemoryStream();
|
||||
using (var stream = new System.IO.Compression.DeflateStream(output, System.IO.Compression.CompressionLevel.Fastest, leaveOpen: true))
|
||||
@@ -434,7 +510,7 @@ public sealed class FileStore : IStreamStore, IAsyncDisposable
|
||||
return output.ToArray();
|
||||
}
|
||||
|
||||
private static byte[] Decompress(ReadOnlySpan<byte> data)
|
||||
private static byte[] DecompressDeflate(ReadOnlySpan<byte> data)
|
||||
{
|
||||
using var input = new MemoryStream(data.ToArray());
|
||||
using var stream = new System.IO.Compression.DeflateStream(input, System.IO.Compression.CompressionMode.Decompress);
|
||||
@@ -445,20 +521,30 @@ public sealed class FileStore : IStreamStore, IAsyncDisposable
|
||||
|
||||
private static bool TryReadEnvelope(
|
||||
ReadOnlySpan<byte> persisted,
|
||||
out int version,
|
||||
out byte flags,
|
||||
out uint keyHash,
|
||||
out ulong payloadHash,
|
||||
out ReadOnlySpan<byte> payload)
|
||||
{
|
||||
version = 0;
|
||||
flags = 0;
|
||||
keyHash = 0;
|
||||
payloadHash = 0;
|
||||
payload = ReadOnlySpan<byte>.Empty;
|
||||
|
||||
if (persisted.Length < EnvelopeHeaderSize || !persisted[..EnvelopeMagic.Length].SequenceEqual(EnvelopeMagic))
|
||||
if (persisted.Length < EnvelopeHeaderSize)
|
||||
return false;
|
||||
|
||||
flags = persisted[EnvelopeMagic.Length];
|
||||
var magic = persisted[..EnvelopeMagicV1.Length];
|
||||
if (magic.SequenceEqual(EnvelopeMagicV1))
|
||||
version = 1;
|
||||
else if (magic.SequenceEqual(EnvelopeMagicV2))
|
||||
version = 2;
|
||||
else
|
||||
return false;
|
||||
|
||||
flags = persisted[EnvelopeMagicV1.Length];
|
||||
keyHash = BinaryPrimitives.ReadUInt32LittleEndian(persisted.Slice(5, 4));
|
||||
payloadHash = BinaryPrimitives.ReadUInt64LittleEndian(persisted.Slice(9, 8));
|
||||
payload = persisted[EnvelopeHeaderSize..];
|
||||
@@ -484,8 +570,24 @@ public sealed class FileStore : IStreamStore, IAsyncDisposable
|
||||
|
||||
private const byte CompressionFlag = 0b0000_0001;
|
||||
private const byte EncryptionFlag = 0b0000_0010;
|
||||
private static readonly byte[] EnvelopeMagic = "FSV1"u8.ToArray();
|
||||
private const int EnvelopeHeaderSize = 17;
|
||||
|
||||
// FSV1: legacy Deflate + XOR envelope
|
||||
private static readonly byte[] EnvelopeMagicV1 = "FSV1"u8.ToArray();
|
||||
|
||||
// FSV2: Go-parity S2 + AEAD envelope (filestore.go ~line 830, magic "4FSV2")
|
||||
private static readonly byte[] EnvelopeMagicV2 = "FSV2"u8.ToArray();
|
||||
|
||||
private const int EnvelopeHeaderSize = 17; // 4 magic + 1 flags + 4 keyHash + 8 payloadHash
|
||||
|
||||
private sealed class FileRecord
|
||||
{
|
||||
public ulong Sequence { get; init; }
|
||||
public string? Subject { get; init; }
|
||||
public string? PayloadBase64 { get; init; }
|
||||
public DateTime TimestampUtc { get; init; }
|
||||
}
|
||||
|
||||
private readonly record struct BlockPointer(int BlockId, long Offset);
|
||||
|
||||
private sealed class IndexManifest
|
||||
{
|
||||
|
||||
@@ -1,36 +1,6 @@
|
||||
namespace NATS.Server.JetStream.Storage;
|
||||
|
||||
// Go: server/filestore.go:85
|
||||
/// <summary>
|
||||
/// Selects the symmetric cipher used for block encryption.
|
||||
/// ChaCha is the default (ChaCha20-Poly1305); AES uses AES-256-GCM.
|
||||
/// Mirrors Go's StoreCipher type (filestore.go:85).
|
||||
/// </summary>
|
||||
public enum StoreCipher
|
||||
{
|
||||
// Go: ChaCha — ChaCha20-Poly1305 (default)
|
||||
ChaCha,
|
||||
|
||||
// Go: AES — AES-256-GCM
|
||||
Aes,
|
||||
|
||||
// Go: NoCipher — encryption disabled
|
||||
None,
|
||||
}
|
||||
|
||||
// Go: server/filestore.go:106
|
||||
/// <summary>
|
||||
/// Selects the compression algorithm applied to each message block.
|
||||
/// Mirrors Go's StoreCompression type (filestore.go:106).
|
||||
/// </summary>
|
||||
public enum StoreCompression : byte
|
||||
{
|
||||
// Go: NoCompression — no compression applied
|
||||
None = 0,
|
||||
|
||||
// Go: S2Compression — S2 (Snappy variant) block compression
|
||||
S2 = 1,
|
||||
}
|
||||
// StoreCipher and StoreCompression are defined in AeadEncryptor.cs (Task 4).
|
||||
|
||||
// Go: server/filestore.go:55
|
||||
/// <summary>
|
||||
@@ -67,9 +37,9 @@ public sealed class FileStoreConfig
|
||||
// flushed asynchronously for higher throughput
|
||||
public bool AsyncFlush { get; set; }
|
||||
|
||||
// Go: FileStoreConfig.Cipher — cipher used for at-rest encryption; None disables it
|
||||
public StoreCipher Cipher { get; set; } = StoreCipher.None;
|
||||
// Go: FileStoreConfig.Cipher — cipher used for at-rest encryption; NoCipher disables it
|
||||
public StoreCipher Cipher { get; set; } = StoreCipher.NoCipher;
|
||||
|
||||
// Go: FileStoreConfig.Compression — compression algorithm applied to block data
|
||||
public StoreCompression Compression { get; set; } = StoreCompression.None;
|
||||
public StoreCompression Compression { get; set; } = StoreCompression.NoCompression;
|
||||
}
|
||||
|
||||
@@ -6,8 +6,20 @@ public sealed class FileStoreOptions
|
||||
public int BlockSizeBytes { get; set; } = 64 * 1024;
|
||||
public string IndexManifestFileName { get; set; } = "index.manifest.json";
|
||||
public int MaxAgeMs { get; set; }
|
||||
|
||||
// Legacy boolean compression / encryption flags (FSV1 envelope format).
|
||||
// When set and the corresponding enum is left at its default (NoCompression /
|
||||
// NoCipher), the legacy Deflate / XOR path is used for backward compatibility.
|
||||
public bool EnableCompression { get; set; }
|
||||
public bool EnableEncryption { get; set; }
|
||||
|
||||
public bool EnablePayloadIntegrityChecks { get; set; } = true;
|
||||
public byte[]? EncryptionKey { get; set; }
|
||||
|
||||
// Go parity: StoreCompression / StoreCipher (filestore.go ~line 91-92).
|
||||
// When Compression == S2Compression the S2/Snappy codec is used (FSV2 envelope).
|
||||
// When Cipher != NoCipher an AEAD cipher is used instead of the legacy XOR.
|
||||
// Enums are defined in AeadEncryptor.cs.
|
||||
public StoreCompression Compression { get; set; } = StoreCompression.NoCompression;
|
||||
public StoreCipher Cipher { get; set; } = StoreCipher.NoCipher;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user