DB-hub Technology 未分类 SQLite数据库加密

SQLite数据库加密

1.SQLite vs SQLCipher

SQLite 本身(开源官方版本)是不带“内置密码”功能的,也就是说,直接对普通 sqlite3 创建的数据库文件是不能简单用一个密码开关来加密的。

常见做法有几种:

  • 使用加密扩展或分支:
    SQLCipher、SEE(SQLite Encryption Extension,官方收费版)、wxSQLite3 等,这些都是在 SQLite 基础上加入透明加密的实现,数据库文件本身是加密的,打开时需要密钥或密码。
  • 在应用层自己加密:
    把敏感字段在写入前用 AES 等算法加密,只把密文存进 SQLite;读出来后在应用内解密。
  • 文件/磁盘层加密:
    不改 SQLite,用操作系统或磁盘工具(如 BitLocker、LUKS、全盘加密等)加密存放数据库的目录或磁盘分区

几种方案对比

SQLCipher

  • SQLCipher 是在 SQLite 基础上改造的库,负责把整个数据库文件做透明 AES 加密。
  • SQLCipher 有完全开源免费的版本(社区版),采用 256-bit AES,对整个 SQLite 数据文件做透明加密,源码在 GitHub 上,遵循开源许可证,可以免费在商用项目中使用(需遵守相应许可证条款)。
  • SQLCipher 也提供收费的商业版/企业版,主要是预编好的静态库、集成更简单、编译更快等“工程便利性”上的差异,核心加密功能与免费社区版一致。

Python 的 SQLCipher 绑定(比如 pysqlcipher3 或 sqlcipher3)来操作加密 SQLite,整体模式是:安装绑定 → 用它代替 sqlite3 连接 → 连接后立即执行 PRAGMA key 设置密码,然后像普通 SQLite 一样用。

安装与导入
Windows 上 pysqlcipher3 或 sqlcipher3 需要先有 SQLCipher 库、OpenSSL 等,本地编译比较折腾,常见问题就是你看到的那类错误(缺头文件、缺 lib)。

pip install sqlcipher3
error: command 'C:\\Program Files (x86)\\Microsoft Visual Studio\\2019\\BuildTools\\VC\\Tools\\MSVC\\14.29.30133\\bin\\HostX86\\x64\\cl.exe' failed with exit code 2
      [end of output]

  note: This error originates from a subprocess, and is likely not a problem with pip.
  ERROR: Failed building wheel for sqlcipher3
Failed to build sqlcipher3
error: failed-wheel-build-for-install

× Failed to build installable wheels for some pyproject.toml based projects
╰─> sqlcipher3

换成“带现成 wheel 的包”:
社区有人做了预编译 wheel 的第三方发行版(比如 sqlcipher3-wheels 类似项目),可以避免你本机编译 C 扩展,但名字、支持的 Python 版本可能会变。

pip install sqlcipher3-wheels

然后在代码里:

from sqlcipher3 import dbapi2 as sqlite

创建或打开加密数据库:

from sqlcipher3 import dbapi2 as sqlite

conn = sqlite.connect("notes.db")
cur = conn.cursor()


# 设置密钥(第一次会创建加密库,以后是解密)
cur.execute("PRAGMA key = 'your-secret-password';")

这一步相当于告诉 SQLCipher 用哪个口令派生密钥;如果文件不存在,会创建一个加密库,如果已存在则用该口令尝试解密。

像普通 SQLite 一样用:

cur.execute("""
    CREATE TABLE IF NOT EXISTS users (
        id   INTEGER PRIMARY KEY AUTOINCREMENT,
        name TEXT
    )
""")
cur.execute("INSERT INTO users(name) VALUES (?)", ("Alice",))
conn.commit()

cur.execute("SELECT id, name FROM users")
rows = cur.fetchall()

除去 PRAGMA key,其余 CRUD 基本与 sqlite3 相同。

关闭连接:

cur.close()
conn.close()

打开已有加密数据库

要打开之前用同一套参数加密过的库,只需要重复“连接 → PRAGMA key”流程,密码要一致,否则会报“file is encrypted or is not a database”之类错误:

