Firebird数据库加密的工作原理
(c) Alexey Kovyazin, IBSurgeon, Alex Peshkoff, Firebird Project, 2021
本文基于2019年在德国柏林举行的Firebird Conference上“数据库加密”研讨会的材料。它描述了Firebird数据库加密如何在服务器级别和客户端工作,如何配置数据库加密,以及如何从各种类型的应用程序(Delphi、Java、.NET)中使用它。本文的示例基于IBSurgeon Firebird Encryption Framework(FEPF),但可以适用于目前大多数可用的加密插件实现。
目录:
- 为什么我们需要数据库加密(以及何时不需要)?
- Firebird数据库加密如何在服务器端工作
- 数据库的哪部分被加密?
- 数据页何时被加密?
- 如何保护密钥传输?
- Firebird加密如何在客户端工作
- 原生应用程序
- Java应用程序
- .NET应用程序
- 安装和配置
- 如何跟踪加密进度
- 总结
1. 为什么我们需要数据库加密(以及何时不需要)?
Firebird数据库加密在Firebird 3.0版本中引入(同时引入了传输协议加密,这常常与本文讨论的主题混淆),大大增强了保护数据免受未授权访问的能力。然而,它并非万能药,有必要了解其优缺点才能正确使用。
在本文中,我们从基础层面探讨数据库加密的内部机制,让Firebird应用程序的开发人员更好地理解数据库加密的工作原理。
那么,为什么我们需要数据库加密?
- 保护包含敏感/有价值数据的数据库免受“物理”窃取。如果入侵者窃取了带有加密数据库副本的磁盘,或以某种方式获得了数据库文件的副本,那么没有相应的密钥就无法从中读取数据,也无法使用像FirstAID这样的恢复软件来提取数据。当然,这取决于加密算法和计算能力,但破解AES256将需要过长的时间或过于昂贵的计算资源。
- 保护数据库免受没有加密密钥的未授权应用程序的访问。例如:
- 未授权人员使用开发工具直接访问,以更改敏感信息(例如,资金交易),
- 更改或窃取业务逻辑(存储过程和触发器的文本)。
- 保护包含预填充数据的数据库,防止被导出到未授权应用程序或由其访问。
- 各国政府最近出台了数据保护法律(欧洲的GDPR/DSVGO、巴西的LGPD等),这些法律要求对个人和其他敏感数据提供更高水平的保护,而加密被列为适当的保护措施之一。
数据库加密何时没有用?
在某些情况下,最好使用Firebird的安全和配置功能,而不是数据库加密:
- 要保护数据库免受通过网络进行的物理访问,需要配置网络访问:即关闭网络共享文件夹,因为Firebird不需要对数据库文件的网络共享访问,并加强安全权限(例如,对于Linux,数据库文件必须仅对用户“firebird”具有读写访问权限)。
- 要限制特定用户子集对特定数据库的访问,更简单的解决方案是配置单独的安全数据库。
- 要限制对数据库对象(表、存储过程)的访问,需要使用Firebird安全机制:用户、角色等。
当然,以上两个列表都不完整,但它们能让您对何时需要或不需要数据库加密有一个印象。
2. Firebird数据库加密如何在服务器端工作
让我们深入了解Firebird数据库加密的内部细节,从服务器部分开始。
2.1. 数据库的哪部分被加密?
我们需要考虑的第一件事是数据库的哪部分被加密?正如您可能知道的,Firebird数据库由大小相等的部分组成,称为“数据库页”。有几种类型的页面,每种类型都有特定的用途。
下面您可以看到主要数据类型的图示:

图1. 数据库页面类型
一些页面用于存储用户数据,另一些则用于存储系统信息,如事务和页面清单页(有关数据库页面的更多详细信息,请点击此处)。
当Firebird数据库被加密时,只有包含用户数据的页面被加密:数据页、索引、生成器和BLOB:

图2. 只有包含用户数据的数据库页面被加密
请注意,数据库元数据(存储过程、表、视图、触发器、生成器名称等)在负责加密的引擎部分中与“用户数据”没有区别,它们也会被加密。
为什么系统页面不被加密?主要是出于性能原因,以及它们不包含需要保护的敏感数据这一事实。
数据库头页不被加密,因为它包含加密所需的信息(例如,密钥名称)。
2.2. 数据页何时被加密?
当用户对加密数据库执行SELECT时,数据从加密文件中读取,但以非加密形式到达应用程序的结果集网格中。
让我们考虑这个过程的细节:

图3. 数据库页面何时被加密?
通常,该过程从对数据库文件的一系列页面读取开始,这些页面被缓存在操作系统文件缓存中。
Firebird也可以配置为绕过文件缓存,仅使用自己的缓存,但默认情况下使用文件缓存。
之后,Firebird读取页面并将其放入Firebird页面缓存(由firebird.conf和/或databases.conf中的DefaultDBCachePages参数或数据库头页定义)。
然后,缓存中的页面被选择到特定SQL语句(在我们的示例中是SELECT)的结果集中。
下图显示了详细信息:

图4. 页面在Firebird缓存和操作系统文件缓存之间被加密
因此,数据库页面在操作系统文件缓存中被加密,但以非加密形式到达Firebird页面缓存,反之亦然。
Firebird软件中负责加密/解密的部分称为“加密插件”。由于大多数插件实现(作者所知的)都称为DbCrypt,我们将称之为DbCrypt。
在下图中,您可以看到Windows(DbCrypt.dll)和Linux(libDbCrypt.so)的变体:


图 5. 加密插件(DbCrypt)正在执行加密/解密
如果你盯着这张图看足够久,下一个问题很快就会浮现:DbCrypt 是如何获取正确的密钥来加密/解密数据库页面的?
答案–还有另一个用于密钥管理的插件。
密钥管理的典型名称是 KeyHolder,它充当加密插件(DbCrypt)的密钥存储/管理设施。KeyHolder 实现了供 DbCrypt 使用的密钥管理接口。

图 6. DbCrypt 和 KeyHolder
“密钥管理”是什么意思?
在最简单的情况下,DbCrypt 可以从服务器上的文件中读取密钥。该文件可以是一个简单的纯文本文件,可以隐藏在“秘密”位置或 U 盘上,也可以是一个加密文件(例如,使用 Windows Crypto API,或使用内置的内部密钥)。
密钥文件可以包含多个密钥,为了方便,以命名列表的形式存储,看起来可能像这样(下面的示例取自 IBSurgeon Encryption Framework):
Key=Red 0xec,0xa1,0x52,0xf6,0x4d,0x27,0xda,0x93,0x53,0xe5,0x48,0x86,0xb9,0x7d,0xe2,0x8f,0x3b,0xfa,0xb7,0x91,0x22,0x5b,0x59,0x15,0x82,0x35,0xf5,0x30,0x1f,0x04,0xdc,0x75,
Key=Green 0xab,0xd7,0x34,0x63,0xae,0x19,0x52,0x00,0xb8,0x84,0xa3,0x44,0xbd,0x11,0x9f,0x72,0xe0,0x04,0x68,0x4f,0xc4,0x89,0x3b,0x20,0x8d,0x2a,0xa7,0x07,0x32,0x3b,0x5e,0x74,
当数据库被加密时,其头部页面保持未加密状态,以存储有关加密插件和密钥名称的信息,你可以通过命令“gstat -h databasename”查看这些信息:
Database header page information:
....
Creation date Jan 11, 2017 15:12:20
Attributes force write, encrypted, plugin DBCRYPT
Variable header data:
Crypt checksum: MUB2NTJqchh9RshmP6xFAiIc2iI=
Key hash: ask88tfWbinvC6b1JvS9Mfuh47c=
Encryption key name: RED
Sweep interval: 0
*END*
DbCrypt 可以处理多个数据库和多个密钥的页面:

