Mnemonic Backup for Keyless Accounts (Developer Guide)
Overview
This page documents how developers can programmatically integrate Private Key Backups in their own wallets, taking advantage of Aptos account rotation and the MultiKey architecture.
Key Concepts
- Keyless Account: An account created with a social login (Google, Apple, etc.).
- Private Key Backup: A mnemonic seed phrase generated for a Keyless account.
- MultiKey account: An account with multiple signers; in this case, one Keyless signer and one private key signer.
Important Notes:
- Each Private Key Backup is generated once and is not stored by Petra.
- Users can generate a new private key if the old one is lost; old keys will no longer work. This can be done as many times as a user wants, but only the latest key will work.
SDK / Developer Integration
Understanding the Indexer Lookup
Because of how account rotation works on Aptos (opens in a new tab), when an account's authentication key is rotated (e.g., from a Keyless account to a MultiKey account to add a backup private key), its underlying account address never changes. This means you cannot mathematically derive the MultiKey account address purely from a given backup private key or Keyless account.
To solve this, the SDK's getAccountsForPublicKey method queries the Aptos Indexer. The indexer tracks all on-chain account rotation events. By querying the indexer, the SDK can automatically discover all on-chain accounts that have registered your specific public key as part of their current authentication key (whether as a single signer or as part of a MultiKey setup). This eliminates the need for users to manually input their original account address to recover their account.
Security Consideration: Verifying the MultiKey Account
When finding all accounts associated with a public key, we must protect against a malicious actor intentionally deriving a new, fake MultiKey using the user's public key alongside their own malicious public key.
For example, imagine a user rotates their Keyless account to a MultiKey using their Keyless signer (PK1) and a backup private key (PK2). A malicious actor could technically create a second, entirely different MultiKey account using PK2 and their own key (PK3). If the wallet accidentally derived the address for this malicious MultiKey instead, any funds the user deposits would go to an address the attacker controls (via PK3).
To solve this, we rely on a verification flag to essentially prove that the user has successfully interacted with the new MultiKey. When the user rotates their Keyless account to the new MultiKey, they must immediately use the newly formed MultiKey authentication key to sign a transaction for their original Keyless account address. Once this transaction is processed, the indexer flips a verification flag for the account. We can consider an account "discoverable" by PK2 if and only if this verification flag is true in the indexer. The malicious MultiKey created by the attacker would never have this flag flipped.
Deriving a MultiKey Account from a Private Key backup
Assumption: The account derivation logic assumes that the targeted account will always be a 1-of-2 MultiKey account (specifically 1 Keyless, 1 Ed25519) that was most recently interacted with and verified, or a rotated Ed25519 account. If multiple accounts are associated with the key, it will select the one with the latest transaction version.
import {
Aptos,
AptosConfig,
Network,
Ed25519Account,
MultiKey,
Ed25519PublicKey,
KeylessPublicKey,
BaseAccountPublicKey
} from '@aptos-labs/ts-sdk';
// 1. Initialize Aptos client and derive the private key from the mnemonic backup
const aptos = new Aptos(new AptosConfig({ network: Network.MAINNET }));
const path = "m/44'/637'/0'/0'/0'";
const signer = Ed25519Account.fromDerivationPath(path, mnemonic);
const publicKey = signer.publicKey;
// 2. Fetch ledger info for the minimum ledger version
const ledgerInfo = await aptos.getLedgerInfo();
// 3. Find all accounts associated with this public key on-chain
const accounts = await aptos.getAccountsForPublicKey({
minimumLedgerVersion: BigInt(ledgerInfo.ledger_version),
publicKey,
});
// Helper functions to identify Keyless MultiKey or rotated accounts
const isKeylessMultiKey = (pubKey: BaseAccountPublicKey) =>
pubKey instanceof MultiKey &&
pubKey.signaturesRequired === 1 &&
pubKey.publicKeys.length === 2 &&
pubKey.publicKeys.some((key) => key.publicKey instanceof KeylessPublicKey) &&
pubKey.publicKeys.some((key) => key.publicKey instanceof Ed25519PublicKey);
const isRotatedEd25519PublicKey = (pubKey: BaseAccountPublicKey) =>
pubKey instanceof Ed25519PublicKey;
// 4. Find the most recently interacted with Keyless or rotated account
let accountInfo;
if (accounts.length === 0) {
// Fallback: Check if there's an original account address (legacy lookup)
const originalAddress = await aptos.lookupOriginalAccountAddress({
authenticationKey: signer.accountAddress,
});
if (originalAddress) {
console.log("Account found at:", originalAddress.toString());
}
} else {
// Filter for Keyless MultiKey or previously rotated Ed25519 accounts
// and sort by the latest transaction version
[accountInfo] = accounts
.filter(
(account) =>
isKeylessMultiKey(account.publicKey) ||
isRotatedEd25519PublicKey(account.publicKey),
)
.sort(
(a, b) =>
Number(b.lastTransactionVersion) - Number(a.lastTransactionVersion),
);
}
// 5. Build the derived account based on the matched account info
let derivedAccount;
if (accountInfo?.publicKey instanceof MultiKey) {
derivedAccount = {
address: accountInfo.accountAddress.toString(),
publicKey: accountInfo.publicKey.toString(),
signaturesRequired: accountInfo.publicKey.signaturesRequired,
signers: [signer],
type: 'MultiKey',
};
} else if (accountInfo?.publicKey instanceof Ed25519PublicKey) {
derivedAccount = {
...signer,
address: accountInfo.accountAddress.toString(),
publicKey: accountInfo.publicKey.toString(),
};
} else {
derivedAccount = signer;
}
console.log("Derived Account Address:", derivedAccount.address);Optional: Selectively Picking an Account by Address
As mentioned above, if you already know the address of the specific account you want to recover or target, you can filter the on-chain accounts directly by address instead of finding the most recently interacted account.
import {
Aptos,
AptosConfig,
Network,
Ed25519Account,
MultiKey,
Ed25519PublicKey,
} from '@aptos-labs/ts-sdk';
import { normalizeAddress } from '@aptos-labs/js-pro';
const knownAddress = "0xYourKnownAccountAddress";
const mnemonic = "your mnemonic here...";
// 1. Initialize Aptos client and create a signer from the backup mnemonic
const aptos = new Aptos(new AptosConfig({ network: Network.MAINNET }));
const path = "m/44'/637'/0'/0'/0'";
const signer = Ed25519Account.fromDerivationPath(path, mnemonic);
const publicKey = signer.publicKey;
// 2. Fetch all on-chain accounts associated with this public key
const ledgerInfo = await aptos.getLedgerInfo();
const accounts = await aptos.getAccountsForPublicKey({
minimumLedgerVersion: BigInt(ledgerInfo.ledger_version),
publicKey,
});
// 3. Directly filter the accounts for the one that matches the known address
const accountInfo = accounts.find(
(account) =>
normalizeAddress(account.accountAddress) ===
normalizeAddress(knownAddress)
);
if (!accountInfo) {
throw new Error(`Account with address ${knownAddress} not found for this private key.`);
}
// 4. Build the derived account based on the matched account's public key type
let derivedAccount;
if (accountInfo.publicKey instanceof MultiKey) {
derivedAccount = {
address: accountInfo.accountAddress.toString(),
publicKey: accountInfo.publicKey.toString(),
signaturesRequired: accountInfo.publicKey.signaturesRequired,
signers: [signer],
type: 'MultiKey',
};
} else if (accountInfo.publicKey instanceof Ed25519PublicKey) {
derivedAccount = {
...signer,
address: accountInfo.accountAddress.toString(),
publicKey: accountInfo.publicKey.toString(),
};
} else {
derivedAccount = signer;
}
console.log("Recovered Account Address:", derivedAccount.address);Generating a Private Key Backup and Rotating a Keyless Account into a MultiKey Account
import {
Account,
AnyPublicKey,
MultiKey,
RotationProofChallenge,
} from '@aptos-labs/ts-sdk';
// 1. We assume you have a newly generated Ed25519 account or mnemonic.
// Here we instantiate an Account from a mnemonic for the backup key.
const ed25519Account = Account.fromDerivationPath({
mnemonic: "your backup recovery phrase here ...",
path: "m/44'/637'/0'/0'/0'",
});
// 2. Fetch the current sequence number of the active Keyless account.
const accountInfo = await aptos.account.getAccountInfo({
accountAddress: activeSigner.accountAddress,
});
// 3. Create the rotation proof challenge to prove possession of the backup key.
const challenge = new RotationProofChallenge({
currentAuthKey: activeSigner.publicKey.authKey().derivedAddress(),
newPublicKey: ed25519Account.publicKey,
originator: activeSigner.accountAddress,
sequenceNumber: Number(accountInfo.sequence_number),
});
// 4. Identify the active Keyless public key. If the account is already a
// MultiKey account, it's typically the first key. Otherwise, wrap the single key.
const keylessSingleKey =
activeSigner.publicKey instanceof MultiKey
? activeSigner.publicKey.publicKeys[0]
: new AnyPublicKey(activeSigner.publicKey);
// 5. Construct the transaction to upsert the Ed25519 backup key.
const transaction = await aptos.transaction.build.simple({
sender: activeSigner.accountAddress,
data: {
function: '0x1::account::upsert_ed25519_backup_key_on_keyless_account',
functionArguments: [
keylessSingleKey.toUint8Array(),
ed25519Account.publicKey.toUint8Array(),
ed25519Account.sign(challenge.bcsToBytes()).toUint8Array(),
],
},
});
// 6. Sign and submit the transaction with the active Keyless signer.
const response = await aptos.signAndSubmitTransaction({
signer: activeSigner,
transaction,
});
await aptos.waitForTransaction({ transactionHash: response.hash });Design Guidelines
Rotation Flow
- Recovery phrase box supports three states (no key, key in memory, key lost).
- The rotation transaction is only fired after explicit user confirmation.
Private Key Backup Import Flow
- The Private Key Backup import flow is similar to the original Private Key import flow of the app. The derivations steps stated above should be used. No UI changes should be necessary.