conn = sqlite.connect("notes.db")
cur = conn.cursor()
cur.execute("PRAGMA key = 'your-secret-password';")
cur.execute("SELECT count(*) FROM sqlite_master;")
print(cur.fetchall())

修改密码(轮换密钥)

可以用 PRAGMA rekey 换密码,SQLCipher 会重写整个文件:

cur.execute("PRAGMA key = 'old-password';")
cur.execute("PRAGMA rekey = 'new-password';")
conn.commit()

以后连接就要用新密码。

密钥管理: 用户口令派生密钥

  • 数据库: 使用 SQLCipher 加密整个库。

  • 第一次启动时让用户设置一个“主密码”(类似密码管理器)。

  • 使用 KDF(如 PBKDF2、Argon2、scrypt)从主密码派生出 SQLCipher 密钥:

  • 在数据库外存一份随机 salt。

  • 每次启动让用户输入主密码 → 用 salt 做 KDF → 得到 key → 喂给 SQLCipher。

  • 字段级加密: 对极敏感字段(比如账号密码本体)在业务层再做一次 AES 加密(使用与 SQLCipher 不同的密钥派生路径),这样即便 SQLCipher 密钥泄露,攻击者也还要攻破第二层。

  • 支持“修改主密码”。

  • 提供导出/备份功能,提醒用户主密码遗失无法恢复。

  • 优点:
    即使程序被拷走,拿不到用户主密码也解不开数据库。
    不需要在磁盘上保存明文密钥。

  • 缺点:
    用户忘记主密码就无法恢复,需在 UX 上强调风险、提供导出/备份功能。

2.什么是盐?

如果salt丢失了,数据库就很容易被b破解了吗?
不会。把 salt 放在 config.json(甚至直接放在数据库里)并不会让数据库“很容易被破解”,这是标准做法,密码学上通常假设攻击者是知道 salt 的。真正提供安全性的,是强 KDF 和足够复杂的主密码,而不是把 salt 藏起来。

为什么盐可以公开保存?

  • 盐的作用是防止彩虹表和批量攻击:同一密码在不同盐下会导出完全不同的密钥,攻击者必须对每个盐单独暴力破解,而不能用一张表打所有数据。

  • 密码学模型中一般默认“算法、参数、盐对攻击者都是已知”的前提(Kerckhoffs 原则),只把密钥/密码当成秘密,因此大部分实践都会直接把盐和密文保存在同一个地方。

  • 你把盐放到 config.json 里,本质上就是“假设攻击者能拿到整个程序目录和数据库文件”,这跟业界常规的“盐和哈希一起存数据库”是一样的威胁模型,并不会额外削弱安全性。

换句话说:即使攻击者拿到了 salt + 加密后的数据库,要想解密,仍然必须暴力/字典猜主密码,而且每猜一次都要跑完整的 KDF;这才是你要让它“贵”的地方(高迭代 / 高成本 KDF + 强主密码),而不是依赖盐的保密性。

更关键的是下面这些:

  • 使用 PBKDF2/scrypt/Argon2 之类的 KDF,并设置足够高的成本参数(例如 PBKDF2 20 万~几十万次迭代,结合你的性能做权衡),让每次尝试都变得昂贵。

  • 引导用户设置足够长、随机度高的主密码(而不是“123456”、“qwerasdf”这种),否则再好的 KDF 也扛不住纯字典攻击。

  • 可以考虑增加一层“pepper”(额外的秘密),例如:

  • 把一个固定的随机值存到系统凭据(Windows 凭据管理器 / macOS Keychain 等)或环境变量里,KDF 输入为 password + salt + pepper;

  • 这样就算攻击者拿到了数据库 + config.json,也还缺这一块秘密,需要控制同一系统账户或更高权限才有机会取到。

3.什么是KDF?

KDF(Key Derivation Function,密钥派生函数)是一种密码学算法,用于从弱密码(如”123456ab”)生成强加密密钥(如你的 64 位 hex 串)。

为什么需要 KDF?

