更换解锁口令通常不用重写全盘,更换卷密钥却可能需要重加密。密码原语、密钥管理和实际数据路径需要分层理解。
保护离线介质,与阻止已解锁系统上的恶意进程读文件,是不同目标;存储加密仍需配合访问控制。
查阅目标 阅读位置
算法实例与异步请求 第 1 节
块加密、keyslot 与目录策略 第 2~3 节
网络加密、TPM 与摘要实验 第 4~6 节
1. Crypto API:算法实例与单次请求
接口类别 任务 例子
skcipher 对称加解密 XTS、CBC 等模式
AEAD 加密并认证数据及附加数据 GCM、ChaCha20-Poly1305
shash / ahash 同步/异步摘要或相关 keyed hash SHA-256、HMAC
akcipher 非对称加密等接口 RSA 类操作
kpp 密钥协商 DH、ECDH
rng 特定随机数生成器接口 已注册 RNG 实现
ECDH 应归入密钥协商,不应拿它作为 RSA 式加密接口的例子。普通内核随机字节需求常使用 get_random_bytes() 等随机数子系统接口,也不是每个调用者都自行分配 crypto_rng。
tfm 是配置算法与密钥等信息的 transformation 对象;一次操作通常还需要 request、源/目标 scatterlist、长度和 IV。正确的 skcipher 加密入口接收 skcipher_request *,不是伪造的 crypto_skcipher_encrypt(tfm, src, dst, len)。
分配 tfm,检查错误并设置密钥。
建立 request、回调、源/目标范围与 IV。
提交后按返回状态区分同步完成或异步处理。
满足完成契约后,才释放请求、数据与上下文。
在可睡眠上下文可按官方示例用 crypto_wait_req() 等待;不能把它搬进原子上下文。见 skcipher API 与完整示例 。
算法名字不是具体执行单元
xts(aes) 之类名字组合模板和底层算法。多个实现可以提供同一通用算法名,Crypto API 根据匹配条件、实现优先级等选择;指定 driver 名则会收窄选择。/proc/crypto 可以观察当前注册实现,但不是系统所有可能加载算法组合的完整清单。
AES-NI、ARM 加密扩展和独立加速器具有不同调用成本。小块数据、队列延迟、DMA 和复制成本都可能影响结果,不能保证“存在硬件实现就固定快十倍”。内核也有直接的密码学库接口,并非每项密码操作都必须经过动态 tfm 分配。见 Crypto API 架构 。
2. dm-crypt 与 LUKS 分别在哪一层
解锁密钥与读写数据走不同路径
口令 / TPM 等解锁材料
用户态处理解锁策略
LUKS keyslot
保护卷密钥
卷密钥
交给运行中的加密映射
文件系统块 I/O
已解锁时可访问明文
dm-crypt
按扇区加解密
下层存储
保存密文数据
更换口令通常改变 keyslot;更换卷密钥才涉及重新加密数据。
dm-crypt 是 device-mapper 的块加密 target,处理发往映射设备的数据读写;上面的文件系统通常看到解密后的块。LUKS 是磁盘格式和密钥管理约定,由 cryptsetup 等用户态工具读取头部、完成解锁,再配置内核映射。内核 dm-crypt 不负责解析每个 LUKS JSON keyslot。
变化 通常涉及哪一层
增加或更换口令 PBKDF 派生解锁材料,处理 keyslot
恢复卷密钥并打开映射 使用 keyslot 中受保护的密钥材料
真正更换卷密钥 涉及数据重加密流程
所以改口令通常不等于重新加密整个文件系统;两种操作的成本与恢复需求不同。
LUKS1 的 keyslot 上限是 8;LUKS2 支持最多 32 个槽位,实际可用数量还受 keyslot 区域大小等条件限制。LUKS2 有冗余元数据及 JSON 描述,头部、keyslot 区和数据偏移均有明确布局及可配置参数,不能把它们统一叫作“固定前 16 MB 的 header”。参见 LUKS2 格式规范 和 cryptsetup 参数 。
PBKDF2、Argon2i、Argon2id 的可用性及参数依格式与工具配置而定。内存成本可以增加离线口令猜测的代价,但不能使弱口令变成不可破解的密钥。解锁阶段的 PBKDF 开销,也不要与每次扇区读写的加密开销混为一谈。
机密性不等于完整性
常见 XTS 模式提供扇区数据机密性,不提供通用的篡改认证和防回滚保证。需要认证加密或完整性保护时,还要选择相应模式、元数据存储及 dm-integrity 等配合机制,并理解其边界。
放行 discard 可以暴露使用空间等模式信息。已解锁时,具有读取权限的进程仍能获得明文;卷密钥和缓存的明文也会参与运行中系统的内存威胁模型。见 dm-crypt 文档 。
下面用浏览器实际加密同一段公开示例内容,再翻转密文中的一位。对比无认证的 CTR 与带认证的 GCM:能解出一些字节,与这些字节通过验证,是两种结果。
正在呈现知识画面 重新加载
密文改一位,解密成功能否说明内容可信? 无认证的 CTR 可以在密文位翻转后解出被修改的明文;GCM 验证认证标签,拒绝这里的改动。机密性、完整性和防回滚需要分别说明。
{"id":"encryption-and-authentication","title":"密文改一位,解密成功能否说明内容可信?","summary":"无认证的 CTR 可以在密文位翻转后解出被修改的明文;GCM 验证认证标签,拒绝这里的改动。机密性、完整性和防回滚需要分别说明。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><p class=\"caption\" data-i18n=\"plain\"></p><div class=\"plain\">ALLOW=0</div><label class=\"check\"><input type=\"checkbox\" id=\"flip\" checked><span data-i18n=\"flip\"></span></label><div class=\"two\"><div class=\"card\"><header>AES-CTR</header><p class=\"caption\" data-i18n=\"cipher\"></p><code id=\"cipher-ctr\" class=\"hex\"></code><p class=\"caption\" data-i18n=\"result\"></p><div id=\"result-ctr\" class=\"crypto-result\" role=\"status\"></div></div><div class=\"card\"><header>AES-GCM</header><p class=\"caption\" data-i18n=\"cipher\"></p><code id=\"cipher-gcm\" class=\"hex\"></code><p class=\"caption\" data-i18n=\"result\"></p><div id=\"result-gcm\" class=\"crypto-result\" role=\"status\"></div></div></div><p class=\"caption\" data-i18n=\"tag\"></p><code id=\"tag\" class=\"hex tag\"></code><div id=\"engine\" class=\"readout\" role=\"status\"></div><div class=\"copy\"><p id=\"explanation\" role=\"status\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"note\"></p></div><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n.plain{font:24px var(--viz-font-ui);padding:12px;background:var(--surface);border-radius:7px}.hex{display:block;font:12px/1.8 var(--viz-font-mono);overflow-wrap:anywhere;min-height:5.4em}.crypto-result{min-height:8em;font-size:14px;overflow-wrap:anywhere}.card .caption{min-height:4.8em}.tag{min-height:3.6em}.readout{min-height:3.2em}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}const plain=new TextEncoder().encode('ALLOW=0'),keyBytes=new Uint8Array(16),counter=new Uint8Array(16),iv=new Uint8Array(12);let keys={},encrypted={},ticket=0;const hex=b=>[...b].map(v=>v.toString(16).padStart(2,'0')).join(' ');async function update(){const current=++ticket,flip=$('#flip').checked;set('engine',t('loading'));const results=await Promise.all(['ctr','gcm'].map(async k=>{const data=new Uint8Array(encrypted[k]);if(flip)data[6]^=1;set('cipher-'+k,hex(data.slice(0,7)));try{const bytes=await crypto.subtle.decrypt(k==='ctr'?{name:'AES-CTR',counter,length:64}:{name:'AES-GCM',iv,tagLength:128},keys[k],data);return[k,new TextDecoder().decode(bytes)];}catch{return[k,t('failure')];}}));if(current!==ticket)return;for(const[k,result]of results)set('result-'+k,result);set('engine','Web Crypto · AES-CTR / AES-GCM');document.body.dataset.cryptoReady='true';}async function init(){$('#flip').disabled=true;set('engine',t('loading'));set('explanation',t('note'));if(!globalThis.crypto?.subtle){set('engine',t('unavailable'));return;}try{for(const k of ['ctr','gcm']){const name=k==='ctr'?'AES-CTR':'AES-GCM';keys[k]=await crypto.subtle.importKey('raw',keyBytes,name,false,['encrypt','decrypt']);encrypted[k]=new Uint8Array(await crypto.subtle.encrypt(k==='ctr'?{name,counter,length:64}:{name,iv,tagLength:128},keys[k],plain));}set('tag',hex(encrypted.gcm.slice(7)));$('#flip').disabled=false;$('#flip').onchange=update;await update();}catch{set('engine',t('unavailable'));}}init();","audio":false,"strings":{"cipher":"当前交给解密的密文","failure":"认证失败,不输出明文","flip":"将密文第 7 字节 XOR 0x01","heading":"密文改一位,解密成功能否说明内容可信?","intro":"对同一条公开消息同时执行 AES-CTR 与 AES-GCM。切换末字节的一位篡改,比较真正解密得到的结果与认证检查。","loading":"正在执行密码运算…","note":"0 和 1 的 ASCII 最低位不同。CTR 的相同密钥流会保留这次位翻转;GCM 标签没有随之重新生成,所以检查失败。","plain":"固定公开明文","reset":"重置","result":"解密 / 认证结果","scope":"浏览器 Web Crypto 执行实际 AES,固定公开 key/IV 只加密一次固定消息 ALLOW=0,再反复验证其原文/改动版本。不能用于真实数据或复用 nonce 加密多条消息。CTR 不是 dm-crypt 的 XTS 模拟;AEAD 本身也不自动提供存储防回滚。","scopeLabel":"模型范围与约定","tag":"GCM 的 128 位认证标签","unavailable":"此浏览器环境没有可用的 Web Crypto;无法执行本例。"}}
3. fscrypt:在文件系统内选择保护范围
fscrypt 为支持它的文件系统提供文件内容和文件名加密。目录策略可以让不同目录树使用不同主密钥,便于在同一文件系统中管理多个解锁域。
它通常不隐藏文件大小、权限、时间戳和目录结构等非文件名元数据,也不能保证离线观察者无法关联文件。默认的按文件派生与 nonce 机制,还存在 DIRECT_KEY、IV_INO_LBLK 等策略差异,不能概括成“任何配置下每个文件都有一把独立数据密钥”。
普通 fscrypt 设计主要保护静态数据机密性,不自动提供抵抗主动篡改和回滚的完整文件系统认证。移除主密钥也需要考虑已打开文件和缓存中仍被使用的派生密钥;不能把 ioctl 返回理解成所有明文瞬间消失。
新目录策略通常在满足空目录等条件时设置,随后创建的文件继承相应策略;不会把一棵已有明文目录凭空转换完成。FS_IOC_GET_ENCRYPTION_PWSALT 是历史口令派生辅助接口,也不是读取任意目录独有盐值的通用接口。策略与限制见 fscrypt 文档 。
比较点 dm-crypt fscrypt
管理边界 块映射 文件系统内的策略及密钥域
文件系统元数据 在加密映射内部时一并成为加密块 许多非文件名元数据仍可见
解锁粒度 通常是整个映射 可管理不同目录树的密钥
解锁后的访问控制 仍依赖权限/LSM 等 同样仍依赖权限/LSM 等
两者也可以叠加;它们不是互斥的产品替代关系。
4. WireGuard 与 kTLS:不同网络层的加密
WireGuard 提供三层加密隧道,使用 Noise 握手、Curve25519 和 ChaCha20-Poly1305 等固定协议组件。静态公钥标识 peer,握手产生会话密钥;数据包的计数器、认证和重放处理共同参与保护。密钥更新由时间、消息数量和会话状态等条件触发,不是严格每两分钟无条件执行一次定时动作。
Curve25519 不是后量子算法。WireGuard 可混入预共享对称密钥作为附加防护,但这不能概括为“后量子问题不重要”或整个系统已获得所有后量子安全属性。配置接口主要使用 generic netlink,网络设备创建等还涉及常规网络接口管理。见 WireGuard 协议 。
kTLS 则将受支持的 TLS 记录层 发送/接收处理交给内核或硬件;握手、证书验证和协议状态管理仍主要由用户态 TLS 库负责。连接协商后需设置 TCP 的 TLS ULP,并分别配置 TX/RX 所需的密码状态,不能只复制一个 setsockopt 就当完整 TLS 客户端。
sendfile 等路径可减少用户态搬运,但软件加密、NIC 卸载、零拷贝发送是不同能力,是否可用依内核、协议、算法与驱动支持而定。见 kTLS 文档 。
5. TPM:按策略使用秘密,策略本身也要管理
TPM 可以保护密钥对象,并依据 PCR 等条件约束解封或使用。典型 LUKS 集成会生成解锁材料,将其与 TPM 对象/策略和 LUKS token 关联;不意味着把整个磁盘加密密钥永远只留在 TPM 内做每次扇区加密。
PCR 是测量事件累积结果,不是 TPM 自己理解“这个内核一定良性”。选择哪些 PCR、测量链是否可信、谁签署授权策略,决定了解锁约束。允许合法升级还需要策略更新、签名 PCR policy 或恢复凭据等设计。
启动授权、测量与解封分别证明什么
Secure Boot 验证引导代码的授权关系,measured boot 记录测量,TPM sealing 依据策略限制秘密使用;三者可以配合但不能互相代替。正常解锁后的运行时漏洞也不会自动被 PCR 绑定阻止。实现可参考 systemd-cryptenroll 源码与文档 。
6. 用 AF_ALG 观察一次摘要请求
下面通过 Linux AF_ALG 调用内核 SHA-256,对公开字符串做已知结果校验,不读取密钥或修改存储。需要 Python/Linux 支持 AF_ALG,内核启用对应算法用户接口。它演示内核接口,不表示一般应用应替换自己的密码库。
import socket
expected = ' ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad'
with socket . socket ( socket . AF_ALG , socket . SOCK_SEQPACKET , 0 ) as alg :
alg . bind ( ( ' hash' , ' sha256' ) )
op , _ = alg . accept ( )
with op :
sent = op . send ( b ' abc' )
if sent != 3 :
raise RuntimeError ( ' unexpected short input' )
digest = op . recv ( 32 )
assert len ( digest ) == 32 and digest . hex ( ) == expected
print ( digest . hex ( ) )
本环境未启用 AF_ALG 时,创建 socket 会返回 EAFNOSUPPORT;这表示接口能力不可用,不能据此判断 SHA-256 算法本身有误。
若 socket 创建返回 EAFNOSUPPORT,先检查 CONFIG_CRYPTO_USER_API_HASH 等支持;不能因此判断 SHA-256 算法实现错误。
摘要匹配证明这条调用路径得到预期结果,不证明整个加密存储配置、密钥管理或硬件卸载已正确。AF_ALG 的接口限制和支持情况见内核用户态加密接口 。