Account roots
An account is named by its root key: the account id is the content
address of the root’s public key (H("calimero.account.genesis.v1", [2 ‖ pk])).
The root certifies devices, scopes them, signs owner-level governance and
proves the account to Calimero Cloud. Most apps never see it — the hosted
wallet holds it and certifies this device. This page is for an app that holds
the root itself: a nodeless account, or a root kept cold on the phone.
Everything here is offline and byte-identical with mero-js
account/account.ts.
Minting and restoring
Section titled “Minting and restoring”// A brand-new root, from the system CSPRNG, with its recovery phrase.let root = try generateAccountRoot()root.phrase // 24 BIP39 English words — show once, have the holder write it downroot.accountId // 64 hexroot.publicKey // 64 hex, safe to publishroot.secret // 64 hex — the whole account
// The same account from the words, on any device.let restored = try accountRootFromPhrase(root.phrase)assert(restored.accountId == root.accountId)
// The public half of a secret you already hold.let known = try accountRootFromSecret(secretHex)accountRootFromPhrase normalises case and whitespace, and throws on a wrong
word count, a word outside the list, or a failed checksum — it never returns
some other account. BIP39’s 8-bit checksum catches all but 1 in 256 mistyped
or transposed words.
A root that never leaves its signer
Section titled “A root that never leaves its signer”A secret held as a String can be logged, persisted or sent anywhere. Every
API that takes a root also takes a Signer — the public key plus
sign(_:) — so code holding it can sign without being able to read the key.
// Mint into the Keychain: the seed is saved, never returned.let vault = KeychainRootSeedVault(account: "account-root")let created = try createAccountRootSigner(vault: vault)created.phrase // the only backup: show it oncecreated.signer // a VaultRootSigner — reads the seed per signature
// After a relaunch.let root = try loadAccountRootSigner(from: vault) // AccountRootSigner?
// Restore on a new device (replaces what the vault held).let again = try accountRootSignerFromPhrase(words, vault: vault)KeychainRootSeedVault stores the 32-byte seed as a generic-password item with
kSecAttrAccessibleWhenUnlockedThisDeviceOnly, not synchronizable: readable
only while the device is unlocked, never in a backup or iCloud Keychain. (The
Secure Enclave holds P-256 keys only, so an Ed25519 root cannot live there.)
Without a vault the seed lives only inside the returned in-memory signer.
MemoryRootSeedVault is for tests. Conform to RootSeedVault for anything else.
RootSource names a root either way — .secret(hex) or .signer(signer) —
and resolveRoot(_:) returns its publicKey and accountId.
resolveRootPair(rootSecret:signer:) is the same for inputs with two optional
fields, and refuses both or neither.
Proving the root to Calimero Cloud
Section titled “Proving the root to Calimero Cloud”// Link a billing login to the account: GET /api/cloud/me/accounts/challenge.let proof = try await signAccountLink(AccountLinkInput(signer: root.signer, nonce: challenge))
// Claim the account and open a session as it: GET /api/auth/account/challenge.let login = try await signAccountLogin(AccountLoginInput(signer: root.signer, nonce: challenge))Each signs domain ‖ nonce (UTF-8) under mdma’s domains,
calimero.mdma.account-link.v1\0 and calimero.mdma.account-login.v1\0 —
separate so a link signature cannot be replayed as a login — and returns
standard base64, which is what mdma accepts.
Next: Signing owner ops uses the same root to authorise owner-level governance and to scope devices.