Skip to content

Commit 9117250

Browse files
committed
Revert "F-A3 Phase 1: client-derived userKey_v2 + GlobalUsersIndex.users_v2 (fixes #15)"
This reverts commit ef56a7a.
1 parent ef56a7a commit 9117250

7 files changed

Lines changed: 5 additions & 237 deletions

File tree

Cargo.lock

Lines changed: 0 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,7 @@ axum = { workspace = true }
4848
base64 = { workspace = true }
4949
md-5 = { workspace = true }
5050
blake3 = { workspace = true }
51-
cid = { workspace = true }
5251
hex = { workspace = true }
53-
serde_ipld_dagcbor = { workspace = true }
5452
jsonwebtoken = { workspace = true }
5553
serde = { workspace = true }
5654
tempfile = { workspace = true }

crates/fula-cli/src/handlers/users_index_publisher.rs

Lines changed: 3 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -273,34 +273,16 @@ pub struct BucketEntry {
273273
/// Global users-index CBOR. Master pins one per snapshot; the CID
274274
/// is published via IPNS (every flush) and to the chain anchor
275275
/// (every 12h).
276-
///
277-
/// **Audit F-A3 (issue #15)**: `users_v2` is the additive
278-
/// client-derived lookup-key map. When `users_v2` is empty (no Mode B
279-
/// users yet), the CBOR byte-shape is identical to the legacy v1
280-
/// schema thanks to `skip_serializing_if = "BTreeMap::is_empty"`.
281-
/// SDK clients on Mode B prefer `users_v2.get(client_derived_v2_key)`
282-
/// and fall back to `users.get(hashed_user_id_v1)` on miss. v1 is
283-
/// kept indefinitely per the design decision — never-upgrading
284-
/// clients keep working.
285276
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
286277
pub struct GlobalUsersIndex {
287278
pub v: u32,
288279
/// Monotonic publisher sequence. Replay defense: SDK persists
289280
/// `highest_seen_sequence`; rejects payloads with regression.
290281
pub sequence: u64,
291282
pub updated_at_unix: u64,
292-
/// **v1, kept indefinitely.** `hashed_user_id_hex` (32 hex chars
293-
/// = 16-byte `BLAKE3("fula:user_id:" || user_id)[..16]`) →
283+
/// `userKey_hex` (32 hex chars = 16-byte hashed_user_id) →
294284
/// per-user bucketsIndex CID (string). BTreeMap for determinism.
295285
pub users: BTreeMap<String, String>,
296-
/// **v2 client-derived lookup keys** (audit F-A3 / issue #15).
297-
/// `client_derived_lookup_key_hex` (32 hex chars = 16-byte
298-
/// `BLAKE3("fula:user-lookup-v2:" || user_id || master_KEK_public)[..16]`) →
299-
/// per-user bucketsIndex CID. Populated only for users whose
300-
/// client has explicitly POSTed a v2 key (Mode B + F-A3-aware
301-
/// SDK). Empty by default → CBOR byte-equivalent to legacy v1.
302-
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
303-
pub users_v2: BTreeMap<String, String>,
304286
}
305287

306288
// ============================================================
@@ -360,46 +342,20 @@ pub fn build_user_buckets_index(
360342

361343
/// Build the global users-index CBOR from a per-user CID map.
362344
/// `entries` is `userKey_hex (32 hex) → bucketsIndexCid`.
363-
///
364-
/// Convenience for the v1-only path; equivalent to
365-
/// `build_global_users_index_v2(entries, &BTreeMap::new(), sequence, now_unix)`.
366345
pub fn build_global_users_index(
367346
entries: &BTreeMap<String, Cid>,
368347
sequence: u64,
369348
now_unix: u64,
370349
) -> GlobalUsersIndex {
371-
build_global_users_index_v2(entries, &BTreeMap::new(), sequence, now_unix)
372-
}
373-
374-
/// Build the global users-index CBOR including BOTH v1 and v2 lookup
375-
/// maps. `v2_entries` is `client_derived_v2_key_hex → bucketsIndexCid`.
376-
///
377-
/// **Audit F-A3 (issue #15)**: when `v2_entries` is empty, the
378-
/// resulting CBOR is byte-equivalent to the legacy v1 shape (the
379-
/// `users_v2` field is omitted via `skip_serializing_if`).
380-
pub fn build_global_users_index_v2(
381-
v1_entries: &BTreeMap<String, Cid>,
382-
v2_entries: &BTreeMap<String, Cid>,
383-
sequence: u64,
384-
now_unix: u64,
385-
) -> GlobalUsersIndex {
386-
let users: BTreeMap<String, String> = v1_entries
387-
.iter()
388-
.map(|(uk, cid)| (uk.clone(), cid.to_string()))
389-
.collect();
390-
let users_v2: BTreeMap<String, String> = v2_entries
350+
let users: BTreeMap<String, String> = entries
391351
.iter()
392352
.map(|(uk, cid)| (uk.clone(), cid.to_string()))
393353
.collect();
394354
GlobalUsersIndex {
395-
// Bumped to 2 to match the schema change. SDK clients that
396-
// know the v2 shape can short-circuit; old clients only read
397-
// the `users` field and continue to function.
398-
v: if users_v2.is_empty() { 1 } else { 2 },
355+
v: 1,
399356
sequence,
400357
updated_at_unix: now_unix,
401358
users,
402-
users_v2,
403359
}
404360
}
405361

crates/fula-client/src/encryption.rs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10854,7 +10854,6 @@ mod tests {
1085410854
sequence: 42,
1085510855
updated_at_unix: 1_700_000_001,
1085610856
users: users_map,
10857-
users_v2: std::collections::BTreeMap::new(),
1085810857
};
1085910858
let global_cbor = serde_ipld_dagcbor::to_vec(&global).expect("global");
1086010859

@@ -10934,7 +10933,6 @@ mod tests {
1093410933
sequence: 5,
1093510934
updated_at_unix: 1_700_000_000,
1093610935
users: users_map,
10937-
users_v2: std::collections::BTreeMap::new(),
1093810936
};
1093910937
let global_cbor = serde_ipld_dagcbor::to_vec(&global).expect("global");
1094010938

@@ -11017,7 +11015,6 @@ mod tests {
1101711015
sequence: 1,
1101811016
updated_at_unix: 0,
1101911017
users: users_map,
11020-
users_v2: std::collections::BTreeMap::new(),
1102111018
};
1102211019
let global_cbor = serde_ipld_dagcbor::to_vec(&global).expect("global");
1102311020

@@ -11104,7 +11101,6 @@ mod tests {
1110411101
sequence: 1,
1110511102
updated_at_unix: 0,
1110611103
users: users_map,
11107-
users_v2: std::collections::BTreeMap::new(),
1110811104
};
1110911105
let global_cbor = serde_ipld_dagcbor::to_vec(&global).expect("global");
1111011106

@@ -11191,7 +11187,6 @@ mod tests {
1119111187
sequence: 1,
1119211188
updated_at_unix: 0,
1119311189
users: users_map,
11194-
users_v2: std::collections::BTreeMap::new(),
1119511190
};
1119611191
let global_cbor = serde_ipld_dagcbor::to_vec(&global).expect("global");
1119711192

crates/fula-client/src/registry_resolver.rs

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -66,25 +66,14 @@ use std::time::Duration;
6666
/// `GlobalUsersIndex` struct in `fula-cli`'s
6767
/// `handlers::users_index_publisher`. The two definitions must stay
6868
/// in lockstep — see plan §3.2.a for the producer side.
69-
///
70-
/// **Audit F-A3 (issue #15)**: `users_v2` is the additive
71-
/// client-derived lookup-key map. SDK cold-start should prefer
72-
/// `users_v2.get(client_derived_v2_key)` and fall back to
73-
/// `users.get(hashed_user_id_v1)` on miss.
7469
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
7570
pub struct GlobalUsersIndex {
7671
pub v: u32,
7772
pub sequence: u64,
7873
pub updated_at_unix: u64,
79-
/// **v1, kept indefinitely.** `hashed_user_id_hex` (32 hex chars) →
80-
/// bucketsIndexCid (string). The SDK looks up its own
81-
/// `hashed_user_id_v1` here on cold-start when v2 is unavailable.
74+
/// `userKey_hex` (32 hex chars) → bucketsIndexCid (string).
75+
/// The SDK looks up its own `userKey` here on cold-start.
8276
pub users: BTreeMap<String, String>,
83-
/// **v2 client-derived lookup keys** (audit F-A3 / issue #15).
84-
/// `client_derived_lookup_key_hex` (32 hex chars) → bucketsIndexCid.
85-
/// Empty on pre-F-A3 deploys; SDK falls through to `users` on miss.
86-
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
87-
pub users_v2: BTreeMap<String, String>,
8877
}
8978

9079
/// Master's per-user `bucketsIndex` CBOR — one per user per snapshot
@@ -1636,7 +1625,6 @@ mod tests {
16361625
sequence,
16371626
updated_at_unix: 1_700_000_000,
16381627
users: BTreeMap::new(),
1639-
users_v2: BTreeMap::new(),
16401628
};
16411629
let bytes = serde_ipld_dagcbor::to_vec(&payload).expect("encode");
16421630
(Bytes::from(bytes), payload)
@@ -2245,7 +2233,6 @@ mod tests {
22452233
sequence,
22462234
updated_at_unix: 1_700_000_000,
22472235
users: BTreeMap::new(),
2248-
users_v2: BTreeMap::new(),
22492236
};
22502237
let bytes = serde_ipld_dagcbor::to_vec(&payload).expect("encode");
22512238
(Bytes::from(bytes), payload)

crates/fula-crypto/src/hashing.rs

Lines changed: 0 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -207,48 +207,6 @@ pub fn derive_key(context: &str, input: &[u8]) -> Blake3Hash {
207207
hasher.finalize()
208208
}
209209

210-
/// Compute the **v2 client-derived user-lookup key** for the
211-
/// global users-index (audit F-A3 / issue #15).
212-
///
213-
/// `userKey_v2 = BLAKE3("fula:user-lookup-v2:" || user_id || master_KEK_public)[..16]`
214-
///
215-
/// The key purpose is **lookup uniqueness** (mapping a user to their
216-
/// bucketsIndex CID in the publicly-resolvable IPNS-published global
217-
/// CBOR) without exposing the mapping to enumeration by anyone who
218-
/// only knows `user_id` (typically email or Google `sub`).
219-
///
220-
/// Unlike the legacy `hash_user_id(user_id) = BLAKE3("fula:user_id:" || user_id)[..16]`,
221-
/// which is server-derivable from a public attribute alone, this v2
222-
/// derivation requires `master_KEK_public` — a client-derived value
223-
/// that the server never sees. An attacker who can resolve the
224-
/// global CBOR can no longer hash a target email and check membership.
225-
///
226-
/// **Effectiveness is gated by audit F-A1 (Mode B sign-up)**: if the
227-
/// master KEK is still identity-derived (Mode A, no user-secret
228-
/// entropy), `master_KEK_public` is also identity-derivable, so an
229-
/// attacker who can compute master KEK can also compute `userKey_v2`.
230-
/// The privacy improvement materialises only when paired with Mode B.
231-
///
232-
/// Domain separation: distinct from `hash_user_id` (`"fula:user_id:"`)
233-
/// to prevent any cross-namespace collision.
234-
///
235-
/// # Arguments
236-
/// * `user_id` — raw user identifier (email / Google `sub` bytes)
237-
/// * `kek_pub` — 32-byte X25519 public component of the master KEK
238-
///
239-
/// # Returns
240-
/// * 16-byte (128-bit) lookup key
241-
pub fn compute_user_lookup_key_v2(user_id: &[u8], kek_pub: &[u8; 32]) -> [u8; 16] {
242-
let mut hasher = blake3::Hasher::new();
243-
hasher.update(b"fula:user-lookup-v2:");
244-
hasher.update(user_id);
245-
hasher.update(kek_pub);
246-
let h = hasher.finalize();
247-
let mut out = [0u8; 16];
248-
out.copy_from_slice(&h.as_bytes()[..16]);
249-
out
250-
}
251-
252210
/// Derive a key from the given input and context using Argon2id
253211
///
254212
/// This provides brute-force resistance for credential-based key derivation.

tests/issue_15_user_lookup_key_v2_test.rs

Lines changed: 0 additions & 124 deletions
This file was deleted.

0 commit comments

Comments
 (0)