1- // <copyright file=" SingleFileStorageProvider.cs" company="MPCoreDeveloper">
1+ // src\SharpCoreDB\Storage\ SingleFileStorageProvider.cs
22// Copyright (c) 2025-2026 MPCoreDeveloper and GitHub Copilot. All rights reserved.
33// Licensed under the MIT License. See LICENSE file in the project root for full license information.
44// </copyright>
@@ -96,6 +96,11 @@ public sealed class SingleFileStorageProvider : IStorageProvider
9696 private const int WRITE_BATCH_SIZE = 200 ; // Batch 200 writes together (increased from 50)
9797 private const int WRITE_BATCH_TIMEOUT_MS = 200 ; // Or flush after 200ms (increased from 50ms)
9898
99+ // ✅ ENCRYPTION: AES-256-GCM instance for block-level encryption at rest
100+ // Null when encryption is disabled. When non-null, all block reads/writes
101+ // pass through Encrypt/Decrypt to ensure data is encrypted on disk.
102+ private readonly SharpCoreDB . Services . AesGcmEncryption ? _encryption ;
103+
99104 private bool _isInTransaction ;
100105 private bool _disposed ;
101106 private ScdbFileHeader _header ;
@@ -109,13 +114,14 @@ public sealed class SingleFileStorageProvider : IStorageProvider
109114 /// <param name="mmf">Optional memory-mapped file</param>
110115 /// <param name="header">File header structure</param>
111116 private SingleFileStorageProvider ( string filePath , DatabaseOptions options , FileStream fileStream ,
112- MemoryMappedFile ? mmf , ScdbFileHeader header )
117+ MemoryMappedFile ? mmf , ScdbFileHeader header , SharpCoreDB . Services . AesGcmEncryption ? encryption = null )
113118 {
114119 _filePath = filePath ;
115120 _options = options ;
116121 _fileStream = fileStream ;
117122 _memoryMappedFile = mmf ;
118123 _header = header ;
124+ _encryption = encryption ;
119125 _blockCache = new ConcurrentDictionary < string , BlockMetadata > ( ) ;
120126
121127 // Initialize subsystems
@@ -134,13 +140,27 @@ private SingleFileStorageProvider(string filePath, DatabaseOptions options, File
134140 /// <param name="filePath">Path to .scdb file</param>
135141 /// <param name="options">Database options</param>
136142 /// <returns>Initialized provider</returns>
137- public static SingleFileStorageProvider Open ( string filePath , DatabaseOptions options )
143+ public static SingleFileStorageProvider Open ( string filePath , DatabaseOptions options , SharpCoreDB . Interfaces . ICryptoService ? cryptoService = null )
138144 {
139145 ArgumentException . ThrowIfNullOrWhiteSpace ( filePath ) ;
140146 ArgumentNullException . ThrowIfNull ( options ) ;
141147
142148 options . Validate ( ) ;
143149
150+ // ✅ ENCRYPTION: Initialize AES-256-GCM if encryption is requested
151+ SharpCoreDB . Services . AesGcmEncryption ? encryption = null ;
152+ if ( options . EnableEncryption )
153+ {
154+ if ( cryptoService is null )
155+ throw new InvalidOperationException (
156+ "ICryptoService must be registered in DI when EnableEncryption is true. " +
157+ "Call services.AddSharpCoreDB() or register ICryptoService manually." ) ;
158+ if ( options . EncryptionKey is null || options . EncryptionKey . Length != 32 )
159+ throw new InvalidOperationException (
160+ "EncryptionKey must be exactly 32 bytes (256 bits) when EnableEncryption is true." ) ;
161+ encryption = cryptoService . GetAesGcmEncryption ( options . EncryptionKey ) ;
162+ }
163+
144164 // Ensure .scdb extension
145165 if ( ! filePath . EndsWith ( ".scdb" , StringComparison . OrdinalIgnoreCase ) )
146166 {
@@ -200,7 +220,7 @@ public static SingleFileStorageProvider Open(string filePath, DatabaseOptions op
200220 }
201221 }
202222
203- return new SingleFileStorageProvider ( filePath , options , fileStream , mmf , header ) ;
223+ return new SingleFileStorageProvider ( filePath , options , fileStream , mmf , header , encryption ) ;
204224 }
205225
206226 /// <summary>
@@ -280,7 +300,19 @@ public bool BlockExists(string blockName)
280300 return null ;
281301 }
282302
283- // Create a sub-stream view of the block
303+ // ✅ ENCRYPTION: If encryption is enabled, read the full block, decrypt,
304+ // and return a read-only MemoryStream over the plaintext.
305+ // BlockStream cannot decrypt in-place, so we must materialize the block.
306+ if ( _encryption is not null )
307+ {
308+ var encryptedData = new byte [ ( int ) entry . Length ] ;
309+ _fileStream . Position = ( long ) entry . Offset ;
310+ _fileStream . ReadExactly ( encryptedData ) ;
311+ var plaintextData = _encryption . Decrypt ( encryptedData ) ;
312+ return new MemoryStream ( plaintextData , index : 0 , count : plaintextData . Length , writable : false ) ;
313+ }
314+
315+ // Create a sub-stream view of the block (unencrypted path)
284316 return new BlockStream ( _fileStream , entry . Offset , entry . Length , FileAccess . Read ) ;
285317 }
286318
@@ -307,12 +339,19 @@ public unsafe ReadOnlySpan<byte> GetReadSpan(string blockName)
307339 var buffer = new byte [ checked ( ( int ) Math . Min ( entry . Length , ( ulong ) int . MaxValue ) ) ] ;
308340 _fileStream . Position = ( long ) entry . Offset ;
309341 _fileStream . ReadExactly ( buffer ) ;
342+ // ✅ ENCRYPTION: Decrypt if encryption is enabled
343+ if ( _encryption is not null )
344+ {
345+ return _encryption . Decrypt ( buffer ) ;
346+ }
310347 return buffer ;
311348 }
312349
313350 // Use memory-mapped file for zero-copy access
314- if ( _memoryMappedFile != null )
351+ if ( _memoryMappedFile != null && _encryption is null )
315352 {
353+ // ✅ ENCRYPTION: Only use zero-copy mmap when encryption is disabled.
354+ // Encrypted data must be read into a buffer and decrypted.
316355 try
317356 {
318357 var viewOffset = checked ( ( long ) entry . Offset ) ;
@@ -341,6 +380,12 @@ public unsafe ReadOnlySpan<byte> GetReadSpan(string blockName)
341380 var buffer2 = new byte [ ( int ) entry . Length ] ;
342381 _fileStream . Position = ( long ) entry . Offset ;
343382 _fileStream . ReadExactly ( buffer2 ) ;
383+
384+ // ✅ ENCRYPTION: Decrypt block data after reading from disk
385+ if ( _encryption is not null )
386+ {
387+ return _encryption . Decrypt ( buffer2 ) ;
388+ }
344389 return buffer2 ;
345390 }
346391
@@ -461,18 +506,32 @@ public async Task WriteBlockAsync(string blockName, ReadOnlyMemory<byte> data, C
461506 // ✅ Convert to array immediately (before async operations)
462507 var checksumArray = checksumSpan . ToArray ( ) ;
463508
509+ // ✅ ENCRYPTION: Encrypt block data before writing to disk
510+ // Checksum is computed on PLAINTEXT (above) so we can verify integrity after decryption.
511+ // The on-disk bytes are [Nonce][Ciphertext][Tag] when encryption is enabled.
512+ byte [ ] dataToStore ;
513+ if ( _encryption is not null )
514+ {
515+ dataToStore = _encryption . Encrypt ( data . ToArray ( ) ) ;
516+ // Update entry length to reflect encrypted size (plaintext + 12 nonce + 16 tag)
517+ entry = entry with { Length = ( ulong ) dataToStore . Length , Flags = entry . Flags | ( uint ) BlockFlags . Dirty } ;
518+ }
519+ else
520+ {
521+ dataToStore = data . ToArray ( ) ;
522+ }
523+
464524 // Write to WAL first (crash safety)
465525 if ( _isInTransaction )
466526 {
467527 await _walManager . LogWriteAsync ( blockName , offset , data , cancellationToken ) . ConfigureAwait ( false ) ;
468528 }
469529
470530 // ✅ Phase 1 Task 1.3: Queue write instead of direct I/O
471- // Copy data to array (required for safe batching)
472531 var writeOp = new WriteOperation
473532 {
474533 BlockName = blockName ,
475- Data = data . ToArray ( ) ,
534+ Data = dataToStore ,
476535 Checksum = checksumArray ,
477536 Offset = offset ,
478537 Entry = SetChecksum ( entry , checksumArray )
@@ -712,11 +771,25 @@ public async Task<long> UpdateBlockAsync(
712771 _fileStream . Position = ( long ) entry . Offset ;
713772 await _fileStream . ReadExactlyAsync ( buffer , cancellationToken ) . ConfigureAwait ( false ) ;
714773
715- // Validate checksum; if mismatch, attempt self-heal
716- if ( ! ValidateChecksum ( entry , buffer . Span ) )
774+ // ✅ ENCRYPTION: Decrypt block data after reading from disk
775+ // The on-disk bytes are [Nonce][Ciphertext][Tag] when encryption is enabled.
776+ // We decrypt first, THEN validate the checksum against the plaintext.
777+ byte [ ] plaintextBytes ;
778+ if ( _encryption is not null )
779+ {
780+ plaintextBytes = _encryption . Decrypt ( buffer . ToArray ( ) ) ;
781+ }
782+ else
783+ {
784+ plaintextBytes = new byte [ entry . Length ] ;
785+ buffer . Span . CopyTo ( plaintextBytes ) ;
786+ }
787+
788+ // Validate checksum against PLAINTEXT; if mismatch, attempt self-heal
789+ if ( ! ValidateChecksum ( entry , plaintextBytes ) )
717790 {
718791 Console . WriteLine ( $ "[SingleFileStorageProvider] Checksum mismatch for block '{ blockName } ', attempting self-heal") ;
719- var repairedEntry = SetChecksum ( entry , SHA256 . HashData ( buffer . Span ) ) ;
792+ var repairedEntry = SetChecksum ( entry , SHA256 . HashData ( plaintextBytes ) ) ;
720793 _blockRegistry . AddOrUpdateBlock ( blockName , repairedEntry ) ;
721794 await _blockRegistry . FlushAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
722795
@@ -736,10 +809,8 @@ public async Task<long> UpdateBlockAsync(
736809 } ;
737810 }
738811
739- // ✅ Phase 3.3: Copy to result array (caller owns this memory)
740- var result = new byte [ entry . Length ] ;
741- buffer . Span . CopyTo ( result ) ;
742- return result ;
812+ // Return the decrypted plaintext to the caller
813+ return plaintextBytes ;
743814 }
744815 finally
745816 {
0 commit comments