spaces: a rotated key goes to the person behind the room key, and the epoch it belongs to is found by number
This commit is contained in:
1 parent
7b7f48ebc6
commit
d6102101af
1 file changed
+48
-9
+48
-9
@@ -209,6 +209,17 @@ async fn my_key(app: &tauri::AppHandle) -> Result<[u8; 32], String> {
|
||||
.ok_or("create a SoulStream identity first")?;
|
||||
key_of(&info.public_key)
|
||||
}
|
||||
/// The key from the newest epoch below `epoch`. The map is keyed by a
|
||||
/// STRING, so it iterates lexicographically — `.last()` picks "9" over "10"
|
||||
/// and hands back a key from six epochs ago the moment a Space gets busy.
|
||||
fn key_below(rec: &SpaceRecord, epoch: u64) -> Option<String> {
|
||||
rec.keys
|
||||
.iter()
|
||||
.filter_map(|(e, k)| e.parse::<u64>().ok().filter(|n| *n < epoch).map(|n| (n, k.clone())))
|
||||
.max_by_key(|(n, _)| *n)
|
||||
.map(|(_, k)| k)
|
||||
}
|
||||
|
||||
/// The secret we sign with inside one Space, minted the first time we need
|
||||
/// it. Custody matches the inbox box key: it sits in the app folder, not in
|
||||
/// the delegate — a persona restored on another device mints fresh room keys
|
||||
@@ -458,19 +469,21 @@ pub async fn open(app: &tauri::AppHandle, owner_b58: &str, space_b58: &str) -> R
|
||||
let (state, reachable) = fetch_state(app, &owner, &space_id).await?;
|
||||
if let Some(c) = &state.checkpoint {
|
||||
let r = l.spaces.get_mut(&rid).unwrap();
|
||||
let changed = r.name != c.name || r.last_epoch != c.epoch || r.private != c.private;
|
||||
let mut dirty = r.name != c.name || r.last_epoch != c.epoch || r.private != c.private;
|
||||
r.name = c.name.clone();
|
||||
r.last_epoch = c.epoch;
|
||||
r.private = c.private;
|
||||
// Adding members never rotates the key: the previous epoch's key
|
||||
// carries forward until a removal issues a new one.
|
||||
// carries forward until a removal issues a new one. A removal DOES
|
||||
// rotate, and the real key arrives sealed — `on_inbox_space_key`
|
||||
// overwrites this one when it lands.
|
||||
if c.private && !r.keys.contains_key(&c.epoch.to_string()) {
|
||||
if let Some((_, k)) = r.keys.iter().filter(|(e, _)| e.parse::<u64>().unwrap_or(0) < c.epoch).last() {
|
||||
let k = k.clone();
|
||||
if let Some(k) = key_below(r, c.epoch) {
|
||||
r.keys.insert(c.epoch.to_string(), k);
|
||||
dirty = true;
|
||||
}
|
||||
}
|
||||
if changed {
|
||||
if dirty {
|
||||
let _ = save(app, &l);
|
||||
}
|
||||
}
|
||||
@@ -581,7 +594,7 @@ pub async fn invite(app: &tauri::AppHandle, owner_b58: &str, space_b58: &str, pe
|
||||
.keys
|
||||
.get(&c.epoch.to_string())
|
||||
.cloned()
|
||||
.or_else(|| rec.keys.values().last().cloned())
|
||||
.or_else(|| key_below(rec, c.epoch + 1))
|
||||
.unwrap_or_else(|| b58(&rand32()));
|
||||
rec.keys.insert(c.epoch.to_string(), k.clone());
|
||||
key = Some(key_of(&k)?);
|
||||
@@ -632,11 +645,21 @@ pub async fn remove_member(app: &tauri::AppHandle, owner_b58: &str, space_b58: &
|
||||
// A removal rotates: everyone who stays gets the new epoch's key.
|
||||
let k = rand32();
|
||||
rec.keys.insert(c.epoch.to_string(), b58(&k));
|
||||
// An inbox is addressed by PERSONA, and since members became
|
||||
// room-local the roster no longer holds one. Only this device can
|
||||
// bridge the two; sealing to the roster key reaches nobody, and every
|
||||
// member who stayed silently keeps a stale key.
|
||||
let targets: Vec<[u8; 32]> = c
|
||||
.members
|
||||
.iter()
|
||||
.filter(|m| m.key != me)
|
||||
.map(|m| rec.roster.get(&b58(&m.key)).and_then(|p| key_of(p).ok()).unwrap_or(m.key))
|
||||
.collect();
|
||||
save(app, &l)?;
|
||||
for m in c.members.iter().filter(|m| m.key != me) {
|
||||
for to in targets {
|
||||
let cargo = Cargo::SpaceKey(SpaceKeyCargo { space_id, owner, epoch: c.epoch, key: k });
|
||||
if let Err(e) = inbox::send(app, &m.key, &cargo).await {
|
||||
log::warn!("spaces: could not hand the epoch-{} key to {}: {e}", c.epoch, b58(&m.key));
|
||||
if let Err(e) = inbox::send(app, &to, &cargo).await {
|
||||
log::warn!("spaces: could not hand the epoch-{} key to {}: {e}", c.epoch, b58(&to));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -923,6 +946,22 @@ mod tests {
|
||||
assert!(rec("member", Some([9u8; 32])).roster.get(&room).is_none());
|
||||
}
|
||||
|
||||
/// Epoch keys are keyed by a STRING, and a BTreeMap orders those
|
||||
/// lexicographically — "9" sorts after "10". A Space only has to reach its
|
||||
/// tenth epoch for the wrong key to carry forward, and the symptom is
|
||||
/// messages that simply will not open.
|
||||
#[test]
|
||||
fn the_newest_epoch_key_is_found_by_number_not_by_string() {
|
||||
let mut r = rec("member", None);
|
||||
for (e, k) in [(1u64, "one"), (9, "nine"), (10, "ten"), (11, "eleven")] {
|
||||
r.keys.insert(e.to_string(), k.into());
|
||||
}
|
||||
assert_eq!(key_below(&r, 12).as_deref(), Some("eleven"));
|
||||
assert_eq!(key_below(&r, 11).as_deref(), Some("ten"), "lexicographic order would say \"nine\"");
|
||||
assert_eq!(key_below(&r, 10).as_deref(), Some("nine"));
|
||||
assert_eq!(key_below(&r, 1), None, "nothing precedes the first epoch");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn contract_generation_is_pinned() {
|
||||
let ids = FreenetAdapter::contract_ids(SPACE_WASM.to_vec(), &sp::make_params(&[7u8; 32], &[1u8; 16]));
|
||||
|
||||
Reference in new issue
Block a user