SQLCipher で暗号化された SQLite(*.db3)を EF Core 7 の Database First で逆生成しようとすると、多くの人が「file is not a database」や「e_sqlite3 は暗号化をサポートしない」というエラーで止まります。本記事では、その原因を設計時ランタイムの仕組みから解体し、最短でテーブル定義を取り出す実践手順と、将来にわたりトラブルを回避する設計指針までを丁寧に解説します。
問題の射程:なぜ「暗号化 SQLite → Scaffold」が失敗するのか
結論から言うと、EF Core 7 の公式 SQLite プロバイダー(Microsoft.EntityFrameworkCore.Sqlite)自体には暗号化機能がなく、設計時(design-time)の逆生成パイプラインが SQLCipher の鍵を設定できないためです。つまり、暗号化 DB を開く前にキーを渡せない構造になっており、ヘッダー判別で「これは通常の SQLite ファイルではない(= 平文ヘッダー SQLite format 3 ではない)」と判断されて失敗します。
再現環境の一例
- .NET 7 / EF Core 7
- プロジェクトに
Microsoft.EntityFrameworkCore.Sqliteを追加済み - DB ファイルは SQLCipher でパスワード付き(
.db3)
代表的なエラー
SQLite Error 26: 'file is not a database'
You specified a password in the connection string, but the native SQLite library 'e_sqlite3' doesn't support encryption.
根本原因を構造から理解する
- 暗号化は SQLite 本体の標準機能ではない
SQLCipher などの派生実装や商用ビルドで提供される機能です。公式のe_sqlite3ネイティブライブラリは暗号化 API を持ちません。 - design-time では「キーを送る場所」がない
Scaffold-DbContextは、与えられた接続文字列でスキーマを読み出します。まだDbContextすら存在しない段階なので、OnConfiguringなどでPRAGMA keyを実行する余地がありません。 - 設計時と実行時で読み込まれるネイティブ DLL がズレやすい
アプリ実行時にSQLitePCLRaw.bundle_e_sqlcipherを導入しても、dotnet-ef/ PMC(パッケージ マネージャー コンソール)から走る設計時は別プロセス。暗号化対応の DLL が読み込まれずに失敗しがちです。
最短の現実解:平文に一時変換して逆生成(推奨)
「今すぐテーブル定義を取り出したい」が要件なら、一時的に平文化してから逆生成するのが最短の現実解です。設計思想としては「設計時は平文、実行時は暗号化」という二相運用に分けます。
手順の全体像
| ステップ | やること | ポイント |
|---|---|---|
| 1 | 暗号化 DB を平文化(.db) | GUI ツール(SQLiteStudio / DB Browser for SQLCipher)または SQLCipher CLI で sqlcipher_export |
| 2 | 平文 DB から Scaffold | Scaffold-DbContext "Data Source=...plain.db" Microsoft.EntityFrameworkCore.Sqlite |
| 3 | 生成コードをプロジェクトへ組み込み | エンティティ/DbContext をリネーム・整理 |
| 4 | 実行時は暗号化 DB を使う | 公式プロバイダー+SQLCipher ネイティブを読み込み、接続直後に PRAGMA key を実行 |
SQLCipher CLI での平文化(例)
# Windows の例(sqlcipher.exe のあるディレクトリで)
sqlcipher.exe secure.db3
PRAGMA key = 'Your$trongP@ss';
ATTACH DATABASE 'plain.db' AS plaintext KEY '';
SELECT sqlcipher_export('plaintext');
DETACH DATABASE plaintext;
.exit
これで plain.db(平文)が得られます。パスワードに $ など PowerShell が解釈する文字を含む場合、PowerShell の文字列リテラルでは `$ のようにエスケープしてください。
Scaffold の実行例(PMC と CLI)
パッケージ マネージャー コンソール(PMC)
Scaffold-DbContext `
"Data Source=C:\data\plain.db" `
Microsoft.EntityFrameworkCore.Sqlite `
-OutputDir Models `
-Context AppDbContext `
-DataAnnotations
dotnet-ef CLI
dotnet ef dbcontext scaffold \
"Data Source=/data/plain.db" \
Microsoft.EntityFrameworkCore.Sqlite \
--output-dir Models \
--context AppDbContext \
--data-annotations
実行時は暗号化 DB へ戻す(PRAGMA key)
ランタイムでは、SQLCipher ネイティブ(e_sqlcipher)を読み込ませたうえで、接続直後にキーを設定します。接続文字列の Password=... は使いません(公式 Microsoft.Data.Sqlite はそれを解釈してネイティブ暗号化を呼びません。むしろエラーになります)。
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
public sealed class AppDbContext : DbContext
{
private readonly string _dbPath;
private readonly string _password;
public AppDbContext(string dbPath, string password)
{
_dbPath = dbPath;
_password = password;
// SQLCipher バイナリを初期化(bundle_e_sqlcipher を参照に追加しておく)
SQLitePCL.Batteries_V2.Init();
}
protected override void OnConfiguring(DbContextOptionsBuilder options)
{
var csb = new SqliteConnectionStringBuilder
{
DataSource = _dbPath,
Mode = SqliteOpenMode.ReadWriteCreate,
Cache = SqliteCacheMode.Shared
// Password は指定しない(Microsoft.Data.Sqlite では未サポート)
};
var connection = new SqliteConnection(csb.ToString());
connection.Open();
// <-- ここが重要:最初の読み取り前にキーを設定する
using (var cmd = connection.CreateCommand())
{
cmd.CommandText = "PRAGMA key = $p; " +
"PRAGMA cipher_page_size = 4096; " +
"PRAGMA kdf_iter = 64000; " +
"PRAGMA cipher_hmac_algorithm = HMAC_SHA512; " +
"PRAGMA cipher_kdf_algorithm = PBKDF2_HMAC_SHA512;";
cmd.Parameters.AddWithValue("$p", _password);
cmd.ExecuteNonQuery();
}
options.UseSqlite(connection);
}
}
NuGet の目安
Microsoft.EntityFrameworkCore.SqliteSQLitePCLRaw.bundle_e_sqlcipher(必須)SQLitePCLRaw.core(通常は依存で入る)
生成済みのモデルをそのまま使い、DbContext だけ暗号化 DB に向ける構成です。
代替アプローチとそのトレードオフ
| 選択肢 | 概要 | メリット | デメリット/リスク | 向いているケース |
|---|---|---|---|---|
| A. 一時的に平文化して Scaffold | 平文 DB を作って逆生成、実行時は暗号化 DB を使用 | 最短で完了。公式プロバイダーのみで安定 | 設計時と実行時で DB が異なる。手順が増える | まずモデルを取り出したい、確実に動かしたい |
| B. Database First を捨て Code First へ | POCO を手書き→マイグレーションで DB 作成 | 設計が整理される。長期メンテが容易 | 既存 DB からの自動生成は不可。初期負荷 | 新規開発/設計を見直したいとき |
| C. SQLCipher 対応のサードパーティ | 暗号化対応プロバイダーで逆生成を狙う | 暗号化のまま自動逆生成できる可能性 | 動作保証・更新性は自己責任。学習コスト | 商用サポートや社内標準がある場合 |
| D. そもそも暗号化しない | 拡張子難読化などで軽く隠す | Scaffold は最も簡単 | セキュリティ強度は低い | 機微性の低いデータ、プロトタイプ |
設計時と実行時の「読み込まれる DLL の差」を抑え込む
暗号化の話で最もハマりやすいのが「設計時プロセスが別のネイティブを読んでしまう」問題です。以下のチェックリストで差分を潰しましょう。
- RID(Runtime Identifier)の不一致
x64 のプロジェクトなのに x86 のツールチェーンを呼ぶとe_sqlcipherを解決できません。dotnet --infoでアーキテクチャを確認。 - bundle の衝突
SQLitePCLRaw.bundle_e_sqlite3とbundle_e_sqlcipherを同時に参照すると、暗号化なしのほうが先に初期化されることがあります。片方に寄せ、初期化呼び出しを明示(SQLitePCL.Batteries_V2.Init())。 - 環境変数・カレントディレクトリ依存
CI / Docker ではネイティブ DLL の配置がズレやすい。runtimes/<rid>/nativeに DLL をパッケージングするのが安全です。
PowerShell / シェルのクォート地獄を避ける
$、!、" などはシェルに解釈されます。パスワードや接続文字列を渡す際は以下の指針を守ると事故が減ります。
- PowerShell:
`$、`"のようにバッククォートでエスケープ。複雑なら$env:...で環境変数に入れる。 - Bash:シングルクォートで全体を囲み、内部のシングルクォートは
'\''で分割。
$env:DBPWD = 'P@ss`$ecret!"'
dotnet ef dbcontext scaffold "Data Source=C:\db\plain.db" Microsoft.EntityFrameworkCore.Sqlite
マイグレーション適用時の注意:毎回キーを設定する
マイグレーションを dotnet ef database update で適用する際も、「最初の読み取り前にキー」が鉄則です。アプリ側で IDesignTimeDbContextFactory<T> を実装し、設計時の DbContext 生成でも PRAGMA key を確実に実行させます。平文 DB を使う場合は、設計時コンテキストだけ別の接続先に向ける手もあります。
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.Data.Sqlite;
public sealed class DesignTimeFactory : IDesignTimeDbContextFactory<AppDbContext>
{
public AppDbContext CreateDbContext(string[] args)
{
// 例:設計時は平文 DB を参照(環境変数で切替)
var usePlain = Environment.GetEnvironmentVariable("EF_USE_PLAIN_DB") == "1";
var dbPath = usePlain ? "C:\\data\\plain.db" : "C:\\data\\secure.db3";
var pwd = Environment.GetEnvironmentVariable("DBPWD") ?? "";
var options = new DbContextOptionsBuilder<AppDbContext>();
var conn = new SqliteConnection(new SqliteConnectionStringBuilder
{
DataSource = dbPath,
}.ToString());
conn.Open();
if (!usePlain)
{
using var cmd = conn.CreateCommand();
cmd.CommandText = "PRAGMA key = $p;";
cmd.Parameters.AddWithValue("$p", pwd);
cmd.ExecuteNonQuery();
}
options.UseSqlite(conn);
return new AppDbContext(dbPath, pwd);
}
}
このように「設計時は平文」「実行時は暗号化」と明確に分けておくと、安全かつ再現性の高いパイプラインになります。
Code First へ切り替える場合の最短コース
既存 DB のスキーマを完全自動では取り込めませんが、長期的な保守を考えると Code First へ舵を切る選択も合理的です。ざっくりの流れは次のとおり。
- 最小限の POCO と
DbContextを作る dotnet ef migrations add Init→dotnet ef database update(設計時は平文/実行時は暗号化)- 必要なインデックス・制約はマイグレーションで追加
public sealed class Person
{
public long Id { get; set; }
public string Name { get; set; } = default!;
}
public sealed class AppDbContext : DbContext
{
protected override void OnModelCreating(ModelBuilder model)
{
model.Entity().ToTable("person");
model.Entity().HasKey(x => x.Id);
model.Entity().Property(x => x.Name).HasColumnType("TEXT").IsRequired();
}
}
サードパーティ製プロバイダーを使う場合の現実的な見積もり
暗号化を保ったまま逆生成したい、という要望に対し、SQLCipher 対応を標榜するコミュニティ/商用プロバイダーを採用する方法もあります。ただし、以下の確認を怠ると結局ハマります。
- 設計時ツール(
dotnet-ef/ PMC)からも 同じネイティブ DLL が確実にロードされるか Password=または独自のキーワードで 設計時に鍵を渡せるか(内部でPRAGMA keyを実行してくれるか)- ターゲット OS / アーキテクチャ(Windows x64 / Linux musl / macOS)向けに 全てのネイティブ配布物 が揃っているか
- 更新ポリシー(セキュリティ修正の追従、EF Core メジャーアップデート対応)
導入時は PoC(検証)用リポジトリを作り、CI まで含めて Scaffold → ビルド → マイグレーション適用 → 実行 が通ることを自動テストで担保するのが安全です。
「file is not a database」を切り分けるための現場テクニック
- ヘッダーを確認:平文 SQLite は先頭 16B が
SQLite format 3\0。SQLCipher の場合、ランダム化されているため一致しません。 - SQLCipher 上での自己診断:
PRAGMA cipher_version;が通るなら暗号化対応ビルド。通らないなら通常ビルド。 - 接続直後に必ず
PRAGMA key:1 レコードでも読む前にキーを設定しないとfile is not a databaseに直行します。
セキュリティ設計の現実解(キー管理)
- 接続文字列にパスワードを直書きしない:
Password=は未サポートかつ漏洩リスク。PRAGMA keyへパラメータで渡す。 - キーは OS の保護機構に委譲:Windows は DPAPI(ユーザー/マシン証明書)、Linux は Keyring、macOS は Keychain の利用を検討。
- ログ出力を抑止:EF Core の
EnableSensitiveDataLoggingは本番で無効。SQL ログに鍵を混入させない。 - キー更新手順:
ATTACH ... KEY 'new'; SELECT sqlcipher_export('...'); DETACH;でのローテーション手順を Runbook 化。
よくある落とし穴と対処法
| 症状 | 原因 | 対応策 |
|---|---|---|
You specified a password... | Password= を使った | 使わずに PRAGMA key を実行する |
file is not a database | キー未設定のまま読み取り | 接続直後に PRAGMA key。先に SELECT 1 などを実行しない |
| 設計時だけ失敗する | ネイティブ DLL 差し替え漏れ | bundle_e_sqlcipher へ寄せ、RID を揃える |
| Linux で動かない | musl / glibc 差異 | 対象ディストリに合うネイティブを含める。Docker ではベースイメージと一致させる |
| CI でのみ DllNotFound | 出力ディレクトリに配布されていない | runtimes/<rid>/native の配布確認。Publish 時のトリム設定も確認 |
逆生成後にやっておきたい仕上げ
- 命名の正規化:テーブル・列名のパスカルケース化、
Fluent APIでの制約再現。 - ValueConverter:
INTEGERのboolマップ、TEXTのDateTimeOffsetなどの型整備。 - インデックス・外部キー:実 DB に合わせて
HasIndex/HasConstraintNameを調整。 - RowVersion 的管理:SQLite は楽観ロックを自前実装しやすい。
updated_atやxmin代替を設計。
ケース別の最適解の選び方
| 前提 | おすすめ | 理由 |
|---|---|---|
| とにかく早くモデルが欲しい | A 一時平文化 → Scaffold | 最短で成功率が高い |
| 長期運用・規模が増える | B Code First へ移行 | スキーマの履歴管理と自動化が容易 |
| 社内で SQLCipher プロバイダーが標準 | C サードパーティ採用 | 暗号化のまま設計時を回せる可能性 |
| 機微性が低く試作段階 | D 暗号化なしで逆生成 | 速度優先。のちほど再暗号化すればよい |
チェックリスト:明日から困らないために
- 設計時(Scaffold / Migrations)と実行時で、どの DB を使うかを明文化したか。
- 暗号化キーは 接続直後に PRAGMA で設定しているか(最初の SELECT より前)。
- ネイティブ DLL は SQLCipher のものに統一されているか(bundle の衝突なし)。
- CI / 配布物に RID ごとのネイティブが含まれているか。
- キーは OS の秘密ストアで保護・ローテーション可能になっているか。
まとめ
EF Core 7 の Scaffold-DbContext は、暗号化された SQLite(SQLCipher)を直接読み解く前提で作られていません。設計時にキーを渡せない構造と、暗号化非対応の e_sqlite3 によって「file is not a database」等の典型的なエラーが生まれます。最短でスキーマを取り出すなら「一時平文化 → Scaffold → 実行時は暗号化に戻す」が最も堅実です。長期的には Code First へ移り、暗号化前提のパイプライン(設計時の平文切替・PRAGMA key の徹底・ネイティブ DLL の統一・キー管理の標準化)を整えることで、逆生成・マイグレーション・本番運用までを安定させられます。
付録:A〜D の手順をもう一歩具体的に
A. 一時平文化 → Scaffold(詳細)
- GUI ツールまたは SQLCipher CLI で平文化(
sqlcipher_export) plain.dbからScaffold-DbContextを実行- 生成された
AppDbContextのOnConfiguringを削除し、上掲の「PRAGMA key」対応版AppDbContextに差し替え - 本番設定:キーは環境変数・OS 秘密ストアから取得。ログへ出さない
B. Code First へ移行(詳細)
- 最低限のエンティティを実装。テーブル名・列名の一致を
Fluent APIで再現 - マイグレーションで DB を作成(設計時は平文、実行時は暗号化)
- 既存 DB の実データ移行が必要なら、
sqlcipher_exportで平文 DB に吐き出し、アプリで取り込み
C. サードパーティ採用(見積もり手順)
- PoC リポジトリを作成。対象プロバイダーのサンプルに沿って最小コードを作る
dotnet-efの設計時でも鍵が渡るか、完全自動でScaffoldできるか確認- Windows / Linux / macOS それぞれで publish → 実行。ネイティブ DLL の解決をチェック
- ライセンスとサポート SLA、更新頻度を評価し、採用可否を判断
D. 暗号化しないで逆生成(注意点)
- プロトタイプ段階のみ。機微性が高いデータでは使用しない
- 逆生成後は必ず暗号化 DB を再作成し、本番は暗号化運用へ戻す
ここまで実装・運用の両面から整理しておけば、暗号化 SQLite × EF Core 7 の「逆生成できない」問題は再発しません。日々の開発とチーム運用のストレスを確実に減らせます。

コメント