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 字段加密
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 细节。