直接问题:

  • 用户密码通常短、易猜(如”123456″、”password”)

  • 加密算法(如 AES、SQLCipher)需要高熵、固定长度的密钥(256 位 = 32 字节)

  • 直接用弱密码当密钥 = 容易暴力破解

KDF 解决:

弱密码("12345678") + 随机盐(salt) + 高迭代次数(256000次) 
    ↓ PBKDF2 (你程序里用的 KDF)
强密钥(x'2dd29ca851e7b56e4697b0e1f08507293d761a05ce4d1b628663f411a8086d99')

程序里的 KDF 工作流程

# 1. 输入:用户密码 + 随机盐(config.json 里存的 16 字节)
master_key = "12345678"
salt = 从config.json读出的随机值

# 2. PBKDF2 执行 256000 次哈希运算
dk = hashlib.pbkdf2_hmac("sha512", master_key, salt, 256000, 64字节)

# 3. 输出:64 字节强密钥
db_key = dk[:32]     # 前32字节 → SQLCipher 整库加密
field_key = dk[32:]  # 后32字节 → AES-GCM 字段加密

KDF 的 4 个关键特性

常见 KDF 对比

PBKDF2 够用:在桌面 GUI 工具里,256k 迭代每次 ~100ms,用户能接受;抗 GPU 攻击也够用(除非国家级对手)。
安全效果总结

攻击者拿到你的 notes.db + config.json:

已知:加密库 + salt + KDF 参数
未知:12345678(主密码)

破解成本:256000 × 密码字典大小 × 哈希时间
       ≈ 几小时~几年(取决于密码强度)

这就是 KDF 的价值:把”人类能记住的弱密码”变成”计算机要算很久才能破解的强密钥”。
这就是为什么我们不直接把”12345678″喂给 SQLCipher,而是先让它过 KDF 这一关。

4.数据库工具

DBeaver 无法直接输入程序生成的 SQLCipher master key,因为 DBeaver 对 SQLCipher 的支持有限,不识别这种自定义 PBKDF2 派生的十六进制密钥格式(x’abc123…’)。
DBeaver 默认期望的是明文密码字符串或标准 SQLite,没有原生支持这种加密格式。

DB Browser for SQLite

  • 下载:https://sqlitebrowser.org/

  • 支持 SQLCipher(Encrypt Database)

  • 密码框直接支持 hex 格式:x’abc123…’ 或 raw hex 字符串

  • 打开 notes.db → Database Structure → Encrypt Database → 输入你的 db_key_hex

如何获取具体 hex key?

import json
import binascii
import hashlib

# 从你的 config.json 读取
with open("config.json", "r") as f:
    config = json.load(f)

salt_hex = config["kdf_salt"]
master_key = "12345678"  # 你输入的 master key
iterations = config["kdf_iter"]

# 计算 db_key
salt = binascii.unhexlify(salt_hex.encode("ascii"))
dk = hashlib.pbkdf2_hmac("sha512", master_key.encode("utf-8"), salt, iterations, dklen=64)
db_key_hex = binascii.hexlify(dk[:32]).decode("ascii")

print(f"SQLCipher hex key 是:")
print(f"x'{db_key_hex}'")
print(f"复制上面整行到 sqlcipher / DB Browser 的密码框")

DB Browser for SQLite
– 打开 sqlite.db

  • Database Encryption → SQLCipher

  • Password 框直接粘贴:x’2dd29ca851e7b56e4697b0e1f08507293d761a05ce4d1b628663f411a8086d99′

  • OK → 就能看到表结构和数据

5.完整示例

import os
import json
import binascii
import hashlib
from pathlib import Path
from sqlcipher3 import dbapi2 as sqlite

APP_DIR   = Path(".")
DB_PATH   = APP_DIR / "notes.db"
CONF_PATH = APP_DIR / "config.json"

# ----------------- KDF 相关工具函数 -----------------

