Skip to content

Commit 20560da

Browse files
committed
test(stack-encrypt): a keyset scope refuses a target value from another keyset
Review of #1095: the resolver opens a target value through the client, which opens every keyset's values, and the lowering's `confine` is the only thing that holds it to the scope's keyset. No test used a second keyset on a target field, so removing `scoped_to` there would have let one tenant's cipher open another's column with every test green. The new test seals a plan of one target field under acme and opens it under globex: ForeignKeyset before any key is retrieved; acme's own scope still opens it. Claude-Session: https://claude.ai/code/session_01V3WFXwax4J3uecpFEJ6yHc
1 parent 2d5daee commit 20560da

1 file changed

Lines changed: 45 additions & 0 deletions

File tree

‎packages/stack-encrypt/src/dynamic/record.rs‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4416,6 +4416,51 @@ mod tests {
44164416
assert_eq!(generates(&cipher), 0, "nothing minted");
44174417
}
44184418

4419+
/// The resolver opens a target value through the client, which opens
4420+
/// every keyset's values; `confine` is what holds it to the scope's
4421+
/// keyset. A plan of one target field, so no lowered leaf can cause
4422+
/// the refusal instead: this fails if `confine` stops calling
4423+
/// `scoped_to`, and one tenant's cipher opens another's column.
4424+
#[tokio::test]
4425+
async fn a_keyset_scope_refuses_a_target_value_from_another_keyset() {
4426+
let cipher = cipher().await;
4427+
let named = |n: &str| IdentifiedBy::Name(n.to_string().into());
4428+
let acme = cipher.keyset(named("acme")).await.expect("acme");
4429+
let globex = cipher.keyset(named("globex")).await.expect("globex");
4430+
let plan = plan_with(
4431+
obj(vec![("email", target_spec(label("email"), TEXT_EQ))]),
4432+
&FakeEql,
4433+
)
4434+
.unwrap();
4435+
let sealed = encrypt_with(&acme, obj(vec![("email", s("a@x"))]), &plan, &FakeEql)
4436+
.unwrap()
4437+
.await
4438+
.unwrap();
4439+
let result = decrypt_with(Scope::Keyset(globex), sealed, &plan, &FakeEql)
4440+
.expect("the record fits")
4441+
.await;
4442+
match result {
4443+
Err(crate::Error::ForeignKeyset { .. }) => {}
4444+
Err(other) => panic!("refused, but not as a foreign keyset: {other}"),
4445+
Ok(_) => panic!("globex opened acme's target value"),
4446+
}
4447+
assert_eq!(
4448+
retrieves(&cipher),
4449+
0,
4450+
"refused before any key was retrieved"
4451+
);
4452+
// acme's own scope, and the client, still open it.
4453+
let sealed = encrypt_with(&acme, obj(vec![("email", s("a@x"))]), &plan, &FakeEql)
4454+
.unwrap()
4455+
.await
4456+
.unwrap();
4457+
let opened = decrypt_with(Scope::Keyset(acme), sealed, &plan, &FakeEql)
4458+
.unwrap()
4459+
.await
4460+
.expect("its own keyset opens it");
4461+
assert_eq!(text_of(&object(opened)[0].1), "a@x");
4462+
}
4463+
44194464
#[test]
44204465
fn the_target_and_output_forms_are_exclusive() {
44214466
let both = obj(vec![

0 commit comments

Comments
 (0)