|
|
|
|
@@ -13,6 +13,9 @@
|
|
|
|
|
//
|
|
|
|
|
// Adapted from server/filestore.go (fileStore struct and methods)
|
|
|
|
|
|
|
|
|
|
using System.Buffers;
|
|
|
|
|
using System.Security.Cryptography;
|
|
|
|
|
using System.Text;
|
|
|
|
|
using System.Text.Json;
|
|
|
|
|
using System.Threading.Channels;
|
|
|
|
|
using ZB.MOM.NatsNet.Server.Internal.DataStructures;
|
|
|
|
|
@@ -54,6 +57,9 @@ public sealed class JetStreamFileStore : IStreamStore, IDisposable
|
|
|
|
|
// Configuration
|
|
|
|
|
private FileStreamInfo _cfg;
|
|
|
|
|
private FileStoreConfig _fcfg;
|
|
|
|
|
private readonly KeyGen? _prf;
|
|
|
|
|
private readonly KeyGen? _oldPrf;
|
|
|
|
|
private AeadCipher? _aek;
|
|
|
|
|
|
|
|
|
|
// Message block list and index
|
|
|
|
|
private MessageBlock? _lmb; // last (active write) block
|
|
|
|
|
@@ -104,6 +110,7 @@ public sealed class JetStreamFileStore : IStreamStore, IDisposable
|
|
|
|
|
// In this incremental port stage, file-store logic delegates core stream semantics
|
|
|
|
|
// to the memory store implementation while file-specific APIs are added on top.
|
|
|
|
|
private readonly JetStreamMemStore _memStore;
|
|
|
|
|
private static readonly ArrayPool<byte> MsgBlockBufferPool = ArrayPool<byte>.Shared;
|
|
|
|
|
|
|
|
|
|
// -----------------------------------------------------------------------
|
|
|
|
|
// Constructor
|
|
|
|
|
@@ -119,16 +126,31 @@ public sealed class JetStreamFileStore : IStreamStore, IDisposable
|
|
|
|
|
/// Thrown when <paramref name="fcfg"/> or <paramref name="cfg"/> is null.
|
|
|
|
|
/// </exception>
|
|
|
|
|
public JetStreamFileStore(FileStoreConfig fcfg, FileStreamInfo cfg)
|
|
|
|
|
: this(fcfg, cfg, null, null)
|
|
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal JetStreamFileStore(FileStoreConfig fcfg, FileStreamInfo cfg, KeyGen? prf, KeyGen? oldPrf)
|
|
|
|
|
{
|
|
|
|
|
ArgumentNullException.ThrowIfNull(fcfg);
|
|
|
|
|
ArgumentNullException.ThrowIfNull(cfg);
|
|
|
|
|
ArgumentNullException.ThrowIfNull(cfg.Config);
|
|
|
|
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(cfg.Config.Name))
|
|
|
|
|
throw new ArgumentException("name required", nameof(cfg));
|
|
|
|
|
if (cfg.Config.Storage != StorageType.FileStorage)
|
|
|
|
|
throw new ArgumentException("file store requires file storage config", nameof(cfg));
|
|
|
|
|
|
|
|
|
|
_fcfg = fcfg;
|
|
|
|
|
_cfg = cfg;
|
|
|
|
|
_prf = prf;
|
|
|
|
|
_oldPrf = oldPrf;
|
|
|
|
|
|
|
|
|
|
// Apply defaults (mirrors newFileStoreWithCreated in filestore.go).
|
|
|
|
|
if (_fcfg.BlockSize == 0)
|
|
|
|
|
_fcfg.BlockSize = FileStoreDefaults.DefaultLargeBlockSize;
|
|
|
|
|
_fcfg.BlockSize = DynBlkSize(cfg.Config.Retention, cfg.Config.MaxBytes, _prf != null);
|
|
|
|
|
if (_fcfg.BlockSize > FileStoreDefaults.MaxBlockSize)
|
|
|
|
|
throw new InvalidOperationException($"filestore max block size is {FileStoreDefaults.MaxBlockSize} bytes");
|
|
|
|
|
if (_fcfg.CacheExpire == TimeSpan.Zero)
|
|
|
|
|
_fcfg.CacheExpire = FileStoreDefaults.DefaultCacheBufferExpiration;
|
|
|
|
|
if (_fcfg.SubjectStateExpire == TimeSpan.Zero)
|
|
|
|
|
@@ -136,6 +158,10 @@ public sealed class JetStreamFileStore : IStreamStore, IDisposable
|
|
|
|
|
if (_fcfg.SyncInterval == TimeSpan.Zero)
|
|
|
|
|
_fcfg.SyncInterval = FileStoreDefaults.DefaultSyncInterval;
|
|
|
|
|
|
|
|
|
|
EnsureStoreDirectoryWritable(_fcfg.StoreDir);
|
|
|
|
|
Directory.CreateDirectory(Path.Combine(_fcfg.StoreDir, FileStoreDefaults.MsgDir));
|
|
|
|
|
Directory.CreateDirectory(Path.Combine(_fcfg.StoreDir, FileStoreDefaults.ConsumerDir));
|
|
|
|
|
|
|
|
|
|
_psim = new SubjectTree<Psi>();
|
|
|
|
|
_bim = new Dictionary<uint, MessageBlock>();
|
|
|
|
|
_qch = Channel.CreateUnbounded<byte>();
|
|
|
|
|
@@ -144,6 +170,387 @@ public sealed class JetStreamFileStore : IStreamStore, IDisposable
|
|
|
|
|
var memCfg = cfg.Config.Clone();
|
|
|
|
|
memCfg.Storage = StorageType.MemoryStorage;
|
|
|
|
|
_memStore = new JetStreamMemStore(memCfg);
|
|
|
|
|
|
|
|
|
|
var keyFile = Path.Combine(_fcfg.StoreDir, FileStoreDefaults.JetStreamMetaFileKey);
|
|
|
|
|
if (_prf == null && File.Exists(keyFile))
|
|
|
|
|
throw new InvalidOperationException("encrypted store requires encryption key function");
|
|
|
|
|
if (_prf != null && File.Exists(keyFile))
|
|
|
|
|
RecoverAEK();
|
|
|
|
|
|
|
|
|
|
var meta = Path.Combine(_fcfg.StoreDir, FileStoreDefaults.JetStreamMetaFile);
|
|
|
|
|
if (!File.Exists(meta) || new FileInfo(meta).Length == 0)
|
|
|
|
|
WriteStreamMeta();
|
|
|
|
|
else if (_prf != null && !File.Exists(keyFile))
|
|
|
|
|
WriteStreamMeta();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Creates a file store using the current UTC timestamp.
|
|
|
|
|
/// Mirrors Go <c>newFileStore</c>.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public static JetStreamFileStore NewFileStore(FileStoreConfig fcfg, StreamConfig cfg)
|
|
|
|
|
=> NewFileStoreWithCreated(fcfg, cfg, DateTime.UtcNow, null, null);
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Creates a file store with an explicit creation timestamp and optional key generators.
|
|
|
|
|
/// Mirrors Go <c>newFileStoreWithCreated</c>.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public static JetStreamFileStore NewFileStoreWithCreated(
|
|
|
|
|
FileStoreConfig fcfg,
|
|
|
|
|
StreamConfig cfg,
|
|
|
|
|
DateTime created,
|
|
|
|
|
KeyGen? prf,
|
|
|
|
|
KeyGen? oldPrf)
|
|
|
|
|
{
|
|
|
|
|
ArgumentNullException.ThrowIfNull(fcfg);
|
|
|
|
|
ArgumentNullException.ThrowIfNull(cfg);
|
|
|
|
|
|
|
|
|
|
var ccfg = cfg.Clone();
|
|
|
|
|
return new JetStreamFileStore(
|
|
|
|
|
fcfg,
|
|
|
|
|
new FileStreamInfo { Created = created, Config = ccfg },
|
|
|
|
|
prf,
|
|
|
|
|
oldPrf);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal void LockAllMsgBlocks()
|
|
|
|
|
{
|
|
|
|
|
foreach (var mb in _blks)
|
|
|
|
|
mb.Mu.EnterWriteLock();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal void UnlockAllMsgBlocks()
|
|
|
|
|
{
|
|
|
|
|
foreach (var mb in _blks)
|
|
|
|
|
{
|
|
|
|
|
if (mb.Mu.IsWriteLockHeld)
|
|
|
|
|
mb.Mu.ExitWriteLock();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal static ulong DynBlkSize(RetentionPolicy retention, long maxBytes, bool encrypted)
|
|
|
|
|
{
|
|
|
|
|
if (maxBytes > 0)
|
|
|
|
|
{
|
|
|
|
|
var blkSize = (maxBytes / 4) + 1;
|
|
|
|
|
if (blkSize % 100 != 0)
|
|
|
|
|
blkSize += 100 - (blkSize % 100);
|
|
|
|
|
|
|
|
|
|
if (blkSize <= (long)FileStoreDefaults.FileStoreMinBlkSize)
|
|
|
|
|
blkSize = (long)FileStoreDefaults.FileStoreMinBlkSize;
|
|
|
|
|
else if (blkSize >= (long)FileStoreDefaults.FileStoreMaxBlkSize)
|
|
|
|
|
blkSize = (long)FileStoreDefaults.FileStoreMaxBlkSize;
|
|
|
|
|
else
|
|
|
|
|
blkSize = (long)FileStoreDefaults.DefaultMediumBlockSize;
|
|
|
|
|
|
|
|
|
|
if (encrypted && blkSize > (long)FileStoreDefaults.MaximumEncryptedBlockSize)
|
|
|
|
|
blkSize = (long)FileStoreDefaults.MaximumEncryptedBlockSize;
|
|
|
|
|
|
|
|
|
|
return (ulong)blkSize;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (encrypted)
|
|
|
|
|
return FileStoreDefaults.MaximumEncryptedBlockSize;
|
|
|
|
|
|
|
|
|
|
return retention == RetentionPolicy.LimitsPolicy
|
|
|
|
|
? FileStoreDefaults.DefaultLargeBlockSize
|
|
|
|
|
: FileStoreDefaults.DefaultMediumBlockSize;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal static AeadCipher GenEncryptionKey(StoreCipher sc, byte[] seed)
|
|
|
|
|
{
|
|
|
|
|
ArgumentNullException.ThrowIfNull(seed);
|
|
|
|
|
return sc switch
|
|
|
|
|
{
|
|
|
|
|
StoreCipher.ChaCha => new AeadCipher(seed),
|
|
|
|
|
StoreCipher.Aes => new AeadCipher(seed),
|
|
|
|
|
_ => throw new InvalidOperationException("unknown cipher"),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal (AeadCipher Aek, byte[] Seed, byte[] Encrypted) GenEncryptionKeys(string context)
|
|
|
|
|
{
|
|
|
|
|
if (_prf == null)
|
|
|
|
|
throw new InvalidOperationException("encryption key function required");
|
|
|
|
|
|
|
|
|
|
var rb = _prf(Encoding.UTF8.GetBytes(context));
|
|
|
|
|
var kek = GenEncryptionKey(_fcfg.Cipher, rb);
|
|
|
|
|
|
|
|
|
|
var seed = new byte[32];
|
|
|
|
|
RandomNumberGenerator.Fill(seed);
|
|
|
|
|
var aek = GenEncryptionKey(_fcfg.Cipher, seed);
|
|
|
|
|
|
|
|
|
|
var nonce = new byte[kek.NonceSize];
|
|
|
|
|
RandomNumberGenerator.Fill(nonce);
|
|
|
|
|
var encryptedSeed = kek.Seal(nonce, seed);
|
|
|
|
|
|
|
|
|
|
var encrypted = new byte[nonce.Length + encryptedSeed.Length];
|
|
|
|
|
Buffer.BlockCopy(nonce, 0, encrypted, 0, nonce.Length);
|
|
|
|
|
Buffer.BlockCopy(encryptedSeed, 0, encrypted, nonce.Length, encryptedSeed.Length);
|
|
|
|
|
return (aek, seed, encrypted);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal static XorStreamCipher GenBlockEncryptionKey(StoreCipher sc, byte[] seed, byte[] nonce)
|
|
|
|
|
{
|
|
|
|
|
ArgumentNullException.ThrowIfNull(seed);
|
|
|
|
|
ArgumentNullException.ThrowIfNull(nonce);
|
|
|
|
|
return sc switch
|
|
|
|
|
{
|
|
|
|
|
StoreCipher.ChaCha => new XorStreamCipher(seed, nonce),
|
|
|
|
|
StoreCipher.Aes => new XorStreamCipher(seed, nonce),
|
|
|
|
|
_ => throw new InvalidOperationException("unknown cipher"),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal void RecoverAEK()
|
|
|
|
|
{
|
|
|
|
|
if (_prf == null || _aek != null)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
var keyFile = Path.Combine(_fcfg.StoreDir, FileStoreDefaults.JetStreamMetaFileKey);
|
|
|
|
|
var ekey = File.ReadAllBytes(keyFile);
|
|
|
|
|
if (ekey.Length < FileStoreDefaults.MinMetaKeySize)
|
|
|
|
|
throw new InvalidDataException("bad key size");
|
|
|
|
|
|
|
|
|
|
var rb = _prf(Encoding.UTF8.GetBytes(_cfg.Config.Name));
|
|
|
|
|
var kek = GenEncryptionKey(_fcfg.Cipher, rb);
|
|
|
|
|
var ns = kek.NonceSize;
|
|
|
|
|
if (ekey.Length <= ns)
|
|
|
|
|
throw new InvalidDataException("malformed encrypted key");
|
|
|
|
|
|
|
|
|
|
var nonce = ekey.AsSpan(0, ns);
|
|
|
|
|
var payload = ekey.AsSpan(ns);
|
|
|
|
|
var seed = kek.Open(nonce, payload);
|
|
|
|
|
_aek = GenEncryptionKey(_fcfg.Cipher, seed);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal void SetupAEK()
|
|
|
|
|
{
|
|
|
|
|
if (_prf == null || _aek != null)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
var (key, _, encrypted) = GenEncryptionKeys(_cfg.Config.Name);
|
|
|
|
|
var keyFile = Path.Combine(_fcfg.StoreDir, FileStoreDefaults.JetStreamMetaFileKey);
|
|
|
|
|
WriteFileWithOptionalSync(keyFile, encrypted);
|
|
|
|
|
_aek = key;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal void WriteStreamMeta()
|
|
|
|
|
{
|
|
|
|
|
SetupAEK();
|
|
|
|
|
|
|
|
|
|
var payload = JsonSerializer.SerializeToUtf8Bytes(_cfg);
|
|
|
|
|
if (_aek != null)
|
|
|
|
|
{
|
|
|
|
|
var nonce = new byte[_aek.NonceSize];
|
|
|
|
|
RandomNumberGenerator.Fill(nonce);
|
|
|
|
|
var encrypted = _aek.Seal(nonce, payload);
|
|
|
|
|
var sealedPayload = new byte[nonce.Length + encrypted.Length];
|
|
|
|
|
Buffer.BlockCopy(nonce, 0, sealedPayload, 0, nonce.Length);
|
|
|
|
|
Buffer.BlockCopy(encrypted, 0, sealedPayload, nonce.Length, encrypted.Length);
|
|
|
|
|
payload = sealedPayload;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var meta = Path.Combine(_fcfg.StoreDir, FileStoreDefaults.JetStreamMetaFile);
|
|
|
|
|
WriteFileWithOptionalSync(meta, payload);
|
|
|
|
|
|
|
|
|
|
var checksum = Convert.ToHexString(SHA256.HashData(payload)).ToLowerInvariant();
|
|
|
|
|
var sum = Path.Combine(_fcfg.StoreDir, FileStoreDefaults.JetStreamMetaFileSum);
|
|
|
|
|
WriteFileWithOptionalSync(sum, Encoding.ASCII.GetBytes(checksum));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal static byte[] GetMsgBlockBuf(int sz)
|
|
|
|
|
{
|
|
|
|
|
var target = sz switch
|
|
|
|
|
{
|
|
|
|
|
<= (int)FileStoreDefaults.DefaultTinyBlockSize => (int)FileStoreDefaults.DefaultTinyBlockSize,
|
|
|
|
|
<= (int)FileStoreDefaults.DefaultSmallBlockSize => (int)FileStoreDefaults.DefaultSmallBlockSize,
|
|
|
|
|
<= (int)FileStoreDefaults.DefaultMediumBlockSize => (int)FileStoreDefaults.DefaultMediumBlockSize,
|
|
|
|
|
<= (int)FileStoreDefaults.DefaultLargeBlockSize => (int)FileStoreDefaults.DefaultLargeBlockSize,
|
|
|
|
|
_ => sz,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return MsgBlockBufferPool.Rent(target);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal static void RecycleMsgBlockBuf(byte[]? buf)
|
|
|
|
|
{
|
|
|
|
|
if (buf == null)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
var cap = buf.Length;
|
|
|
|
|
if (cap == (int)FileStoreDefaults.DefaultTinyBlockSize ||
|
|
|
|
|
cap == (int)FileStoreDefaults.DefaultSmallBlockSize ||
|
|
|
|
|
cap == (int)FileStoreDefaults.DefaultMediumBlockSize ||
|
|
|
|
|
cap == (int)FileStoreDefaults.DefaultLargeBlockSize)
|
|
|
|
|
{
|
|
|
|
|
MsgBlockBufferPool.Return(buf, clearArray: false);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal bool NoTrackSubjects()
|
|
|
|
|
{
|
|
|
|
|
var hasSubjectIndex = _psim is { } psim && psim.Size() > 0;
|
|
|
|
|
var hasSubjects = _cfg.Config.Subjects is { Length: > 0 };
|
|
|
|
|
var hasMirror = _cfg.Config.Mirror != null;
|
|
|
|
|
var hasSources = _cfg.Config.Sources is { Length: > 0 };
|
|
|
|
|
return !(hasSubjectIndex || hasSubjects || hasMirror || hasSources);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal MessageBlock InitMsgBlock(uint index)
|
|
|
|
|
{
|
|
|
|
|
var mb = new MessageBlock
|
|
|
|
|
{
|
|
|
|
|
Fs = this,
|
|
|
|
|
Index = index,
|
|
|
|
|
Cexp = _fcfg.CacheExpire,
|
|
|
|
|
Fexp = _fcfg.SubjectStateExpire,
|
|
|
|
|
NoTrack = NoTrackSubjects(),
|
|
|
|
|
SyncAlways = _fcfg.SyncAlways,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var mdir = Path.Combine(_fcfg.StoreDir, FileStoreDefaults.MsgDir);
|
|
|
|
|
Directory.CreateDirectory(mdir);
|
|
|
|
|
mb.Mfn = Path.Combine(mdir, string.Format(FileStoreDefaults.BlkScan, index));
|
|
|
|
|
mb.Kfn = Path.Combine(mdir, string.Format(FileStoreDefaults.KeyScan, index));
|
|
|
|
|
return mb;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal void LoadEncryptionForMsgBlock(MessageBlock mb)
|
|
|
|
|
{
|
|
|
|
|
ArgumentNullException.ThrowIfNull(mb);
|
|
|
|
|
|
|
|
|
|
if (_prf == null)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
var keyPath = string.IsNullOrWhiteSpace(mb.Kfn)
|
|
|
|
|
? Path.Combine(_fcfg.StoreDir, FileStoreDefaults.MsgDir, string.Format(FileStoreDefaults.KeyScan, mb.Index))
|
|
|
|
|
: mb.Kfn;
|
|
|
|
|
|
|
|
|
|
byte[] ekey;
|
|
|
|
|
if (!File.Exists(keyPath))
|
|
|
|
|
{
|
|
|
|
|
var (_, generatedSeed, encrypted) = GenEncryptionKeys($"{_cfg.Config.Name}:{mb.Index}");
|
|
|
|
|
WriteFileWithOptionalSync(keyPath, encrypted);
|
|
|
|
|
mb.Seed = generatedSeed;
|
|
|
|
|
var nonceSize = GenEncryptionKey(_fcfg.Cipher, _prf(Encoding.UTF8.GetBytes($"{_cfg.Config.Name}:{mb.Index}"))).NonceSize;
|
|
|
|
|
mb.Nonce = encrypted[..nonceSize];
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ekey = File.ReadAllBytes(keyPath);
|
|
|
|
|
if (ekey.Length < FileStoreDefaults.MinBlkKeySize)
|
|
|
|
|
throw new InvalidDataException("bad key size");
|
|
|
|
|
|
|
|
|
|
var rb = _prf(Encoding.UTF8.GetBytes($"{_cfg.Config.Name}:{mb.Index}"));
|
|
|
|
|
var kek = GenEncryptionKey(_fcfg.Cipher, rb);
|
|
|
|
|
var ns = kek.NonceSize;
|
|
|
|
|
if (ekey.Length <= ns)
|
|
|
|
|
throw new InvalidDataException("malformed encrypted key");
|
|
|
|
|
|
|
|
|
|
var seed = kek.Open(ekey.AsSpan(0, ns), ekey.AsSpan(ns));
|
|
|
|
|
mb.Seed = seed;
|
|
|
|
|
mb.Nonce = ekey[..ns];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal MessageBlock RecoverMsgBlock(uint index)
|
|
|
|
|
{
|
|
|
|
|
var mb = InitMsgBlock(index);
|
|
|
|
|
if (!File.Exists(mb.Mfn))
|
|
|
|
|
throw new FileNotFoundException("message block file not found", mb.Mfn);
|
|
|
|
|
|
|
|
|
|
using var file = new FileStream(mb.Mfn, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
|
|
|
|
mb.RBytes = (ulong)file.Length;
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
LoadEncryptionForMsgBlock(mb);
|
|
|
|
|
}
|
|
|
|
|
catch
|
|
|
|
|
{
|
|
|
|
|
// For parity with the Go flow, recovery keeps going and falls back to rebuild paths.
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var lchk = new byte[FileStoreDefaults.RecordHashSize];
|
|
|
|
|
if (mb.RBytes >= (ulong)FileStoreDefaults.RecordHashSize)
|
|
|
|
|
{
|
|
|
|
|
file.Seek(-(long)FileStoreDefaults.RecordHashSize, SeekOrigin.End);
|
|
|
|
|
file.ReadExactly(lchk, 0, lchk.Length);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var readIndexOk = TryReadBlockIndexInfo(mb, lchk);
|
|
|
|
|
if (!readIndexOk)
|
|
|
|
|
{
|
|
|
|
|
mb.Lchk = lchk;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
mb.CloseFDs();
|
|
|
|
|
AddMsgBlock(mb);
|
|
|
|
|
return mb;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static void EnsureStoreDirectoryWritable(string storeDir)
|
|
|
|
|
{
|
|
|
|
|
if (string.IsNullOrWhiteSpace(storeDir))
|
|
|
|
|
throw new ArgumentException("store directory required", nameof(storeDir));
|
|
|
|
|
|
|
|
|
|
Directory.CreateDirectory(storeDir);
|
|
|
|
|
var dirInfo = new DirectoryInfo(storeDir);
|
|
|
|
|
if (!dirInfo.Exists)
|
|
|
|
|
throw new InvalidOperationException("storage directory is not accessible");
|
|
|
|
|
|
|
|
|
|
var probe = Path.Combine(storeDir, $"_test_{Guid.NewGuid():N}.tmp");
|
|
|
|
|
using (File.Create(probe))
|
|
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
File.Delete(probe);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void WriteFileWithOptionalSync(string path, byte[] payload)
|
|
|
|
|
{
|
|
|
|
|
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
|
|
|
|
|
using var stream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None);
|
|
|
|
|
stream.Write(payload, 0, payload.Length);
|
|
|
|
|
stream.Flush(_fcfg.SyncAlways);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private bool TryReadBlockIndexInfo(MessageBlock mb, byte[] lchk)
|
|
|
|
|
{
|
|
|
|
|
var idxFile = Path.Combine(_fcfg.StoreDir, FileStoreDefaults.MsgDir, string.Format(FileStoreDefaults.IndexScan, mb.Index));
|
|
|
|
|
if (!File.Exists(idxFile))
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var data = JsonSerializer.Deserialize<MessageBlockIndexFile>(File.ReadAllBytes(idxFile));
|
|
|
|
|
if (data == null || data.LastChecksum == null)
|
|
|
|
|
return false;
|
|
|
|
|
if (!lchk.AsSpan().SequenceEqual(data.LastChecksum))
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
mb.Msgs = data.Msgs;
|
|
|
|
|
mb.Bytes = data.Bytes;
|
|
|
|
|
mb.RBytes = data.RawBytes;
|
|
|
|
|
mb.NoTrack = data.NoTrack;
|
|
|
|
|
mb.First = new MsgId { Seq = data.FirstSeq, Ts = data.FirstTs };
|
|
|
|
|
mb.Last = new MsgId { Seq = data.LastSeq, Ts = data.LastTs };
|
|
|
|
|
mb.Lchk = data.LastChecksum;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
catch
|
|
|
|
|
{
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void AddMsgBlock(MessageBlock mb)
|
|
|
|
|
{
|
|
|
|
|
if (!_bim.ContainsKey(mb.Index))
|
|
|
|
|
_blks.Add(mb);
|
|
|
|
|
_bim[mb.Index] = mb;
|
|
|
|
|
_blks.Sort((a, b) => a.Index.CompareTo(b.Index));
|
|
|
|
|
_lmb = _blks.Count > 0 ? _blks[^1] : null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// -----------------------------------------------------------------------
|
|
|
|
|
@@ -410,4 +817,87 @@ public sealed class JetStreamFileStore : IStreamStore, IDisposable
|
|
|
|
|
/// <inheritdoc/>
|
|
|
|
|
public (ulong Total, ulong Reported, Exception? Error) Utilization()
|
|
|
|
|
=> _memStore.Utilization();
|
|
|
|
|
|
|
|
|
|
internal sealed class XorStreamCipher
|
|
|
|
|
{
|
|
|
|
|
private readonly byte[] _keySeed;
|
|
|
|
|
|
|
|
|
|
public XorStreamCipher(byte[] seed, byte[] nonce)
|
|
|
|
|
{
|
|
|
|
|
using var sha = SHA256.Create();
|
|
|
|
|
var input = new byte[seed.Length + nonce.Length];
|
|
|
|
|
Buffer.BlockCopy(seed, 0, input, 0, seed.Length);
|
|
|
|
|
Buffer.BlockCopy(nonce, 0, input, seed.Length, nonce.Length);
|
|
|
|
|
_keySeed = sha.ComputeHash(input);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void XorKeyStream(Span<byte> buffer)
|
|
|
|
|
{
|
|
|
|
|
for (var i = 0; i < buffer.Length; i++)
|
|
|
|
|
buffer[i] ^= _keySeed[i % _keySeed.Length];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal sealed class AeadCipher
|
|
|
|
|
{
|
|
|
|
|
private const int AeadNonceSize = 12;
|
|
|
|
|
private const int AeadTagSize = 16;
|
|
|
|
|
|
|
|
|
|
private readonly byte[] _key;
|
|
|
|
|
|
|
|
|
|
public AeadCipher(byte[] seed)
|
|
|
|
|
{
|
|
|
|
|
using var sha = SHA256.Create();
|
|
|
|
|
_key = sha.ComputeHash(seed);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public int NonceSize => AeadNonceSize;
|
|
|
|
|
public int Overhead => AeadTagSize;
|
|
|
|
|
|
|
|
|
|
public byte[] Seal(ReadOnlySpan<byte> nonce, ReadOnlySpan<byte> plaintext)
|
|
|
|
|
{
|
|
|
|
|
if (nonce.Length != AeadNonceSize)
|
|
|
|
|
throw new ArgumentException("invalid nonce size", nameof(nonce));
|
|
|
|
|
|
|
|
|
|
var ciphertext = new byte[plaintext.Length];
|
|
|
|
|
var tag = new byte[AeadTagSize];
|
|
|
|
|
using var aes = new AesGcm(_key, AeadTagSize);
|
|
|
|
|
aes.Encrypt(nonce, plaintext, ciphertext, tag);
|
|
|
|
|
|
|
|
|
|
var sealedPayload = new byte[ciphertext.Length + tag.Length];
|
|
|
|
|
Buffer.BlockCopy(ciphertext, 0, sealedPayload, 0, ciphertext.Length);
|
|
|
|
|
Buffer.BlockCopy(tag, 0, sealedPayload, ciphertext.Length, tag.Length);
|
|
|
|
|
return sealedPayload;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public byte[] Open(ReadOnlySpan<byte> nonce, ReadOnlySpan<byte> ciphertextAndTag)
|
|
|
|
|
{
|
|
|
|
|
if (nonce.Length != AeadNonceSize)
|
|
|
|
|
throw new ArgumentException("invalid nonce size", nameof(nonce));
|
|
|
|
|
if (ciphertextAndTag.Length < AeadTagSize)
|
|
|
|
|
throw new ArgumentException("invalid ciphertext size", nameof(ciphertextAndTag));
|
|
|
|
|
|
|
|
|
|
var ciphertextLength = ciphertextAndTag.Length - AeadTagSize;
|
|
|
|
|
var ciphertext = ciphertextAndTag[..ciphertextLength];
|
|
|
|
|
var tag = ciphertextAndTag[ciphertextLength..];
|
|
|
|
|
|
|
|
|
|
var plaintext = new byte[ciphertextLength];
|
|
|
|
|
using var aes = new AesGcm(_key, AeadTagSize);
|
|
|
|
|
aes.Decrypt(nonce, ciphertext, tag, plaintext);
|
|
|
|
|
return plaintext;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private sealed class MessageBlockIndexFile
|
|
|
|
|
{
|
|
|
|
|
public ulong Msgs { get; set; }
|
|
|
|
|
public ulong Bytes { get; set; }
|
|
|
|
|
public ulong RawBytes { get; set; }
|
|
|
|
|
public ulong FirstSeq { get; set; }
|
|
|
|
|
public long FirstTs { get; set; }
|
|
|
|
|
public ulong LastSeq { get; set; }
|
|
|
|
|
public long LastTs { get; set; }
|
|
|
|
|
public byte[]? LastChecksum { get; set; }
|
|
|
|
|
public bool NoTrack { get; set; }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|