图 7. 同一服务器(即 Firebird 实例)上多个数据库的多个密钥
选择正确的密钥
开发人员经常问这样一个问题:“插件如何识别哪个密钥对应哪个数据库?”答案很简单:通过存储在数据库头部页面上的密钥名称。
一个不太常见但仍然重要的问题是–如果密钥名称与头部记录的一致,但密钥值不同怎么办?为了防止因错误密钥导致的页面读取错误,DbCrypt 插件在头部存储了一个加密的测试序列(数字 0…F),然后当密钥被激活时,插件尝试用该密钥加密样本数据,并将其哈希值与存储的结果进行比较,以确保传入的密钥值确实对该特定数据库是正确的。
加密插件直接读取密钥的方法简单,有利于调试、性能测试等,因为它以透明的方式实现加密:即客户端应用程序和开发人员工具不知道数据库已加密。
然而,在现实中,我们需要限制应用程序对数据库的访问:只有拥有密钥的客户端应用程序才能连接到加密数据库。
为此,我们需要 KeyHolder 插件,通过 Firebird 网络协议从客户端应用程序(通常位于另一台计算机上)获取密钥。
2.3. 如何保护密钥传输?
有可能我们希望保护数据库,以防客户决定绕过授权应用程序直接访问加密数据库(许多供应商希望限制对数据的直接访问,无论是只读还是读写)。
这等同于入侵者可以访问服务器但没有密钥的情况。
让我们考虑以下在服务器端拦截密钥的攻击场景:
- 当入侵者创建伪造的加密插件(DBCrypt.dll)并将其放在服务器上,当 KeyHolder 传递密钥时,伪造的 DbCrypt 会转储密钥:

图 8. 使用伪造的 DbCrypt.dll 进行攻击
- 当入侵者创建伪造的 firebird.exe 文件并使用它进行转储:

图 9. 使用伪造的 firebird.exe 进行攻击
为了防止此类攻击,良好的加密和密钥管理插件实现需要保护密钥交换。
密钥交换可以使用非对称加密来保护,即使用一对公钥/私钥。
这些密钥在构建过程中生成,并内置于特定的一对加密和密钥管理插件中。为了获得最佳保护,有必要使用专门构建的 DbCrypt/KeyHolder 配对。

当 DbCrypt 和 KeyHolder 交换密钥时,它们使用以下协议(这是简化版本,但我想思路是清晰的):
DbCrypt → KeyHolder:
使用此盐给我数据库密钥
KeyHolder:
使用 DbCrypt 提供的盐和公钥加密 DbKey
将加密的 DbKey 传输给 DbCrypt
DbCrypt:
使用私钥解密 DbKey
验证盐的正确性
准备开始工作
KeyHolder 实例之间交换密钥,以及客户端应用程序与 KeyHolder 之间交换密钥,也使用大致相同的协议。
请注意,传输密钥的加密和正确性取决于插件的实现,Firebird 引擎仅提供基本的低级传输服务“将 N 字节从此插件实例发送到彼插件实例”。
加密服务器端部分总结
- 加密/解密由数据库加密插件(DbCrypt)在操作系统文件缓存与 Firebird 页面缓存之间的数据交换过程中逐页完成
- 密钥管理可以以简单的方式实现,即 DbCrypt 直接读取密钥,但通常通过密钥管理插件(KeyHolder)完成
现在让我们探讨客户端应用程序如何与加密数据库一起工作。
3. Firebird 加密在客户端如何工作
3.1. 原生应用程序
为了理解客户端应用程序连接到加密数据库时会发生什么,让我们考虑原生应用程序连接到未加密数据库的常规连接过程。
请注意:从这里开始,“原生”意味着此类应用程序使用 fbclient.dll 与服务器建立网络连接,通常此类应用程序是用 Delphi、C++、PHP 构建的。与原生应用程序不同,Java 和 .NET 实现了自己的协议版本,将在下面讨论。
连接过程:
- 客户端应用程序加载客户端库
- fbclient.dll - 原生 Windows 应用程序
- libfbclient.so - 原生 Linux 应用程序
- 客户端应用程序发起连接,发送
- 用户名,例如 SYSDBA
- 密码,例如 masterkey
- 数据库的路径/别名
在加密数据库的情况下,需要额外的步骤:必须传递加密密钥的名称及其值。
重要的是要说明,传递密钥应该在常规连接之前完成,因为包含元数据的数据页面(包括数据库所有者名称、字符集等)是加密的。
因此,这引出了以下内容:
- 在常规连接之前,需要额外的网络往返来传递密钥
- 从客户端应用程序向 Firebird 传递密钥需要使用非对称加密和回调接口的实现进行编码,这可能相当复杂。为了简化此任务,插件供应商提供了连接代码示例,或者像在 IBSurgeon 插件框架中那样,创建额外的库 fbcrypt.dll/libfbcrypt.so,该库实现了易于使用的合适接口,用于从客户端应用程序传递密钥。
要将使用fbclient.dll的原生应用程序连接到加密数据库,需要执行3个调用。以下是一个Delphi示例(已简化,未包含错误处理):
在BeforeConnect事件处理程序中:
fbcrypt_init(PAnsiChar(‘C:\Firebird30.bclient.dll’));
fbcrypt_key(‘RED’, ‘0xec,0xa1,0x52,0xf6,...’));
fbcrypt_callback(nil);
// 然后照常连接
Database1.Active:=True;
下图展示了原生应用程序连接加密数据库的流程概览:

图10. 原生应用程序连接加密数据库的流程
那么对于多线程客户端应用程序,线程安全性如何?
让我提醒您一些实现多线程客户端应用程序的通用要点。
自Firebird 2.5起,应用程序内的多个线程可以安全地使用同一个数据库连接(attachment),因为所有必要的同步都在fbclient.dll内部完成。
但是,在这种情况下,线程只能逐个使用该连接。
这对于不需要与数据库进行高性能数据交换的应用程序来说是可以接受的–如果一个线程执行SQL查询时等待另一个线程执行其他SQL不是问题,那么使用多个线程共享1个连接的简单模型会更容易。
如果应用程序需要并行执行SQL查询(即它是一个全功能的客户端应用程序),最好为每个连接使用单独的线程。
对于加密数据库连接的密钥交换情况则稍微复杂一些。
在每次连接时,客户端库会从客户端传输密钥,但这与客户端应用程序中的线程没有直接关系,具体情况取决于所使用的API。
如您所知,自3.0起,Firebird客户端库提供两种类型的API:基于提供者(provider)概念的新型面向对象API,以及为保持与旧版Firebird驱动程序的兼容性而作为变通方案实现的传统isc_ API。
如果使用新的面向对象客户端API,只需创建提供者,为其提供必要的密钥,然后将其用于新的连接即可。
如果使用isc_客户端API,对于每个连接,客户端库都会创建自己的临时提供者,最终用户无法直接看到或访问它。
在这种情况下,密钥恰好从调用isc_attach_database的线程中传输,并使用线程本地存储来保存该密钥。
实际上,由于几乎所有客户端库都使用isc_ API(目前,在流行的驱动程序中,只有Python驱动程序使用OO API),因此有必要在每个连接到加密数据库的线程中调用fb_database_crypt_callback()。
密钥传输调用(FEPF示例中的fbcrypt.dll调用)必须在连接之前、在将要建立连接的同一线程中完成。
当我们处理多个数据库时(例如,拥有许多客户端数据库的SaaS Web服务器),重要的是要记住,每次调用fbcrypt_key()都会向与当前连接关联的KeyHolder存储中添加一个密钥。
密钥值必须在连接之前设置,连接建立后,密钥值无法更改。
在断开连接时,密钥不会被卸载,它们将保留在内存中,直到fbcrypt.dll被卸载。
3.2. Java应用程序
Java驱动程序(JayBird)拥有自己的Firebird连接协议实现(纯Java)。Jaybird 4(及3.0.4+)在版本13协议的纯Java实现中添加了对Firebird 3数据库加密回调的支持。
“ 当前实现很简单,仅支持从连接属性回复静态值。请注意,用于数据库加密的静态值响应并不十分安全,因为它很容易导致重放攻击或意外密钥泄露。
Jaybird的未来版本(可能是5)将引入对需要更复杂回调的数据库加密插件的插件支持。”
实际上,这意味着我们需要在连接属性dbCryptConfig中设置加密回调的值(通常是密钥名称和密钥值对)。
例如:
edConnectionString.setText("jdbc:firebirdsql://localhost/g:/Databases/ODS12/crypt.fdb?lc_ctype=utf8&dbCryptConfig=MyKey:0xec,0xa1,0x52,0xf6,0x4d,0x27,0xda,0x93,0x53,0xe5,0x48,0x86,0xb9,0x7d,0xe2,0x8f,0x3b,0xfa,0xb7,0x91,0x22,0x5b,0x59,0x15,0x82,0x35,0xf5,0x30,0x1f,0x04,0xdc,0x75,");
也可以指定base64格式的字符串–来自readme:
“ 以base64::为前缀的字符串,其余部分将作为base64解码为字节。
=填充字符是可选的,但如果存在则必须有效(即:如果使用填充,则必须使用与长度匹配的正确填充字符数)。
当base64编码值包含+时,在JDBC URL中必须转义为%2B。为了与Jaybird 3向后兼容,我们不能切换到URL安全的base64变体。“
在IBSurgeon的密钥管理插件实现中,这种密钥传输方式被认为不太安全:如果未启用网络协议加密( 顺便说一下,要启用它,请在firebird.conf中设置WireCrypt=Required,并且不要使用旧版身份验证),密钥很容易被WireShark等网络流量分析工具发现,因此,为了以这种方式启用密钥传输,有必要在IBSurgeon密钥管理插件的KeyHolder.conf中设置UnsafeClient=true。
3.3 .NET应用程序
Firebird.NET提供程序为加密数据库实现了类似的密钥交换方案,并且还要求在FEPF的KeyHolder.conf中设置参数UnsafeClient=true。
加密数据库的.NET连接字符串示例:
string connectionString =
"User=SYSDBA;" +
"Password=masterkey;" +
"Database=G:\\Databases\\ODS12.RYPT.FDB;" +
"DataSource=localhost;" +
"Port=3053;" +
"Dialect=3;" +
"Charset=NONE;" +
"Role=;" +
"Connection lifetime=15;" +
"Pooling=true;" +
"MinPoolSize=0;" +
"MaxPoolSize=50;" +
"Packet Size=8192;" +
"ServerType=0;" +
"cryptkey = TXlLZXk6MHhlYywweG…...;";
您可能会注意到,加密密钥看起来与原生应用程序和JayBird示例中的不同,这是因为它是Base64转换的结果,因此要为.NET或Java应用程序获取密钥,需要从字符串计算base64:
“MyKey:0xec,0xa1,0x52,0xf6,0x4d,0x27,0xda,0x93,0x53,0xe5,0x48,0x86,0xb9,0x7d,0xe2,0x8f,0x3b,0xfa,0xb7,0x91,0x22,0x5b,0x59,0x15,0x82,0x35,0xf5,0x30,0x1f,0x04,0xdc,0x75,”
并将其用作"cryptkey=xxx;“的参数,连接字符串末尾带有”;"。
4. 安装与配置
为了启用数据库加密和密钥管理插件,有必要在Firebird配置文件firebird.conf中指定加密插件的名称:
KeyHolderPlugin = KeyHolder
或者,在databases.conf中,为加密数据库的别名指定:
myencrypted = C:\Temp\EMPLOYEE30.MPLOYEE30.FDB { KeyHolderPlugin = KeyHolder }
然后,有必要检查插件所需的所有文件是否都在服务器上。
以下示例适用于IBSurgeon的FEPF,但其他插件大致类似:
在%FirebirdFolder$\plugins中
• DbCrypt.dll
• DbCrypt.conf
• KeyHolder.dll
• KeyHolder.conf - 仅用于调试模式!
**在 %FirebirdFolder$ 中**
• fbcrypt.dll
• libcrypto-1_1-x64.dll
• libssl-1_1-x64.dll
• firebird.msg
之后,我们可以在服务器上执行测试加密,为此,在 isql 中:
isql.exe localhost:C:\Temp\EMPLOYEE30.MPLOYEE30.FDB -user SYSDBA -pass masterkey SQL>alter database encrypt with dbcrypt key red; SQL> show database; Database: localhost:C:\Temp\EMPLOYEE30.MPLOYEE30.FDB …. ODS = 12.0 Database encrypted Default Character set: NONE
如果你在 Linux 上,请记住大小写很重要,因此命令将是:
alter database encrypt with “DbCrypt” key Red;
现在,我们可以测试客户端对加密数据库的访问。为此,我们将移除(或重命名或编辑)配置文件 KeyHolder.conf,并尝试使用简单的测试应用程序连接到加密数据库。
为此,我们必须将以下文件放入客户端应用程序所在的文件夹中:
- 来自 [FEPF](/zh/download-demo-firebird-encryption-plugin) 的演示应用程序 - CryptTest.exe(32位)
- 必需文件:
- fbclient.dll
- fbcrypt.dll
- libcrypto-1.1.dll
- libssl-1.1-x64.dll
- 可选文件:
- firebird.conf
- 在 plugins 中
- ◦ KeyHolder.conf
- ◦ keyhodler.dll
在某些密钥管理插件的实现中,可以在不修改客户端软件的情况下[在客户端库中加载密钥](/zh/download-demo-firebird-encryption-plugin#Connect%20Firebird%20database%20with%20developer%20tools)(fbclient.dll)。
这使得 Firebird 开发者工具(如 Firebird SQL Studio、DatabaseWorkbench、IBExpert、FlameRobin、RedExpert 等)能够透明地工作,并且 Firebird 命令行工具(gfix.exe、nbackup.exe 等)也能透明地使用。
## 5. 如何跟踪加密进度
Firebird 仅在存在活动连接时才对数据库进行加密。加密过程在单独的并行线程中运行,对于大型数据库,完整加密可能需要相当长的时间。
为了跟踪加密过程,可以从 MON$ 运行 SQL 查询:
select mon$crypt_page * 100.0 / mon$pages as Percent from mon$database; commit;
或者使用特殊开关执行 gstat 工具:
gstat -e dbname
Database “D:\ENCDB\TESTENCRYPT.FDB” Gstat execution time Tue Mar 16 11:45:11 2021
Database header page information: Flags 0 Generation 10697 System Change Number 3 Page size 8192 ODS version 12.0 Oldest transaction 7053 Oldest active 7054 Oldest snapshot 7054 Next transaction 7054 Sequence number 0 Next attachment ID 17834 Implementation HW=Intel/i386 little-endian OS=Windows CC=MSVC Shadow count 0 Page buffers 0 Next header page 0 Database dialect 3 Creation date Oct 9, 2019 6:42:31 Attributes encrypted, plugin DBCRYPT
Variable header data:
Database backup GUID: {866B4967-ED58-427E-A481-DB9206CEA2ED}
Crypt checksum: MUB2NTJqchh9RshmP6xFAiIc2iI=
Key hash: ask88tfWbinvC6b1JvS9Mfuh47c=
Encryption key name: RED
Database GUID: {323FE494-1771-4608-E99D-C1B69C84578B}
*END*
Data pages: total 105, encrypted 105, non-crypted 0 Index pages: total 95, encrypted 95, non-crypted 0 Blob pages: total 0, encrypted 0, non-crypted 0 Generator pages: total 1, encrypted 1, non-crypted 0 Gstat completion time Tue Mar 16 11:45:11 2021
请注意,gstat 的执行可能是一个耗时的过程。
## 6. 总结
1. Firebird 数据库加密是一项强大的功能,可以保护数据库中的信息免受未经授权的访问。
2. 加密过程需要服务器端的动态库--加密插件(通常称为 DbCrypt),并且在绝大多数情况下,还需要密钥管理插件(通常称为 KeyHolder)。
3. DbCrypt 和 KeyHolder 插件的安全可靠实现应考虑最流行的攻击类型。
4. 为了使用加密数据库,客户端应用程序必须传输加密密钥。
5. 在服务器端安装和配置加密插件非常简单,只需要在 firebird.conf/databases.conf 中设置 1 个参数,以及几个文件。
6. 加密过程可能耗时较长,它在单独的后台线程中完成,可以通过 MON$ 调用或 gstat 跟踪进度。
### 下一步是什么?
我们正在对 Firebird 数据库加密进行详细的性能测试。总的来说,性能会降低 4-8%,但这取决于硬件和 Firebird 的设置。敬请期待!
### 联系我们:
请通过电子邮件发送您的建议、错别字、错误等以及任何问题:[[email protected]](mailto:[email protected]?subject=Firebird%20Db%20encryption%20article)