Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/SignedPackageFileList.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
**/CommunityToolkit.Datasync.Client
**/CommunityToolkit.Datasync.Client.EncryptedSqlite
**/CommunityToolkit.Datasync.Server.Abstractions
**/CommunityToolkit.Datasync.Server.Automapper
**/CommunityToolkit.Datasync.Server.CosmosDb
Expand Down
14 changes: 14 additions & 0 deletions Datasync.Toolkit.sln
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Datasync.C
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Datasync.Client.Test", "tests\CommunityToolkit.Datasync.Client.Test\CommunityToolkit.Datasync.Client.Test.csproj", "{2889E6B2-9CD1-437C-A43C-98CFAFF68B99}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Datasync.Client.EncryptedSqlite", "src\CommunityToolkit.Datasync.Client.EncryptedSqlite\CommunityToolkit.Datasync.Client.EncryptedSqlite.csproj", "{9BF4A280-C2CF-4EA1-9B5C-1FC600BB4AA3}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Datasync.Client.EncryptedSqlite.Test", "tests\CommunityToolkit.Datasync.Client.EncryptedSqlite.Test\CommunityToolkit.Datasync.Client.EncryptedSqlite.Test.csproj", "{5AC81D60-8D19-45F6-96D8-2A7BBF5C311F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Datasync.Server.CosmosDb", "src\CommunityToolkit.Datasync.Server.CosmosDb\CommunityToolkit.Datasync.Server.CosmosDb.csproj", "{D9356867-0A30-4B17-BD4C-0F7EF70984C6}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Datasync.Server.MongoDB", "src\CommunityToolkit.Datasync.Server.MongoDB\CommunityToolkit.Datasync.Server.MongoDB.csproj", "{DC20ACF9-12E9-41D9-B672-CB5FD85548E9}"
Expand Down Expand Up @@ -158,6 +162,14 @@ Global
{2889E6B2-9CD1-437C-A43C-98CFAFF68B99}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2889E6B2-9CD1-437C-A43C-98CFAFF68B99}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2889E6B2-9CD1-437C-A43C-98CFAFF68B99}.Release|Any CPU.Build.0 = Release|Any CPU
{9BF4A280-C2CF-4EA1-9B5C-1FC600BB4AA3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9BF4A280-C2CF-4EA1-9B5C-1FC600BB4AA3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9BF4A280-C2CF-4EA1-9B5C-1FC600BB4AA3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9BF4A280-C2CF-4EA1-9B5C-1FC600BB4AA3}.Release|Any CPU.Build.0 = Release|Any CPU
{5AC81D60-8D19-45F6-96D8-2A7BBF5C311F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5AC81D60-8D19-45F6-96D8-2A7BBF5C311F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5AC81D60-8D19-45F6-96D8-2A7BBF5C311F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5AC81D60-8D19-45F6-96D8-2A7BBF5C311F}.Release|Any CPU.Build.0 = Release|Any CPU
{A9967817-2A2C-4C6D-A133-967A6062E9B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A9967817-2A2C-4C6D-A133-967A6062E9B3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A9967817-2A2C-4C6D-A133-967A6062E9B3}.Release|Any CPU.ActiveCfg = Release|Any CPU
Expand Down Expand Up @@ -211,6 +223,8 @@ Global
{45D47A4E-AD58-40C8-B4CC-95BC888C47A7} = {84AD662A-4B9E-4E64-834D-72529FB7FCE5}
{D3B72031-D4BD-44D3-973C-2752AB1570F6} = {84AD662A-4B9E-4E64-834D-72529FB7FCE5}
{2889E6B2-9CD1-437C-A43C-98CFAFF68B99} = {D59F1489-5D74-4F52-B78B-88037EAB2838}
{9BF4A280-C2CF-4EA1-9B5C-1FC600BB4AA3} = {84AD662A-4B9E-4E64-834D-72529FB7FCE5}
{5AC81D60-8D19-45F6-96D8-2A7BBF5C311F} = {D59F1489-5D74-4F52-B78B-88037EAB2838}
{D9356867-0A30-4B17-BD4C-0F7EF70984C6} = {84AD662A-4B9E-4E64-834D-72529FB7FCE5}
{DC20ACF9-12E9-41D9-B672-CB5FD85548E9} = {84AD662A-4B9E-4E64-834D-72529FB7FCE5}
{4FC45D20-0BA9-484B-9040-641687659AF6} = {D59F1489-5D74-4F52-B78B-88037EAB2838}
Expand Down
2 changes: 2 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
<PackageVersion Include="Microsoft.EntityFrameworkCore.InMemory" Version="$(EFCoreVersion)" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Proxies" Version="$(EFCoreVersion)" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="$(EFCoreVersion)" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="$(EFCoreVersion)" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="$(EFCoreVersion)" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="$(EFCoreVersion)" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="$(DotNetVersion)" />
Expand All @@ -38,6 +39,7 @@
<PackageVersion Include="NSubstitute" Version="5.3.0" />
<PackageVersion Include="NSwag.AspNetCore" Version="14.7.1" />
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.2" />
<PackageVersion Include="SQLite3MC.PCLRaw.bundle" Version="2.3.5" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="10.2.1" />
<PackageVersion Include="System.Formats.Asn1" Version="$(DotNetVersion)" />
<PackageVersion Include="System.Linq.Async" Version="7.0.0" />
Expand Down
123 changes: 123 additions & 0 deletions docs/in-depth/client/encryption.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# Encrypted offline store

