Skip to content

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() // RecoverableAccountRoot
root.accountId // 64 hex: the account
root.publicKey // 64 hex: safe to publish
root.phrase // 24 BIP39 English words: show once, store nowhere
root.secret // 64 hex: the whole account. Never send it or log it
val again = accountRootFromPhrase(root.phrase) // the same secret, key and account
val fromHex = accountRootFromSecret(root.secret) // the public half of a root you hold

accountRootFromPhrase 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() // NewAccountRootSigner
showOnce(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.

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 account
val linkProof = signAccountLink(AccountLinkInput(nonce, signer = root))
// GET /api/auth/account/challenge → claim the account and open a session as it
val 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.