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.
using System;
using System.IO;
using System.Security.Cryptography;
using Microsoft.Extensions.DependencyInjection;
using SharpCoreDB;
// 1. Setup DI
var services = new ServiceCollection();
services.AddSharpCoreDB();
using var sp = services.BuildServiceProvider();
var factory = sp.GetRequiredService<DatabaseFactory>();
var path = "test.scdb";
var correctKey = RandomNumberGenerator.GetBytes(32);
var wrongKey = RandomNumberGenerator.GetBytes(32);
var options = new DatabaseOptions
{
StorageMode = StorageMode.SingleFile,
EnableEncryption = true,
EncryptionKey = correctKey,
CreateImmediately = true
};
// 2. Create and write with correct key
using (var db = 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 (var dbWrong = factory.CreateWithOptions(path, "unused", options with { EncryptionKey = wrongKey }))
{
var rows = 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)
var fileBytes = File.ReadAllBytes(path);
var secretBytes = System.Text.Encoding.UTF8.GetBytes("classified-payload");
bool isPlaintext = fileBytes.AsSpan().IndexOf(secretBytes.AsSpan()) >= 0;
Console.WriteLine($"Plaintext found on disk: {isPlaintext}"); // Outputs: True
Expected 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.