The offline store is a standard Entity Framework Core SQLite database, so by default it is **not** encrypted. If your application stores sensitive data offline, you can encrypt the database file on disk.

## Background: SQLitePCLRaw 3 and encryption

Historically, applications enabled SQLite encryption by swapping the default `SQLitePCLRaw.bundle_e_sqlite3` bundle for `SQLitePCLRaw.bundle_e_sqlcipher`. As of **SQLitePCLRaw 3.0** (pulled in by EF Core 10), the free encryption bundles (`bundle_e_sqlcipher` and `bundle_e_sqlite3mc`) are no longer distributed, and the maintainer's recommended replacement &mdash; the SQLite Encryption Extension (SEE) &mdash; requires a **paid license**.

The `CommunityToolkit.Datasync.Client.EncryptedSqlite` package provides encryption **without a paid third-party license** by using [SQLite3 Multiple Ciphers](https://github.com/utelle/SQLite3MultipleCiphers) (SQLite3MC) &mdash; an open-source, MIT-licensed encryption engine that is compatible with SQLCipher database files.

!!! warning Reference exactly one SQLitePCLRaw bundle
A project may reference only **one** SQLitePCLRaw bundle. The base `CommunityToolkit.Datasync.Client` package uses the bundle-less `Microsoft.EntityFrameworkCore.Sqlite.Core` provider so that you can choose the native library:

* For a **plaintext** offline store, add `SQLitePCLRaw.bundle_e_sqlite3`.
* For an **encrypted** offline store, add `CommunityToolkit.Datasync.Client.EncryptedSqlite` (which brings the SQLite3MC bundle).

Do not reference both. Two bundles produce a duplicate `SQLitePCL.Batteries_V2` and will not compile.

## Set up

1. Install the `CommunityToolkit.Datasync.Client.EncryptedSqlite` package from NuGet. Do not add any other SQLitePCLRaw bundle to the application.

2. Configure your `OfflineDbContext` to use the encrypted store via `UseEncryptedSqlite`, supplying the encryption key from secure storage (for example the platform keychain/keystore):

public class AppDbContext : OfflineDbContext
{
private readonly string encryptionKey;

public AppDbContext(string encryptionKey)
{
this.encryptionKey = encryptionKey;
}

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (!optionsBuilder.IsConfigured)
{
optionsBuilder.UseEncryptedSqlite("Data Source=app.db", this.encryptionKey);
}

base.OnConfiguring(optionsBuilder);
}

protected override void OnDatasyncInitialization(DatasyncOfflineOptionsBuilder optionsBuilder)
{
optionsBuilder.UseEndpoint(new Uri("https://YOURSITEHERE.azurewebsites.net/"));
}

public DbSet<TodoItem> TodoItems => Set<TodoItem>();
}

