Description
When using StorageMode.SingleFile (.scdb), the database accepts DatabaseOptions.EnableEncryption = true and a 32-byte EncryptionKey, but completely bypasses the encryption pipeline. Data is written to disk in plaintext, and reopening with a completely different (wrong) 32-byte key succeeds without throwing a cryptographic exception.
Minimal Reproduction
The following snippet demonstrates the bug. It creates an encrypted database, inserts a secret, and then successfully reopens it with a different key, proving the key is ignored and data is unencrypted.
usingSystem;usingSystem.IO;usingSystem.Security.Cryptography;usingMicrosoft.Extensions.DependencyInjection;usingSharpCoreDB;// 1. Setup DIvarservices=newServiceCollection();services.AddSharpCoreDB();usingvarsp=services.BuildServiceProvider();varfactory=sp.GetRequiredService<DatabaseFactory>();varpath="test.scdb";varcorrectKey=RandomNumberGenerator.GetBytes(32);varwrongKey=RandomNumberGenerator.GetBytes(32);varoptions=newDatabaseOptions{StorageMode=StorageMode.SingleFile,EnableEncryption=true,EncryptionKey=correctKey,CreateImmediately=true};// 2. Create and write with correct keyusing(vardb=factory.CreateWithOptions(path,"unused",options)){db.ExecuteSQL("CREATE TABLE Secrets (Id INT, Data TEXT)");db.ExecuteSQL("INSERT INTO Secrets VALUES (1, 'classified-payload')");db.ForceSave();}// 3. Reopen with WRONG key (BUG: This should fail, but it succeeds)using(vardbWrong=factory.CreateWithOptions(path,"unused",optionswith{EncryptionKey=wrongKey})){varrows=dbWrong.ExecuteQuery("SELECT * FROM Secrets");Console.WriteLine($"Wrong key opened DB. Rows found: {rows.Count}");// Outputs: 1}// 4. Verify plaintext on disk (BUG: Secret is visible in hex editor)varfileBytes=File.ReadAllBytes(path);varsecretBytes=System.Text.Encoding.UTF8.GetBytes("classified-payload");boolisPlaintext=fileBytes.AsSpan().IndexOf(secretBytes.AsSpan())>=0;Console.WriteLine($"Plaintext found on disk: {isPlaintext}");// Outputs: TrueExpected Behavior
- The
.scdb file should contain AES-256-GCM encrypted ciphertext, not plaintext. - Attempting to open the database with an incorrect 32-byte key should fail (e.g., throw a cryptographic authentication exception, or fail to decrypt the table directory, resulting in an empty/missing schema).
Actual Behavior
- The
SingleFileStorageProvider accepts the EncryptionKey in DatabaseOptions but never instantiates AesGcmEncryption or passes the key to the I/O layer. WriteBlockAsync queues raw plaintext bytes directly to the write-behind cache.ReadBlockAsync, GetReadSpan, and GetReadStream return raw plaintext bytes without attempting decryption.- Wrong keys are silently accepted because no cryptographic validation occurs during the read path.
Environment
- OS: Windows 11 / Linux x64 (Reproducible on both)
- .NET Version: .NET 10.0
- SharpCoreDB Version: 1.9.6
- Storage Mode:
StorageMode.SingleFile (.scdb)
Root Cause Analysis
Forensic analysis of src/SharpCoreDB/Storage/SingleFileStorageProvider.cs shows:
- The
Open method reads options.EnableEncryption but never resolves ICryptoService or instantiates AesGcmEncryption. - The constructor does not accept or store an encryption instance.
- I/O paths (
WriteBlockAsync, ReadBlockAsync, GetReadSpan, GetReadStream) operate directly on the FileStream without any encryption/decryption interception.
(Note: Directory mode correctly uses ICryptoService and PageEncryption via Database.Core.cs, so this bug is isolated to the SingleFile storage provider.)
Proposed Fix
I have already implemented a working local patch that:
- Updates
DatabaseFactory.CreateSingleFileDatabase to resolve ICryptoService from DI when EnableEncryption is true. - Passes the resolved
ICryptoService to SingleFileStorageProvider.Open. - Instantiates
AesGcmEncryption in the provider and intercepts all read/write paths (WriteBlockAsync, ReadBlockAsync, GetReadSpan, GetReadStream) to encrypt/decrypt block data. - Includes a comprehensive regression test suite (
SingleFileEncryptionTests.cs) covering plaintext-at-rest, correct-key roundtrip, wrong-key rejection, and file integrity.
I have opened a corresponding Pull Request (fix/singlefile-encryption-bypass) that implements this fix.
Description
When using
StorageMode.SingleFile(.scdb), the database acceptsDatabaseOptions.EnableEncryption = trueand a 32-byteEncryptionKey, but completely bypasses the encryption pipeline. Data is written to disk in plaintext, and reopening with a completely different (wrong) 32-byte key succeeds without throwing a cryptographic exception.Minimal Reproduction
The following snippet demonstrates the bug. It creates an encrypted database, inserts a secret, and then successfully reopens it with a different key, proving the key is ignored and data is unencrypted.
Expected Behavior
.scdbfile should contain AES-256-GCM encrypted ciphertext, not plaintext.Actual Behavior
SingleFileStorageProvideraccepts theEncryptionKeyinDatabaseOptionsbut never instantiatesAesGcmEncryptionor passes the key to the I/O layer.WriteBlockAsyncqueues raw plaintext bytes directly to the write-behind cache.ReadBlockAsync,GetReadSpan, andGetReadStreamreturn raw plaintext bytes without attempting decryption.Environment
StorageMode.SingleFile(.scdb)Root Cause Analysis
Forensic analysis of
src/SharpCoreDB/Storage/SingleFileStorageProvider.csshows:Openmethod readsoptions.EnableEncryptionbut never resolvesICryptoServiceor instantiatesAesGcmEncryption.WriteBlockAsync,ReadBlockAsync,GetReadSpan,GetReadStream) operate directly on theFileStreamwithout any encryption/decryption interception.(Note: Directory mode correctly uses
ICryptoServiceandPageEncryptionviaDatabase.Core.cs, so this bug is isolated to the SingleFile storage provider.)Proposed Fix
I have already implemented a working local patch that:
DatabaseFactory.CreateSingleFileDatabaseto resolveICryptoServicefrom DI whenEnableEncryptionis true.ICryptoServicetoSingleFileStorageProvider.Open.AesGcmEncryptionin the provider and intercepts all read/write paths (WriteBlockAsync,ReadBlockAsync,GetReadSpan,GetReadStream) to encrypt/decrypt block data.SingleFileEncryptionTests.cs) covering plaintext-at-rest, correct-key roundtrip, wrong-key rejection, and file integrity.I have opened a corresponding Pull Request (
fix/singlefile-encryption-bypass) that implements this fix.