SQLCipher暗号化SQLiteをEF Core 7でScaffoldできない原因と対処法|Database First逆生成の完全ガイド

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 から ScaffoldScaffold-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.Sqlite
  • SQLitePCLRaw.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&lt;AppDbContext&gt;
{
    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&lt;AppDbContext&gt;();

        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 へ舵を切る選択も合理的です。ざっくりの流れは次のとおり。

  1. 最小限の POCO と DbContext を作る
  2. dotnet ef migrations add Init → dotnet ef database update(設計時は平文/実行時は暗号化)
  3. 必要なインデックス・制約はマイグレーションで追加
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(詳細)

  1. GUI ツールまたは SQLCipher CLI で平文化(sqlcipher_export)
  2. plain.db から Scaffold-DbContext を実行
  3. 生成された AppDbContext の OnConfiguring を削除し、上掲の「PRAGMA key」対応版 AppDbContext に差し替え
  4. 本番設定:キーは環境変数・OS 秘密ストアから取得。ログへ出さない

B. Code First へ移行(詳細)

  1. 最低限のエンティティを実装。テーブル名・列名の一致を Fluent API で再現
  2. マイグレーションで DB を作成(設計時は平文、実行時は暗号化)
  3. 既存 DB の実データ移行が必要なら、sqlcipher_export で平文 DB に吐き出し、アプリで取り込み

C. サードパーティ採用(見積もり手順)

  1. PoC リポジトリを作成。対象プロバイダーのサンプルに沿って最小コードを作る
  2. dotnet-ef の設計時でも鍵が渡るか、完全自動で Scaffold できるか確認
  3. Windows / Linux / macOS それぞれで publish → 実行。ネイティブ DLL の解決をチェック
  4. ライセンスとサポート SLA、更新頻度を評価し、採用可否を判断

D. 暗号化しないで逆生成(注意点)

  • プロトタイプ段階のみ。機微性が高いデータでは使用しない
  • 逆生成後は必ず暗号化 DB を再作成し、本番は暗号化運用へ戻す

ここまで実装・運用の両面から整理しておけば、暗号化 SQLite × EF Core 7 の「逆生成できない」問題は再発しません。日々の開発とチーム運用のストレスを確実に減らせます。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次