Skip to main content

SessionKeys

Struct SessionKeys 

Source
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

Source

pub fn new() -> Self

新建空会话,使用默认 15 分钟空闲锁定。

Source

pub const fn with_idle_timeout(self, timeout: Duration) -> Self

设置空闲锁定时长。

Source

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。

Source

pub fn insert_kek( &mut self, label: &str, kind: CredentialKind, vault_salt: &[u8; 16], kek: Kek, )

直接插入一个已有的 KEK。

用于生物识别(OS 密钥库解出)和恢复码(熵已足够,无需 KDF)两种来源。

Source

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。

Source

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 当失败:重复装入同一把钥匙是正常操作 (比如又按了一次指纹),只是没有新增条目。

Source

pub fn forget( &mut self, label: &str, kind: CredentialKind, vault_salt: &[u8; 16], ) -> bool

移除某个凭据(其 KEK 会被清零)。

返回是否确实移除了条目。

Source

pub fn scannable_for(&self, vault_salt: &[u8; 16]) -> Vec<UnlockedCredential>

列出适用于指定 vault_salt 且参与扫描的凭据。

扫描器用这个列表逐一尝试解包 slot。返回的 KEK 是副本, 调用方用完即析构清零。

Source

pub fn all_for(&self, vault_salt: &[u8; 16]) -> Vec<UnlockedCredential>

列出适用于指定 vault_salt 的全部凭据(含不参与扫描的)。

用于「手动打开单个文件」——此时可以接受 portable/recovery 的额外成本。

Source

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 副本,调用方用完即析构清零。

Source

pub fn len(&self) -> usize

缓存中的凭据数量。

Source

pub fn is_empty(&self) -> bool

缓存是否为空(即处于锁定状态)。

Source

pub const fn kdf_runs(&self) -> u64

至今执行过的 Argon2 次数。

用于验证缓存生效:解锁 N 个不同凭据后该值应为 N, 无论调用了多少次 SessionKeys::unlock_password。

Source

pub fn lock(&mut self)

立即锁定:清零所有 KEK。

调用方还应同步关闭已打开的媒体流、清理 WebView 缓存(规范 §11.2)。

Source

pub fn touch(&mut self)

刷新活动时间戳,推迟空闲锁定。

Source

pub fn idle_for(&self) -> Duration

距上次活动的时长。

Source

pub fn should_auto_lock(&self) -> bool

是否已达到空闲锁定阈值。

Source

pub fn auto_lock_if_idle(&mut self) -> bool

若已空闲超时则锁定,返回是否执行了锁定。

宿主应用应定期(如每 30 秒)调用此方法。

Trait Implementations§

Source§

impl Debug for SessionKeys

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl Default for SessionKeys

Source§

fn default() -> Self

Returns the “default value” for a type. Read more
Source§

impl Drop for SessionKeys

Source§

fn drop(&mut self)

会话析构时确保清零。

Source§

fn pin_drop(self: Pin<&mut Self>)

🔬This is a nightly-only experimental API. (pin_ergonomics)
Execute the destructor for this type, but different to Drop::drop, it requires self to be pinned. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.