pub struct SessionKeys { /* private fields */ }Expand description
会话密钥缓存。
§生命周期
会话内有效。任一锁定条件触发(空闲超时、系统睡眠、切后台、手动锁定)
即调用 SessionKeys::lock 清零全部 KEK。
§示例
use omy_core::session::{SessionKeys, CredentialKind};
use omy_core::crypto::Argon2Params;
let mut s = SessionKeys::new();
let salt = [0x11u8; 16];
s.unlock_password("主密码", &salt, "hunter2", Argon2Params::TEST_WEAK)?;
// 同名同盐再解锁一次会覆盖,不会多出一条凭据
s.unlock_password("主密码", &salt, "hunter2", Argon2Params::TEST_WEAK)?;
assert_eq!(s.len(), 1);Implementations§
Source§impl SessionKeys
impl SessionKeys
Sourcepub const fn with_idle_timeout(self, timeout: Duration) -> Self
pub const fn with_idle_timeout(self, timeout: Duration) -> Self
设置空闲锁定时长。
Sourcepub fn unlock_password(
&mut self,
label: &str,
vault_salt: &[u8; 16],
password: &str,
params: Argon2Params,
) -> Result<()>
pub fn unlock_password( &mut self, label: &str, vault_salt: &[u8; 16], password: &str, params: Argon2Params, ) -> Result<()>
用密码解锁并缓存 KEK。
同一个 (vault_salt, kind, label) 再次解锁时会重新派生并覆盖旧条目,
旧 KEK 在被挤出时清零。
§为什么不能「命中缓存就跳过 KDF」
缓存键里没有密码(有意如此,见 [CacheKey]),所以「命中」只说明
这个名字用过,不说明密码相同。早先的实现在命中时直接
return Ok(()),于是同一 label 下的第二个密码从未被派生,会话里留着
的还是上一个 KEK——而返回值是 Ok,调用方以为解锁成功了。
上层大量使用固定 label(GUI 恒为 "main",见 commands::unlock),
这条路径因此是常态而非边角:
- 先打错一次密码、再输对的 → 第二次被忽略,一直提示打不开;
- 改完密码后自动装入新密码 → 装不进去,文件被显示成「需要密码」。
扫描性能不依赖这个早退:慢速 KDF 每个密码只跑一次是靠调用方在
文件循环外解锁(见 scan 命令),而每个文件复用 KEK 是靠
SessionKeys::scannable_for 从缓存读。想显式避免重复派生的调用方
可以自己留住 KEK 走 SessionKeys::insert_kek。
§为什么不是把密码指纹加进缓存键
那需要在 Argon2 之前先算一个快速的密码哈希并驻留内存。能读到 进程内存的攻击者本来只能对着 Argon2 逐个猜(每次几百毫秒),有了 快速指纹就能离线秒破——而密码常被复用到别处。为省一次 KDF 削弱 密码强度不值得。
§Errors
Argon2 派生失败时返回 crate::Error::KeyDerivation。
Sourcepub fn insert_kek(
&mut self,
label: &str,
kind: CredentialKind,
vault_salt: &[u8; 16],
kek: Kek,
)
pub fn insert_kek( &mut self, label: &str, kind: CredentialKind, vault_salt: &[u8; 16], kek: Kek, )
直接插入一个已有的 KEK。
用于生物识别(OS 密钥库解出)和恢复码(熵已足够,无需 KDF)两种来源。
Sourcepub fn add_password(
&mut self,
label: &str,
vault_salt: &[u8; 16],
password: &str,
params: Argon2Params,
) -> Result<bool>
pub fn add_password( &mut self, label: &str, vault_salt: &[u8; 16], password: &str, params: Argon2Params, ) -> Result<bool>
追加一个密码,不挤掉已装入的其它密码。
这是「同时用多个密码浏览」的入口:真实密码与诱饵密码可以并存, 扫描时一遍遍历就能各自命中自己的文件。
§与 SessionKeys::unlock_password 的分工
那个是替换语义:同一个 (salt, kind, label) 再解锁一次就覆盖,
适合「这个位置只该有一个密码」的场景(如改完密码后装入新密码)。
本方法是累加语义,同一个 vault 下可以有任意多个。
§为什么去重判据是指纹,不是 label
两个方向都必须挡住,而 label 一个都挡不住:
- 同一个密码输两次(用户忘了已经输过、或加密后又手动解锁一次)
必须只算一条。按 label 判断做不到:调用方给的名字可能不同,
于是同一个密码被算成两条凭据,状态栏显示「2 个密码已解锁」。
这正是上一轮踩过的缺陷,当时的对策是把 label 统一写死成
"main", 而那又导致第二个不同的密码被静默挤掉——两个缺陷是同一个根因 的两面:拿名字当身份。 - 两个不同的密码恰好重名必须各算一条。写死 label 时这条必然失败。
KEK 指纹是密码在本 vault 下的稳定标识,两个方向都正确。
§返回值
true 表示这是一个新密码,false 表示它与已装入的某个密码相同、
本次没有新增条目。调用方不应把 false 当作失败:用户重复输入
同一个密码是正常操作,只是不该让计数虚高。
§开销
无论是否重复都会跑一次 Argon2——判断重复需要先有 KEK,而拿到 KEK
就得跑 KDF。想省掉这次 KDF 只能在派生前先算一个快速的密码哈希
并驻留内存,那等于给能读进程内存的攻击者一个可离线秒破的口令摘要
(同 SessionKeys::unlock_password 中的论述)。为省几百毫秒削弱
密码强度不值得。
§Errors
Argon2 派生失败时返回 crate::Error::KeyDerivation。
Sourcepub fn add_kek(
&mut self,
label: &str,
kind: CredentialKind,
vault_salt: &[u8; 16],
kek: Kek,
) -> bool
pub fn add_kek( &mut self, label: &str, kind: CredentialKind, vault_salt: &[u8; 16], kek: Kek, ) -> bool
追加一把已有的 KEK,不挤掉已装入的其它凭据。
§与 SessionKeys::insert_kek 的分工
那个是替换语义:同一个 (salt, kind, label) 再插一次就覆盖。
本方法是累加语义,并按 KEK 指纹去重——与
SessionKeys::add_password 一致。
设备密钥必须走这条:用户按了指纹之后,之前手动输入的密码不该消失。
走 insert_kek 的话只要 label 撞上就会静默挤掉一条,而那条可能
正是他刚输的另一个库的密码。
§返回值
true 表示这是一把新凭据,false 表示它与已装入的某条相同。
调用方不应把 false 当失败:重复装入同一把钥匙是正常操作
(比如又按了一次指纹),只是没有新增条目。
Sourcepub fn forget(
&mut self,
label: &str,
kind: CredentialKind,
vault_salt: &[u8; 16],
) -> bool
pub fn forget( &mut self, label: &str, kind: CredentialKind, vault_salt: &[u8; 16], ) -> bool
移除某个凭据(其 KEK 会被清零)。
返回是否确实移除了条目。
Sourcepub fn scannable_for(&self, vault_salt: &[u8; 16]) -> Vec<UnlockedCredential>
pub fn scannable_for(&self, vault_salt: &[u8; 16]) -> Vec<UnlockedCredential>
列出适用于指定 vault_salt 且参与扫描的凭据。
扫描器用这个列表逐一尝试解包 slot。返回的 KEK 是副本, 调用方用完即析构清零。
Sourcepub fn all_for(&self, vault_salt: &[u8; 16]) -> Vec<UnlockedCredential>
pub fn all_for(&self, vault_salt: &[u8; 16]) -> Vec<UnlockedCredential>
列出适用于指定 vault_salt 的全部凭据(含不参与扫描的)。
用于「手动打开单个文件」——此时可以接受 portable/recovery 的额外成本。
Sourcepub fn all_keks(&self) -> Vec<Kek>
pub fn all_keks(&self) -> Vec<Kek>
列出会话里全部已解锁凭据的 KEK 副本,不按 vault_salt 过滤。
§为什么需要一个「不分库」的入口
Self::all_for 按 vault_salt 过滤,服务的是「打开某个 vault 里的
文件」——那里 KEK 必须与文件所属 vault 的盐匹配。但远程位置的密码槽
不属于任何 vault:它按位置自己的 uuid 派生槽密钥(见
omy-remote 的 place_secret),解锁时要拿所有已输入过的密码去
试,无论它当初是为哪个库输的。这正是「输过任一密码就能开设了该密码的
位置」的实现基础。
返回的是 KEK 副本,调用方用完即析构清零。
Sourcepub const fn kdf_runs(&self) -> u64
pub const fn kdf_runs(&self) -> u64
至今执行过的 Argon2 次数。
用于验证缓存生效:解锁 N 个不同凭据后该值应为 N,
无论调用了多少次 SessionKeys::unlock_password。
Sourcepub fn should_auto_lock(&self) -> bool
pub fn should_auto_lock(&self) -> bool
是否已达到空闲锁定阈值。
Sourcepub fn auto_lock_if_idle(&mut self) -> bool
pub fn auto_lock_if_idle(&mut self) -> bool
若已空闲超时则锁定,返回是否执行了锁定。
宿主应用应定期(如每 30 秒)调用此方法。