KDBX 解密过程#
为了简化解密步骤,解密过程没有进行数据校验
- 输入密码,对密码进行 sha256 哈希计算得到密码哈希值
- 密码哈希值加上 key 文件内容再次进行 sha256 哈希计算,如果没有 key 文件就密码哈希值再次进行 sha256 哈希计算得到新的密码哈希值,也就是 SHA256 (SHA256 (password) + keyfile)
- 从文件头得到加密算法 KDF 相关参数,默认是 AES-KDF, 从 KDF parameters 中得到 KDF 的 Salt/seed 和 Rounds
- 使用二次 sha256 密码得到的哈希值,还有 KDF 的 Salt/seed 和 Rounds, 进行 AES-KDF 计算,得到转换后的密钥
- 从文件头得到 Master salt/seed 和转化后的密钥再次进行 sha256 计算,得到最终的解密密钥
- 从文件头得到 Encryption IV/nonce, 加上解密密钥对加密数据进行 AES 解密
- 解密后的数据进行 Gzip 解压,得到 Inner Header 和 XML 内容,XML 中的密码默认使用 ChaCha20 加密
- 从 Inner Header 中得到 Inner encryption key, 使用 sha512 进行哈希计算,哈希字符串索引从 0 到 31 字节为 ChaCha20 的 key, 32 到 43 为 ChaCha20 加密的 nonce
- 根据 ChaCha20 的 key 和 nonce 解密加密字符串得到明文密码,由于是流密码,所以需要根据加密字符串顺序依次进行解密
创建 kdbx 文件#
使用 KeePass 客户端创建 kdbx 文件,全部使用默认参数,使用 Hex 编辑器打开 kdbx 文件,根据 KeePass 文档KDBX File Format Specification - KeePass将文件头部拆分如下图
十六进制详细分析#
1. header#
文件开头 12 个字节分成三组,每组都是 UInt32 类型,占用四个字节,之后为多个 Fields
Fields 由 id, 值长度,值组成,id 为 1 个字节,值长度为为 Int32 类型,占用 4 个字节,根据值长度截取对应字节数据作为值
详细文件格式说明参考KDBX File Format Specification - KeePass
kdbx 文件使用小端序字节序,数字类型需要从右至左读取
Signature1#
Signature1 是固定值0x9AA2D903
, 类型为 UInt32, 占用 4 字节
Signature2#
Signature2 是固定值0xB54BFB67
, 类型为 UInt32, 占用 4 字节
Format version#
Format version 是文件格式版本,类型为 UInt32, 占用 4 字节,当前为类型为0x00040000
, 当前版本为 4
Encryption algorithm#
0x02
转 Int32 为 2, id 为 2 对应 Encryption algorithm, 表示加密算法,类型为 UUID, 长度0x00000010
转 Int32 为 16, 表示长度为 16 个字节,UUID 为 16 进制字符串,值为31C1F2E6BF714350BE5805216AFC5AFF
, 对应上图中的 AES-256
Compression algorithm#
0x03
转 Int32 为 3, id 为 3 对应 Compression algorithm, 表示压缩算法,类型为 UInt32, 长度0x00000004
转 Int32 为 4, 表示值长度为之后 4 个字节,16 进制0x00000001
转 UInt32 为1
, 表示使用 GZip 压缩
Master salt/seed#
0x04
转 Int32 为 4, id 为 4 对应 Master salt/seed, 表示加密的盐 / 种子,类型为 32 个字节的数组,长度0x00000020
转 Int32 为 32, 表示值长度为之后 32 字节,对应字节数组为[0xFA, 0xC9, 0x50, 0x39, 0x79, 0x5F, 0x19, 0x83, 0x21, 0x22, 0x8F, 0x24, 0x8F, 0x59, 0x63, 0xD5, 0x30, 0x38, 0x7A, 0x55, 0xD8, 0x08, 0xD5, 0x19, 0xC4, 0xD6, 0xEC, 0xEC, 0xEE, 0xDF, 0xA8, 0x24]
, 这是 AES 加密用的盐
KDF parameters#
0x0B
转为 Int32 为 11, 对应 KDF parameters, 表示密钥派生函数 (KDF) 的参数,长度0x0000005D
转 Int32 为 93, 表示值为之后 93 个字符,值类型为 Variant Dictionary, 翻译为变体词典,可以理解为特殊的对象结构,可以拆分为版本信息和多个词典对象组成的数组,词典对象由值类型,名称长度,名称,值长度,值组成
可将 Variant Dictionary 进行如下拆分,最后一个字节0x00
没有实际用途
KDF Format version#
首先是 UInt16 类型的版本信息,值为0x0100
, 占用 2 字节
KDF UUID#
0x42
表示 [] byte 类型,0x000005
转 Int32 为 5, 表示名称长度 5 个字节,0x2455554944
对应 ASCII 字符串为$UUID
, 之后0x00000010
转 Int32 为 16, 表示 UUID 长度为 16 个字符,对应 16 进制字符串是C9D9F39A628A4460BF740D08C18A4FEA
, 默认使用 AES-KDF
KDF Rounds#
0x05
表示 UInt64 类型,0x000001
转 Int32 为 1, 表示名称长度为 1 个字节,0x52
对应 ASCII 字符为R
, 值长度0x000008
转 Int32 为 8, 表示 Rounds 值为之后 8 个字节,0x00000000000927C0
转 UInt64 为 600000, 表示迭代次数为 600000
KDF Salt/seed#
0x42
表示 [] byte 类型,0x000001
转 Int32 为 1, 表示名称长度 1 个字节,0x53
对应 ASCII 字符串S
, 之后0x00000020
转 Int32 为 32, 表示 Salt/Seed 长度为 32 个字符,对应字节数组为[0x0C, 0x34, 0x88, 0x19, 0x8E, 0x90, 0xA3, 0x65, 0x40, 0xB4, 0x1C, 0x4E, 0xCC, 0x85, 0x5D, 0x11, 0xC3, 0x0A, 0x20, 0xD6, 0xF4, 0xAA, 0x20, 0xE4, 0xEC, 0xF3, 0xCF, 0xF3, 0xD9, 0x81, 0xC5, 0xED]
, 也是就是 KDF 加密用的盐
Encryption IV/nonce#
0x07
转为 Int32 为 7, id 为 7 对应 Encryption IV/nonce, 表示加密算法的初始化向量或随机数,长度0x00000010
转 Int32 为 16, 表示值为之后的 16 个字节组成的数组,初始化向量为字节数组[0x6C, 0x77, 0x12, 0xF7, 0xE2, 0xD0, 0x2E, 0xAE, 0x63, 0x9D, 0x53, 0x92, 0xD6, 0x03, 0xFD, 0x08]
End of header#
0x00
转 Int32 为 0, id 为 0 对应 End of header, 表示头部结束,长度0x00000004
转 Int32 为 4, 值为之后的 4 个字节,是固定值[0x0D, 0x0A, 0x0D, 0x0A]
2. SHA-256 hash of the header#
在[0x0D, 0x0A, 0x0D, 0x0A]
之后的 32 个字节为 header 的 SHA-256 哈希值,用来校验头部信息的完整性
3. HMAC-SHA-256 hash of the header#
在 hash of the header 之后的 32 个字节为 header 的 HMAC-SHA-256 值,需要主密钥才能完成校验
4. In HMAC-protected block stream#
Unknown Data#
未知部分,解密与校验未使用这部分数据,文档没有相关说明,解密需要跳过这部分,否则会出错
Encrypted Data#
之后到文件结尾为加密部分 (需要去掉最后四个字节0x00000000
)
解密后得到 Gzip 压缩的数据,Gzip 压缩内容的头部是0x1F8B
Gzip Decompress#
进行 Gzip 解压后得到 Inner Header 和 XML 内容
Inner Header#
Inner encryption algorithm#
0x01
转 Int32 为 1, id 为 1 对应 Inner encryption algorithm, 表示内部加密算法,长度0x00000004
转 Int32 为 4, 表示值长度为 4 字节,值0x0000003
转 Int32 为 3, 表示使用 ChaCha20 加密
Inner encryption key#
0x02
转 Int32 为 2, id 为 2 对应 Inner encryption key, 表示内部加密密钥,长度0x00000040
表示长度为 64 字节的数组,也就是[0x68, 0x34, 0xD6, 0x1D, 0xFA, 0x98, 0x64, 0xB1, 0x8A, 0x51, 0x05, 0xAB, 0x26, 0x10, 0x35, 0xC8, 0xB0, 0x22, 0xF5, 0x14, 0x91, 0x0F, 0x47, 0x8C, 0x3D, 0xE7, 0x63, 0x86, 0x9C, 0x87, 0xC1, 0x26, 0xC6, 0xB7, 0xDE, 0x59, 0x77, 0x3B, 0xC6, 0x13, 0xF7, 0x17, 0x6C, 0x05, 0x7E, 0x1C, 0xF3, 0xDC, 0x78, 0x25, 0x64, 0xCA, 0x44, 0x7E, 0xFD, 0xB4, 0x6F, 0xEB, 0x4D, 0xDC, 0xD5, 0x91, 0x7B, 0x5C]
此处的值需要进行 sha512 哈希计算,然后哈希值数组索引从 0 到 31 是 key, 32 到 43 是 nonce
XML document#
XML 文档中密码包含Protected="True"
属性,需要将该字符串先进行 Base64 解码,然后进行 ChaCha20 解密,由于是流密码,所以解密后面的密码需要之前的密码流