From aaf591956a2b2222a3a0fa1db3ae690ef1b05c68 Mon Sep 17 00:00:00 2001 From: fylorn <249551762+fylorn@users.noreply.github.com> Date: Mon, 19 Oct 2026 14:25:43 +0800 Subject: [PATCH] tw-pack: read_strict refuses a plain pack when a key is given; scrypt cap 18 - read_strict(bytes, unseal): with a passphrase or an identity, a pack that is not encrypted is refused (pack.not_encrypted). For a sync location with encryption on, where accepting a plain version would let anyone who can write there change this device's configuration. - The largest scrypt work factor a pack may ask for is now the one packs are written with (18), like the sync keyring; 22 meant about 4 GiB. Co-Authored-By: Claude Opus 5.5 --- crates/tw-api/msg-codes.txt | 1 + crates/tw-pack/src/lib.rs | 26 +++++++++++++++++++++++--- crates/tw-pack/src/tests.rs | 31 +++++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 3 deletions(-) diff --git a/crates/tw-api/msg-codes.txt b/crates/tw-api/msg-codes.txt index 5dd966db..3f3a4c23 100644 --- a/crates/tw-api/msg-codes.txt +++ b/crates/tw-api/msg-codes.txt @@ -512,6 +512,7 @@ pack.need_key pack.need_passphrase pack.no_such_item pack.not_base64 +pack.not_encrypted pack.passphrase_short pack.schema_too_new pack.too_large diff --git a/crates/tw-pack/src/lib.rs b/crates/tw-pack/src/lib.rs index 3ede6064..f32736b5 100644 --- a/crates/tw-pack/src/lib.rs +++ b/crates/tw-pack/src/lib.rs @@ -305,6 +305,9 @@ pub enum PackError { MissingEntry { name: String }, #[error("{}", self.msg())] TooLarge { limit: usize }, + /// 要求加密的地方读到了一个没加密的包([`read_strict`]) + #[error("{}", self.msg())] + NotEncrypted, } impl PackError { @@ -347,6 +350,10 @@ impl PackError { "pack.missing_entry", name = name => "This configuration pack has no {name}." ), + PackError::NotEncrypted => msg!( + "pack.not_encrypted" => + "This configuration pack is not encrypted, though it has to be, so it was not opened." + ), PackError::TooLarge { limit } => msg!( "pack.too_large", limit = limit / (1024 * 1024) => "This configuration pack is larger than {limit} MiB." @@ -452,8 +459,9 @@ fn work_factor() -> u8 { } } -/// 解开时最多接受多大的工作量。比写的时候大一些:将来调高了,旧版本照样读得了新包 -const MAX_WORK_FACTOR: u8 = 22; +/// 解开时最多接受多大的工作量:**和写的时候一样**(同步位置的 keyring 也是这个上限)。一个 +/// 不可信的包能指定工作量,放宽了就是让它一打开就占掉几个 GB 内存(`2^22` 约 4 GiB) +const MAX_WORK_FACTOR: u8 = 18; /// **只给测试用**:把口令加密的工作量调低,测试不用每次等一秒。0 是不改 #[doc(hidden)] @@ -466,7 +474,19 @@ pub fn is_encrypted(bytes: &[u8]) -> bool { bytes.starts_with(b"age-encryption.org/") } -/// 读一个配置包。 +/// 同 [`read`],**加密得和给的钥匙对得上**:给了口令或密钥([`Unseal::Passphrase`] / +/// [`Unseal::Identity`])时,没加密的包不收([`PackError::NotEncrypted`])。同步位置开着加密 +/// 时用它:一个照收的明文版本,等于对那个位置有写权限的谁都能把配置写进这台设备。不给 +/// ([`Unseal::None`])时和 [`read`] 一样 +pub fn read_strict(bytes: &[u8], unseal: Unseal) -> Result { + if !matches!(unseal, Unseal::None) && !is_encrypted(bytes) { + return Err(PackError::NotEncrypted); + } + read(bytes, unseal) +} + +/// 读一个配置包。**给了钥匙也照收明文的包**:导入用户自己挑的文件时,加没加密都行;要求加密 +/// 的地方用 [`read_strict`] pub fn read(bytes: &[u8], unseal: Unseal) -> Result { if bytes.len() > MAX_BYTES { return Err(PackError::TooLarge { limit: MAX_BYTES }); diff --git a/crates/tw-pack/src/tests.rs b/crates/tw-pack/src/tests.rs index d4e9e03b..1a7a78ed 100644 --- a/crates/tw-pack/src/tests.rs +++ b/crates/tw-pack/src/tests.rs @@ -332,3 +332,34 @@ fn origins_keep_their_words() { } assert_eq!(PackOrigin::from_slug("nope"), PackOrigin::Other); } + +/// 要求加密的地方:给了钥匙就不收明文;加密的照常解;没给钥匙时和 `read` 一样 +#[test] +fn a_strict_read_refuses_a_plain_pack_when_a_key_is_given() { + fast(); + let pack = sample(); + let plain = write(&pack, Seal::None).unwrap(); + let id = age::x25519::Identity::generate(); + let sealed = write(&pack, Seal::Recipient(&id.to_public())).unwrap(); + assert_eq!( + read_strict(&plain, Unseal::Identity(&id)).unwrap_err(), + PackError::NotEncrypted + ); + assert_eq!( + read_strict(&plain, Unseal::Passphrase("whatever it is")).unwrap_err(), + PackError::NotEncrypted + ); + assert_eq!(PackError::NotEncrypted.msg().code, "pack.not_encrypted"); + assert_eq!( + read_strict(&sealed, Unseal::Identity(&id)) + .unwrap() + .config_yaml, + pack.config_yaml + ); + assert_eq!( + read_strict(&plain, Unseal::None).unwrap().config_yaml, + pack.config_yaml + ); + // `read` 照收:导入的是用户自己挑的文件 + assert!(read(&plain, Unseal::Identity(&id)).is_ok()); +}