简介:这份源码资源面向使用C# 2012与VS2012开发64位Windows应用的开发者,重点解决SQLite嵌入式数据库的调用与密码设置问题。内容涵盖System.Data.SQLite集成、连接字符串配置、建表、增删改查等完整操作,并演示了在连接串中加入密码参数实现数据库加密保护的思路,适合需要为轻量级数据驱动程序增加安全机制的初中级C#开发者参考。压缩包共42个文件,约2.48MB,以cs源码、config配置、dll程序集、exe可执行文件、resx资源及sln解决方案为主,另含pdb调试符号与suo用户选项文件,结构完整可直接在VS2012中打开运行。目前已有850人学习下载。通过研读源码,读者可掌握SQLiteConnection、SQLiteCommand、SQLiteDataReader等核心类的实际用法,理解密码参数在连接层的处理方式,并借鉴其工程组织与配置管理经验,快速迁移到自己的数据管理项目中。
1. 64位C#调用SQLite带密码:一个被低估的工程细节
很多做 C# 上位机的朋友,第一次把 SQLite 数据库加上密码,都会经历一个相同的翻车现场:本地调试一切正常,程序一编译成 64 位、拷到客户机器上,打开数据库直接抛file is not a database。代码没改,连接字符串没改,就是打不开。问题往往不在 SQL 语句,而在“64 位 + 密码 + 驱动版本”这三件事的交叉点上。
这篇笔记就围绕“64 位 C# 调用 SQLite 数据库源码,含设置密码”这个具体场景展开。它解决的不是“SQLite 怎么用”这种入门问题,而是:在 64 位进程里,如何选对带加密能力的 SQLite 驱动、如何用源码方式把密码设置和校验跑通、以及换机器换环境时哪些参数会让你前功尽弃。适合正在做 C# 上位机、工控采集、本地缓存这类需要“单文件数据库 + 访问口令”的从业者,也适合被 System.Data.SQLite 和 SQLCipher 版本问题折腾过的人。
2. 为什么原生 SQLite 没有密码,加密方案怎么选
2.1 标准 SQLite 的“密码”其实是个伪命题
标准 SQLite 官方库根本不支持数据库级密码。你在网上看到的Password=xxx连接字符串,绝大多数是驱动层做的封装,底层要么是 SQLCipher,要么是 System.Data.SQLite 自带的加密扩展。理解这一点很关键:密码不是 SQLite 内核的能力,而是编译期引入的加密模块能力。
所以当你决定“给 SQLite 设置密码”时,真正要做的选型是:用哪一套带加密的 SQLite 实现。常见的有三条路:
| 方案 | 加密能力 | 64 位支持 | 典型使用场景 |
|---|---|---|---|
| System.Data.SQLite + SQLCipher 编译版 | 支持 | 需选 x64 包 | C# 桌面/上位机,最省事 |
| Microsoft.Data.Sqlite + SQLCipher | 支持 | 需自行替换 native 库 | .NET Core / .NET 5+ |
| 纯 SQLCipher 命令行 + C# 调进程 | 支持 | 天然支持 | 运维脚本、批量处理 |
对 2012 年前后起步、现在仍在维护的 C# 项目,System.Data.SQLite 的 x64 加密包是最常见的落点。它的好处是 API 和普通 SQLite 几乎一致,改造成本低;坏处是 32 位和 64 位的 native DLL 不能混用,一旦进程位数和 DLL 位数对不上,报错信息又极其含糊。
2.2 64 位进程下必须确认的三件事
在写任何代码之前,先把这三件事确认掉,能省掉后面一半的排查时间。
第一,确认你的 C# 项目目标平台。右键项目 → 属性 → 生成 → 目标平台,必须是x64,不能是Any CPU。Any CPU在 64 位系统上默认以 64 位运行,但如果你引用的 native DLL 是 32 位的,就会在运行时炸掉。工控现场很多老驱动只有 32 位,这时候你反而要强制x86。
第二,确认 System.Data.SQLite 包的位数。NuGet 上System.Data.SQLite和System.Data.SQLite.Core是两回事,后者不带 native 库。带加密的版本通常叫System.Data.SQLite.Linq或专门的 SQLCipher 包。安装后检查输出目录里x64和x86两个子目录是否都存在,以及你的进程实际加载的是哪一个。
第三,确认加密方式。System.Data.SQLite 支持两种:一种是它自己的加密扩展,一种是 SQLCipher 兼容模式。两者生成的数据库文件不通用。如果你拿一个 SQLCipher 加密的库去用普通驱动打开,就会得到那个经典的file is not a database。
提示:不要用
Any CPU去赌运行环境。上位机、服务、计划任务,任何一个环节都可能是 32 位宿主,显式指定 x64 或 x86 才是可控的。
3. 用源码跑通带密码的建库、开库与改密
3.1 最小可复现工程结构
先给一个我常用的最小工程结构,后面所有代码都基于它。新建一个 C# 控制台或 WinForms 项目,目标平台设为 x64,通过 NuGet 安装带加密能力的 System.Data.SQLite 包。项目输出目录里应该能看到System.Data.SQLite.dll以及x64、x86两个文件夹。
# 以命令行方式确认 native 库位数,避免装错包 # 进入输出目录后执行 dumpbin /headers x64\SQLite.Interop.dll | findstr machine # 期望看到 x64这段命令的作用是确认SQLite.Interop.dll的真实位数。dumpbin是 Visual Studio 自带的工具,在开发者命令提示符里可用。如果显示x86,说明你装的是 32 位包,后面在 x64 进程里必然加载失败。参数上没有什么可调的,关键是看machine后面跟的是x64还是x86。
3.2 建一个带密码的数据库
下面这段代码演示如何创建一个带密码的 SQLite 数据库,并写入一条测试数据。连接字符串里的Password就是加密口令,Version=3是 SQLite 文件格式版本,不要改成 2。
using System.Data.SQLite; class Program { static void Main() { string dbPath = @"C:\test\secure.db"; string password = "MySecret123"; // 如果文件已存在,先删掉,保证从零开始 if (System.IO.File.Exists(dbPath)) System.IO.File.Delete(dbPath); // 关键:Password 参数触发加密建库 string connStr = $"Data Source={dbPath};Version=3;Password={password};"; using (var conn = new SQLiteConnection(connStr)) { conn.Open(); using (var cmd = conn.CreateCommand()) { cmd.CommandText = "CREATE TABLE T_Data(Id INTEGER PRIMARY KEY, Name TEXT);"; cmd.ExecuteNonQuery(); cmd.CommandText = "INSERT INTO T_Data(Name) VALUES('hello');"; cmd.ExecuteNonQuery(); } } System.Console.WriteLine("建库完成"); } }逻辑说明:SQLiteConnection在打开时如果发现文件不存在,会根据连接字符串里的Password创建一个加密数据库。这里最容易踩的坑是——如果文件已经存在且是未加密的,再用带Password的连接去打开,会直接报错,而不是自动加密。所以上面先删文件,保证是全新创建。
参数说明:Data Source是文件路径,建议用绝对路径;Version=3固定;Password就是你的口令,长度没有硬性限制,但工控场景建议 8 位以上且不要用纯数字。改口令的方式是打开后执行ChangePassword,后面会讲。
3.3 用密码打开并读取数据
建完库之后,换一个进程或换一台机器,用同样的连接字符串打开即可。下面这段代码演示读取。
using System.Data.SQLite; class Program { static void Main() { string dbPath = @"C:\test\secure.db"; string password = "MySecret123"; string connStr = $"Data Source={dbPath};Version=3;Password={password};"; using (var conn = new SQLiteConnection(connStr)) { conn.Open(); using (var cmd = conn.CreateCommand()) { cmd.CommandText = "SELECT Id, Name FROM T_Data;"; using (var reader = cmd.ExecuteReader()) { while (reader.Read()) { System.Console.WriteLine($"{reader.GetInt32(0)} - {reader.GetString(1)}"); } } } } } }逻辑说明:打开加密库和打开普通库在代码上没有任何区别,区别全在连接字符串。如果密码错误,Open()会抛SQLiteException,错误信息通常是file is encrypted or is not a database。注意,这个报错和“文件损坏”的报错很像,不要一看到就去修文件,先确认密码和驱动。
参数说明:Password必须和建库时完全一致,大小写敏感。如果你的项目里密码来自配置文件,注意配置文件本身的编码,BOM 或多余空格都会导致密码不匹配。
3.4 修改密码与清除密码
改密是实际项目里一定会遇到的需求,比如客户要求定期换口令。System.Data.SQLite 提供了ChangePassword方法。
using System.Data.SQLite; class Program { static void Main() { string dbPath = @"C:\test\secure.db"; string oldPwd = "MySecret123"; string newPwd = "NewSecret456"; string connStr = $"Data Source={dbPath};Version=3;Password={oldPwd};"; using (var conn = new SQLiteConnection(connStr)) { conn.Open(); // 改成新密码 conn.ChangePassword(newPwd); } System.Console.WriteLine("改密完成"); } }逻辑说明:ChangePassword必须在连接打开后调用,传入新密码。执行成功后,旧密码立即失效。如果要清除密码,传入空字符串即可,但清除后数据库就变成明文,任何标准 SQLite 工具都能直接打开。
参数说明:新密码同样大小写敏感。改密过程中如果程序崩溃,数据库有可能处于中间状态,所以改密前建议先备份文件。这一点在工控现场尤其重要,数据库往往承载着采集数据,丢了很难补。
4. 64 位环境下最容易翻车的几个点
4.1 现象:本地能开,拷到客户机报 file is not a database
原因:客户机上进程是 32 位,或者加载了 32 位的SQLite.Interop.dll。System.Data.SQLite 会根据进程位数去对应的子目录找 native 库,如果目录结构被破坏,或者发布时只拷了x86,就会加载错。
解决:发布时把x64和x86两个目录一起带上,或者显式在项目里设置目标平台并只保留对应目录。用dumpbin确认位数,不要靠猜。
4.2 现象:密码明明是对的,却提示 file is encrypted
原因:建库时用的是 SQLCipher 兼容模式,打开时用的是普通加密模式,或者反过来。两种模式的密钥派生算法不同,同一个密码生成的密钥也不一样。
解决:统一驱动来源。要么全用 System.Data.SQLite 自带加密,要么全用 SQLCipher。不要混用不同来源的 native 库。检查连接字符串里是否有Password之外的加密相关参数,比如HexPassword,这个参数会改变密码的解释方式。
4.3 现象:改密后旧密码还能打开
原因:改密操作没有真正提交,或者程序里存在多个连接池连接,旧连接还持有旧密钥。SQLite 的连接池在带密码场景下行为比较微妙。
解决:改密后调用SQLiteConnection.ClearAllPools(),然后重新打开验证。改密前先关闭所有其他连接。如果是在服务里,确保没有后台线程还在用旧连接。
4.4 现象:数据库文件大小正常,但查询报 database disk image is malformed
原因:加密库被非加密工具打开过,或者写入过程中断电导致页结构损坏。带密码的库用普通 SQLite 工具打开,工具可能会尝试解析并写回,直接破坏文件。
解决:加密库只用配套的驱动和工具打开。如果已经损坏,用sqlite3的.recover命令尝试恢复,但前提是你能用正确密码打开。恢复前先备份原文件,不要在原文件上操作。
4.5 现象:程序运行一段时间后报 unable to open database file
原因:64 位进程下,如果数据库放在Program Files或系统保护目录,权限不足。另外,如果多个进程同时以写模式打开同一个加密库,也会出现这个报错。
解决:数据库文件放到有写权限的目录,比如ProgramData或应用自己的数据目录。多进程场景下,考虑用 WAL 模式,但 WAL 和加密的兼容性要实测,不是所有版本都支持。
注意:带密码的 SQLite 在并发写入上比明文更脆弱。如果业务需要高频写入,建议在应用层做队列,而不是依赖数据库锁。
5. 把密码管好:一个容易被忽略的工程习惯
5.1 密码不要硬编码在源码里
上面示例为了演示把密码写在了代码里,实际项目里这是大忌。C# 程序即使编译成 64 位,反编译工具依然能轻松还原字符串。常见做法是把密码放在配置文件里,但配置文件本身也要保护。
我一般会这样做:密码不落盘,而是由上位机在启动时从加密的配置节读取,或者由操作员在登录时输入。如果必须落盘,至少用 DPAPI 做一层机器绑定加密。下面是一个用 DPAPI 保护密码的简单示例。
using System; using System.Security.Cryptography; using System.Text; class PasswordProtector { // 用当前用户身份加密,换用户或换机器无法解密 public static string Protect(string plain) { byte[] data = Encoding.UTF8.GetBytes(plain); byte[] encrypted = ProtectedData.Protect(data, null, DataProtectionScope.CurrentUser); return Convert.ToBase64String(encrypted); } public static string Unprotect(string cipher) { byte[] data = Convert.FromBase64String(cipher); byte[] plain = ProtectedData.Unprotect(data, null, DataProtectionScope.CurrentUser); return Encoding.UTF8.GetString(plain); } }逻辑说明:ProtectedData是 .NET 自带的 DPAPI 封装,CurrentUser作用域意味着只有同一台机器的同一个用户能解密。这样即使配置文件被拷走,在别的机器上也解不开。参数上,null是可选熵,一般不用;如果要用,加解密必须一致。
5.2 验证密码是否生效的三种方法
写完代码不要只看“没报错”,要用下面三种方式交叉验证。
第一种,用十六进制工具看文件头。未加密的 SQLite 文件开头是SQLite format 3,加密后这个字符串会消失。用certutil -dump或任何十六进制查看器确认。
第二种,用错误的密码尝试打开,必须抛异常。如果错误密码也能打开,说明加密根本没生效,你只是建了一个普通库。
第三种,换一台没有装过你开发环境的机器,用发布包跑一遍。这一步能暴露 native 库缺失、位数不匹配、权限不足等一堆本地发现不了的问题。
5.3 一个我踩过的坑:连接池与改密顺序
最后说一个血泪经验。早期做的一个采集项目,客户要求每周自动改密。我写了个定时任务,先ChangePassword,再更新配置文件。结果有一次改密成功但配置文件写入失败,程序重启后用旧密码打不开,现场直接停线。
后来改成:先把新密码写入一个临时文件,改密成功后原子替换配置文件,最后删除临时文件。改密前先备份数据库,改密后立即用新密码做一次读验证。这套流程虽然啰嗦,但再没出过事。
带密码的 SQLite 在 64 位 C# 里跑通不难,难的是把密码的生命周期管住。建库、开库、改密、备份、恢复,每一步都要有后悔药。希望帮到你。
本文还有配套的精品资源,点击获取