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")?;
|
.ok_or("create a SoulStream identity first")?;
|
||||||
key_of(&info.public_key)
|
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
|
/// 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
|
/// 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
|
/// 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?;
|
let (state, reachable) = fetch_state(app, &owner, &space_id).await?;
|
||||||
if let Some(c) = &state.checkpoint {
|
if let Some(c) = &state.checkpoint {
|
||||||
let r = l.spaces.get_mut(&rid).unwrap();
|
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.name = c.name.clone();
|
||||||
r.last_epoch = c.epoch;
|
r.last_epoch = c.epoch;
|
||||||
r.private = c.private;
|
r.private = c.private;
|
||||||
// Adding members never rotates the key: the previous epoch's key
|
// 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 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() {
|
if let Some(k) = key_below(r, c.epoch) {
|
||||||
let k = k.clone();
|
|
||||||
r.keys.insert(c.epoch.to_string(), k);
|
r.keys.insert(c.epoch.to_string(), k);
|
||||||
|
dirty = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if changed {
|
if dirty {
|
||||||
let _ = save(app, &l);
|
let _ = save(app, &l);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -581,7 +594,7 @@ pub async fn invite(app: &tauri::AppHandle, owner_b58: &str, space_b58: &str, pe
|
|||||||
.keys
|
.keys
|
||||||
.get(&c.epoch.to_string())
|
.get(&c.epoch.to_string())
|
||||||
.cloned()
|
.cloned()
|
||||||
.or_else(|| rec.keys.values().last().cloned())
|
.or_else(|| key_below(rec, c.epoch + 1))
|
||||||
.unwrap_or_else(|| b58(&rand32()));
|
.unwrap_or_else(|| b58(&rand32()));
|
||||||
rec.keys.insert(c.epoch.to_string(), k.clone());
|
rec.keys.insert(c.epoch.to_string(), k.clone());
|
||||||
key = Some(key_of(&k)?);
|
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.
|
// A removal rotates: everyone who stays gets the new epoch's key.
|
||||||
let k = rand32();
|
let k = rand32();
|
||||||
rec.keys.insert(c.epoch.to_string(), b58(&k));
|
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)?;
|
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 });
|
let cargo = Cargo::SpaceKey(SpaceKeyCargo { space_id, owner, epoch: c.epoch, key: k });
|
||||||
if let Err(e) = inbox::send(app, &m.key, &cargo).await {
|
if let Err(e) = inbox::send(app, &to, &cargo).await {
|
||||||
log::warn!("spaces: could not hand the epoch-{} key to {}: {e}", c.epoch, b58(&m.key));
|
log::warn!("spaces: could not hand the epoch-{} key to {}: {e}", c.epoch, b58(&to));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -923,6 +946,22 @@ mod tests {
|
|||||||
assert!(rec("member", Some([9u8; 32])).roster.get(&room).is_none());
|
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]
|
#[test]
|
||||||
fn contract_generation_is_pinned() {
|
fn contract_generation_is_pinned() {
|
||||||
let ids = FreenetAdapter::contract_ids(SPACE_WASM.to_vec(), &sp::make_params(&[7u8; 32], &[1u8; 16]));
|
let ids = FreenetAdapter::contract_ids(SPACE_WASM.to_vec(), &sp::make_params(&[7u8; 32], &[1u8; 16]));
|
||||||
|
|||||||
Reference in new issue
Block a user