Skip to content

Commit e71b036

Browse files
authored
Merge pull request #37 from xskcdf/development
Fix SQLITE_READONLY(8): move WAL/synchronous PRAGMAs out of intercept…
2 parents c81ba75 + c9be297 commit e71b036

4 files changed

Lines changed: 42 additions & 22 deletions

File tree

1-Aquiis.Infrastructure/Data/SqlCipherConnectionInterceptor.cs

Lines changed: 17 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,19 @@ public override void ConnectionOpened(DbConnection connection, ConnectionEndEven
3535
// Pre-derived raw key — SQLCipher loads it directly, no PBKDF2 (~0 ms)
3636
cmd.CommandText = $"PRAGMA key = \"{_encryptionKey}\";";
3737
cmd.ExecuteNonQuery();
38+
39+
// Raw key still needs cipher params to match how the DB was encrypted
40+
cmd.CommandText = "PRAGMA cipher_page_size = 4096;";
41+
cmd.ExecuteNonQuery();
42+
cmd.CommandText = "PRAGMA cipher_hmac_algorithm = HMAC_SHA512;";
43+
cmd.ExecuteNonQuery();
44+
cmd.CommandText = "PRAGMA cipher_kdf_algorithm = PBKDF2_HMAC_SHA512;";
45+
cmd.ExecuteNonQuery();
3846
}
3947
else
4048
{
4149
// Passphrase fallback — SQLCipher runs PBKDF2(256000) internally (~20–50 ms)
42-
cmd.CommandText = $"PRAGMA key = '{_encryptionKey}';";
50+
cmd.CommandText = $"PRAGMA key = '{_encryptionKey}';";
4351
cmd.ExecuteNonQuery();
4452

4553
cmd.CommandText = "PRAGMA cipher_page_size = 4096;";
@@ -53,16 +61,6 @@ public override void ConnectionOpened(DbConnection connection, ConnectionEndEven
5361
}
5462
}
5563

56-
// WAL mode is persistent in the database file — no-op if already set, upgrades fresh DBs.
57-
// NORMAL synchronous is safe with WAL and reduces flush overhead on every commit.
58-
using (var cmd = connection.CreateCommand())
59-
{
60-
cmd.CommandText = "PRAGMA journal_mode = WAL;";
61-
cmd.ExecuteNonQuery();
62-
cmd.CommandText = "PRAGMA synchronous = NORMAL;";
63-
cmd.ExecuteNonQuery();
64-
}
65-
6664
base.ConnectionOpened(connection, eventData);
6765
}
6866

@@ -77,6 +75,14 @@ public override async Task ConnectionOpenedAsync(DbConnection connection, Connec
7775
// Pre-derived raw key — SQLCipher loads it directly, no PBKDF2 (~0 ms)
7876
cmd.CommandText = $"PRAGMA key = \"{_encryptionKey}\";";
7977
await cmd.ExecuteNonQueryAsync(cancellationToken);
78+
79+
// Raw key still needs cipher params to match how the DB was encrypted
80+
cmd.CommandText = "PRAGMA cipher_page_size = 4096;";
81+
await cmd.ExecuteNonQueryAsync(cancellationToken);
82+
cmd.CommandText = "PRAGMA cipher_hmac_algorithm = HMAC_SHA512;";
83+
await cmd.ExecuteNonQueryAsync(cancellationToken);
84+
cmd.CommandText = "PRAGMA cipher_kdf_algorithm = PBKDF2_HMAC_SHA512;";
85+
await cmd.ExecuteNonQueryAsync(cancellationToken);
8086
}
8187
else
8288
{
@@ -95,16 +101,6 @@ public override async Task ConnectionOpenedAsync(DbConnection connection, Connec
95101
}
96102
}
97103