The `UseEncryptedSqlite` extension lives in the `Microsoft.EntityFrameworkCore` namespace (like the built-in `UseSqlite`), so no additional `using` directive is required for the common case.

!!! note Never hard-code the key
The encryption key should come from a secure source such as the device keychain/keystore, a user-derived passphrase, or a secret store. Do not hard-code it in source or configuration.

## Generate and store the key on first run

The toolkit does not generate or store the key for you &mdash; that is the application's responsibility. A common pattern is to **generate a random key the first time the application runs and store it in the platform secure store**, then reuse the same key on every later launch. The database can only be opened with that key, so keep it safe and consider backing it up if losing it would mean losing the data.

On .NET MAUI, use [`SecureStorage`](https://learn.microsoft.com/dotnet/maui/platform-integration/storage/secure-storage) (backed by the iOS/macOS Keychain, the Android KeyStore, and Windows DPAPI):

using System.Security.Cryptography;
using Microsoft.Maui.Storage;

public static class EncryptionKeyProvider
{
private const string KeyName = "offline-db-key";

public static async Task<string> GetOrCreateKeyAsync()
{
string? key = await SecureStorage.Default.GetAsync(KeyName);
if (string.IsNullOrEmpty(key))
{
// First run: generate a 256-bit key and persist it to the secure store.
key = Convert.ToBase64String(RandomNumberGenerator.GetBytes(32));
await SecureStorage.Default.SetAsync(KeyName, key);
}

return key;
}
}

Resolve the key before configuring the context (for example during start-up) and pass it to `UseEncryptedSqlite`. The [TodoApp.MAUI.Encrypted sample](../../samples/todoapp/maui-encrypted.md) wires this up end-to-end.

### On other platforms

`SecureStorage` is MAUI-only. On other clients, generate the key the same way (`RandomNumberGenerator.GetBytes(32)`, base64-encoded, created once and reused) but persist it in that platform's secure store:

* **Windows / WinUI / WPF** &mdash; Windows Credential Locker (`PasswordVault`) or DPAPI (`ProtectedData`).
* **macOS / iOS** &mdash; the Keychain.
* **Android** &mdash; the Android KeyStore (for example via EncryptedSharedPreferences).
* **Linux** &mdash; the Secret Service API / libsecret (GNOME Keyring, KWallet).
* **Avalonia / Uno Platform** &mdash; use the platform options above, or a community secure-storage plugin for your framework.

In every case the rule is the same: generate once on first run, store it securely, and never hard-code it.

## Changing the key

Use `RekeyEncryptedSqlite` on a connection opened with the current key to re-encrypt the database with a new key:

using CommunityToolkit.Datasync.Client.EncryptedSqlite;

using SqliteConnection connection = EncryptedSqliteFactory.CreateConnection("Data Source=app.db", currentKey);
connection.RekeyEncryptedSqlite(newKey);

## Opening an existing SQLCipher database

If you are migrating from a database created with SQLCipher, select the `sqlcipher` cipher (and the matching legacy compatibility level) when opening the connection, then pass that connection to `UseEncryptedSqlite`:

using CommunityToolkit.Datasync.Client.EncryptedSqlite;

EncryptedSqliteOptions options = new() { Cipher = "sqlcipher", LegacyCompatibility = 4 };
SqliteConnection connection = EncryptedSqliteFactory.CreateConnection("Data Source=legacy.db", key, options);

// The caller owns the connection and is responsible for disposing it.
optionsBuilder.UseEncryptedSqlite(connection);

The `CreateConnection` helper opens and keys the connection for you; because the context does not own the connection, dispose it yourself when you are finished.

## Support and further information

For more information about the underlying encryption engine and the available cipher schemes, review the [SQLite3 Multiple Ciphers documentation](https://utelle.github.io/SQLite3MultipleCiphers/).
3 changes: 3 additions & 0 deletions docs/in-depth/client/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ Use the `OfflineDbContext` as the base for your offline storage:

The Datasync Community Toolkit does not rely on the storage of the `UpdatedAt` field in your model for synchronization.

!!! note Choosing a SQLite native library
The client uses the bundle-less `Microsoft.EntityFrameworkCore.Sqlite.Core` provider, so your application must reference exactly one SQLitePCLRaw bundle: add `SQLitePCLRaw.bundle_e_sqlite3` for a plaintext store, or the `CommunityToolkit.Datasync.Client.EncryptedSqlite` package for an [encrypted offline store](./encryption.md).

Each synchronizable entity in an offline context **MUST** have the following properties:

* `Id` - string, primary key - the globally unique ID for the entity.
Expand Down
30 changes: 30 additions & 0 deletions docs/samples/todoapp/maui-encrypted.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# TodoApp client for MAUI (encrypted offline store)

`TodoApp.MAUI.Encrypted` is a copy of the [MAUI sample](./maui.md) that stores its data in an **encrypted, on-disk** SQLite database instead of an in-memory one. It demonstrates the [encrypted offline store](../../in-depth/client/encryption.md) and, in particular, how to **generate the encryption key on first run and keep it in the platform secure store**.

The original `TodoApp.MAUI` sample is left unchanged; use this project when you want to see encryption wired up end-to-end.

## What is different from `TodoApp.MAUI`

* **Encrypted file store.** The database lives at `Path.Combine(FileSystem.AppDataDirectory, "todoapp.db")` and is configured with `UseEncryptedSqlite` (from the `CommunityToolkit.Datasync.Client.EncryptedSqlite` package) rather than `UseSqlite` over an in-memory connection.
* **Key generated on first run.** `Services/EncryptionKeyProvider.cs` adds a `SecureStorageEncryptionKeyProvider` that, on first launch, generates a cryptographically strong 256-bit key (`RandomNumberGenerator.GetBytes(32)`, base64) and stores it in MAUI [`SecureStorage`](https://learn.microsoft.com/dotnet/maui/platform-integration/storage/secure-storage) (Keychain on iOS/macOS, KeyStore on Android, DPAPI on Windows). Every later launch loads the same key.
* **Key resolved once at start-up.** `App.xaml.cs` resolves the key from a single point before configuring the database, so the first-run generation cannot race.

!!! warning Losing the key means losing the data
The encrypted database can only be opened with the key it was created with. If the stored key is cleared or changed, the existing offline data becomes permanently unreadable. Treat the key as the root secret protecting the local store.

## Run the application

* [Configure Visual Studio for MAUI development](https://learn.microsoft.com/dotnet/maui/get-started/installation).
* Open `samples/todoapp/Samples.TodoApp.sln` in Visual Studio.
* In the Solution Explorer, right-click the `TodoApp.MAUI.Encrypted` project, then select **Set as Startup Project**.
* Select a target (in the top bar), then press F5 to run the application.

After the first run you can confirm the store is encrypted: the `todoapp.db` file under the app data directory does **not** begin with the plaintext `SQLite format 3` header, and a `todoapp-offline-db-key` entry is present in the platform secure store.

!!! note Referencing the package
Until `CommunityToolkit.Datasync.Client.EncryptedSqlite` is published to NuGet, this sample references the toolkit projects from source (see the comment in `TodoApp.MAUI.Encrypted.csproj`). Once the package ships, replace those `ProjectReference`s with the corresponding `PackageReference`s.

## Enabling datasync operations

Adding offline synchronization is identical to the [MAUI sample](./maui.md#update-the-application-for-datasync-operations) &mdash; the encryption only changes how the local store is opened, not how push/pull work.
2 changes: 2 additions & 0 deletions mkdocs.shared.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,15 @@ nav:
- The basics: in-depth/client/index.md
- Authentication: in-depth/client/auth.md
- Online operations: in-depth/client/online.md
- Encrypted offline store: in-depth/client/encryption.md
- Advanced topics:
- MAUI AOT: in-depth/client/advanced/maui-aot.md
- Samples:
- Todo App:
- The server: samples/todoapp/server.md
- Avalonia: samples/todoapp/avalonia.md
- MAUI: samples/todoapp/maui.md
- "MAUI (encrypted)": samples/todoapp/maui-encrypted.md
- "Uno Plaform": samples/todoapp/unoplatform.md
- WinUI3: samples/todoapp/winui3.md
- WPF: samples/todoapp/wpf.md
Loading