Account roots
An account root is the Ed25519 key an account is named after: the account id is
the content address of its genesis (version ‖ root public key). The root certifies
devices, scopes them to applications, signs owner operations and proves the account
to Calimero Cloud. A device key can be revoked; the root cannot, because nothing sits
above it. Lose it and you lose the account. Leak it and someone else has it.
On a phone the root usually lives in the Calimero wallet, and the app never holds it
(see Cloud sign-in). This page is for the other case:
an app or tool that mints and keeps its own root. It is a port of mero-js
account/account.ts and produces the same bytes.
Mint a root, and restore it from its phrase
Section titled “Mint a root, and restore it from its phrase”import com.calimero.mero.account.*
val root = generateAccountRoot() // RecoverableAccountRootroot.accountId // 64 hex: the accountroot.publicKey // 64 hex: safe to publishroot.phrase // 24 BIP39 English words: show once, store nowhereroot.secret // 64 hex: the whole account. Never send it or log it
val again = accountRootFromPhrase(root.phrase) // the same secret, key and accountval fromHex = accountRootFromSecret(root.secret) // the public half of a root you holdaccountRootFromPhrase normalises case and whitespace. It throws
IllegalArgumentException when a word is not on the list, the count is not 24, or the
checksum fails. BIP39’s checksum catches a mistyped or transposed word in all but 1
case in 256, so the call fails before it can return some other account.
Keep a root on Android without exposing it
Section titled “Keep a root on Android without exposing it”AccountRoot.secret is a plain string. An app that keeps a root should use an
AccountRootSigner instead. It is a DeviceSigner, so every API that takes a root
signer accepts it. It has no secret property, and its toString prints only the
public key and the account.
val store = EncryptedPrefsSecureStore(context, "mero_account_root")val keys = AccountRootKeyStore(store) // key: "calimero.account-root"
val created = keys.create() // NewAccountRootSignershowOnce(created.phrase) // the only backup there will ever be
// Next launch:val root: AccountRootSigner = keys.load() ?: keys.restore(phraseTypedByTheUser)
// Anywhere a root signs:DeviceCert.sign(root, deviceId, signPublicKey, kemPublicKey)mero.admin.transferOwnership(groupId, newOwner, root)The seed lives only in the SecureStore. The signer reads it for each signature and
zeroes it straight after. keys.clear() forgets the root, and after that only the
phrase brings it back. createAccountRootSigner() and
accountRootSignerFromPhrase(phrase) default to a MemorySecureStore, which forgets
the root when the process dies. Pass a store to keep it.
Prove the account to Calimero Cloud
Section titled “Prove the account to Calimero Cloud”Two cloud exchanges need the root, because a device certificate is public and can never prove ownership:
// GET /api/cloud/me/accounts/challenge → link a billing login to the accountval linkProof = signAccountLink(AccountLinkInput(nonce, signer = root))
// GET /api/auth/account/challenge → claim the account and open a session as itval loginProof = signAccountLogin(AccountLoginInput(nonce, signer = root))Both sign domain ‖ nonce (UTF-8) under separate domains,
calimero.mdma.account-link.v1\0 and calimero.mdma.account-login.v1\0. Each
returns standard base64, because the cloud refuses base64url. Pass rootSecret
instead of signer if you hold the hex. Passing both, or neither, is refused.
resolveRoot(secretOrSigner) and resolveRootPair(rootSecret, signer) return the
signer, the public key and the account id, whichever form you were given.