98-
// WAL mode is persistent in the database file — no-op if already set, upgrades fresh DBs.
99-
// NORMAL synchronous is safe with WAL and reduces flush overhead on every commit.
100-
using (var cmd = connection.CreateCommand())
101-
{
102-
cmd.CommandText = "PRAGMA journal_mode = WAL;";
103-
await cmd.ExecuteNonQueryAsync(cancellationToken);
104-
cmd.CommandText = "PRAGMA synchronous = NORMAL;";
105-
await cmd.ExecuteNonQueryAsync(cancellationToken);
106-
}
107-
108104
await base.ConnectionOpenedAsync(connection, eventData, cancellationToken);
109105
}
110106
}

2-Aquiis.Application/Services/DatabaseService.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,12 +77,14 @@ public async Task<bool> CanConnectAsync()
7777
public async Task<int> GetPendingMigrationsCountAsync()
7878
{
7979
var pending = await _businessContext.Database.GetPendingMigrationsAsync();
80+
_logger.LogInformation($"Business context has {pending.Count()} pending migrations.");
8081
return pending.Count();
8182
}
8283

8384
public async Task<int> GetIdentityPendingMigrationsCountAsync()
8485
{
8586
var pending = await _identityContext.Database.GetPendingMigrationsAsync();
87+
_logger.LogInformation($"Identity context has {pending.Count()} pending migrations.");
8688
return pending.Count();
8789
}
8890

4-Aquiis.SimpleStart/Aquiis.SimpleStart.csproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@
5353
</PackageReference>
5454
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.1" />
5555
<PackageReference Include="SendGrid" Version="9.29.3" />
56-
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.11" />
56+
<!-- <PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.11" /> -->
57+
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlcipher" Version="2.1.11" />
5758
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.1" />
5859
<PackageReference Include="QuestPDF" Version="2025.12.1" />
5960
<PackageReference Include="Twilio" Version="7.14.0" />

4-Aquiis.SimpleStart/Extensions/WebServiceExtensions.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,13 @@ public static IServiceCollection AddWebServices(
5959
{
6060
NeedsUnlock = encryptionPassword == null && IsDatabaseEncrypted(connectionString),
6161
DatabasePath = ExtractDatabasePath(connectionString),
62+
6263
ConnectionString = connectionString
6364
};
6465
services.AddSingleton(unlockState);
6566

67+
Console.WriteLine($"Database unlock state: NeedsUnlock={unlockState.NeedsUnlock}, DatabasePath={unlockState.DatabasePath}");
68+
6669
// Register encryption status as singleton for use during startup
6770
services.AddSingleton(new EncryptionDetectionResult
6871
{
@@ -85,6 +88,24 @@ public static IServiceCollection AddWebServices(
8588

8689
// Clear connection pools to ensure no connections bypass the interceptor
8790
SqliteConnection.ClearAllPools();
91+
92+
// Set WAL mode and synchronous=NORMAL once on a writable connection.
93+
// These are persistent database settings — no need to repeat on every connection open.
94+
// This MUST happen here (not in the interceptor) because EF Core also opens read-only
95+
// connections (e.g. SqliteDatabaseCreator.Exists()), and PRAGMA journal_mode = WAL
96+
// returns SQLITE_READONLY (8) on a read-only connection.
97+
using var setupConn = new SqliteConnection(connectionString);
98+
setupConn.Open();
99+
using var setupCmd = setupConn.CreateCommand();
100+
setupCmd.CommandText = $"PRAGMA key = \"{encryptionPassword}\";";
101+
setupCmd.ExecuteNonQuery();
102+
setupCmd.CommandText = "PRAGMA journal_mode = WAL;";
103+
setupCmd.ExecuteNonQuery();
104+
setupCmd.CommandText = "PRAGMA synchronous = NORMAL;";
105+
setupCmd.ExecuteNonQuery();
106+
setupConn.Close();
107+
108+
SqliteConnection.ClearAllPools();
88109
}
89110

90111
// ✅ Register Application layer (includes Infrastructure internally) with encryption interceptor

0 commit comments

Comments
 (0)