def load_or_init_config() -> dict:
    """加载或初始化配置文件,主要是存 PBKDF2 的 salt 和迭代次数。"""
    if CONF_PATH.exists():
        with open(CONF_PATH, "r", encoding="utf-8") as f:
            return json.load(f)

    # 第一次运行时生成
    config = {
        "kdf_salt": binascii.hexlify(os.urandom(16)).decode("ascii"),  # 16 bytes salt
        "kdf_iter": 256_000,                                           # PBKDF2 迭代次数,可按性能调整
    }
    with open(CONF_PATH, "w", encoding="utf-8") as f:
        json.dump(config, f, indent=2)
    return config


def derive_keys_from_master(master_key: str, salt_hex: str, iterations: int):
    """
    使用 PBKDF2 从 master_key 派生 32 字节 root_key,
    再从中切出 db_key 和 field_key(各 32 字节)。
    """
    salt = binascii.unhexlify(salt_hex.encode("ascii"))
    master_bytes = master_key.encode("utf-8")

    # 派生 64 字节,前 32 做 db_key, 后 32 做 field_key
    dk = hashlib.pbkdf2_hmac(
        hash_name="sha512",
        password=master_bytes,
        salt=salt,
        iterations=iterations,
        dklen=64,
    )

    db_key_bytes    = dk[:32]
    field_key_bytes = dk[32:]

    # SQLCipher 习惯用十六进制 key,这里转成 hex 方便 PRAGMA 使用
    db_key_hex = binascii.hexlify(db_key_bytes).decode("ascii")
    return db_key_hex, field_key_bytes


# ----------------- 数据库打开/初始化 -----------------

def open_or_init_db(master_key: str):
    """
    用 master_key 经 PBKDF2 派生出的 db_key 打开/创建加密数据库,
    并返回 (conn, cursor, field_key_bytes)。
    """
    config = load_or_init_config()
    db_key_hex, field_key = derive_keys_from_master(
        master_key,
        salt_hex=config["kdf_salt"],
        iterations=config["kdf_iter"],
    )

    first_time = not DB_PATH.exists()

    conn = sqlite.connect(str(DB_PATH))
    cur = conn.cursor()

    # 注意这里用的是十六进制 key 形式:x'ABCD...'
    cur.execute(f"PRAGMA key = \"x'{db_key_hex}'\";")

    if first_time:
        # 第一次运行:创建表
        cur.execute("""
            CREATE TABLE users (
                name     TEXT NOT NULL,
                password BLOB NOT NULL,
                comment  TEXT
            );
        """)
        conn.commit()
    else:
        # 验证密钥是否正确
        try:
            cur.execute("SELECT name FROM sqlite_master LIMIT 1;")
            cur.fetchall()
        except Exception as e:
            cur.close()
            conn.close()
            raise RuntimeError("无法用当前 master key 打开已存在的加密数据库") from e

    return conn, cur, field_key


# ----------------- 示例主程序 -----------------

def main():
    master_key = input("请输入 master key: ").strip()

    conn, cur, field_key = open_or_init_db(master_key)
    print("数据库已打开。")
    print(f"field_key 长度: {len(field_key)} 字节(用于应用层字段加密)")

    # 这里暂时不对 password 字段再加密,只是演示流程;
    # 你可以之后写 encrypt_field(field_key, plaintext) 再存入 DB。
    cur.execute(
        "INSERT INTO users(name, password, comment) VALUES (?, ?, ?);",
        ("alice", b"plain-password", "first user"),
    )
    conn.commit()

    cur.execute("SELECT rowid, name, password, comment FROM users;")
    for row in cur.fetchall():
        print(row)

    cur.close()
    conn.close()


if __name__ == "__main__":
    main()

说明:

  • config.json 中只保存 salt 和 PBKDF2 的迭代次数,丢了盐就打不开原库,但盐本身不是秘密;它只是让每个数据库实例的密钥空间独立。

  • master_key 不直接用作 SQLCipher 密钥,而是先经过 PBKDF2 派生成 root_key,再切分成 db_key(hex 给 SQLCipher)和 field_key(原始 32 字节,后续可用于 AES‑GCM 等应用层字段加密)。

  • 可以在 field_key 上封装 encrypt_field/decrypt_field,对 users.password 或其他敏感列做二次加密,而 GUI 和业务逻辑都不需要接触 KDF/SQLCipher 细节。

Related Post