Skip to main content

offline_protocol/
storage_namespace.rs

1//! The account namespace: which directory, or which origin-private folder,
2//! holds one protocol account's records.
3//!
4//! Every store that keeps an account apart from another derives its location
5//! here, so a directory written by one binding opens under another for the
6//! same account. Outside the `file-store` feature because the browser's store
7//! uses it too, and a browser build has no POSIX files.
8
9use sha2::{Digest, Sha256};
10
11/// Domain separator of the account namespace digest. Shared with the Python,
12/// Swift and Kotlin `StorageNamespace` helpers; changing it strands every
13/// existing store.
14const NAMESPACE_DOMAIN: &[u8] = b"offline-protocol-storage-v1";
15
16/// Prefix of every account namespace.
17const NAMESPACE_PREFIX: &str = "account-";
18
19/// Returns the opaque, filesystem-safe namespace for one protocol account.
20///
21/// `account-` followed by the lowercase hex SHA-256 of
22/// `"offline-protocol-storage-v1" || 0x00 || app_id || 0x00 || profile`, the
23/// derivation every binding uses, so a directory written by one binding opens
24/// under another for the same account.
25pub fn account_storage_namespace(app_id: &str, profile: &str) -> String {
26    let mut sha = Sha256::new();
27    sha.update(NAMESPACE_DOMAIN);
28    sha.update([0u8]);
29    sha.update(app_id.as_bytes());
30    sha.update([0u8]);
31    sha.update(profile.as_bytes());
32    format!("{NAMESPACE_PREFIX}{}", hex::encode(sha.finalize()))
33}
34
35/// Whether `value` is a well-formed account namespace.
36///
37/// The namespace becomes a path component, so an arbitrary string could
38/// escape its own account (`..`) or collide with another's. Only the exact
39/// shape [`account_storage_namespace`] produces is accepted: `account-` and
40/// 64 lowercase hex digits.
41pub fn is_account_storage_namespace(value: &str) -> bool {
42    value.strip_prefix(NAMESPACE_PREFIX).is_some_and(|digest| {
43        digest.len() == 64
44            && digest
45                .bytes()
46                .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
47    })
48}