17 KiB
SSSS Wallet Backup Implementation Plan
Executive Summary
This document details the implementation plan for backing up the Cardano wallet mnemonic to Matrix account data using SSSS (Secure Secret Storage). After analyzing the codebase, I've identified two viable approaches with different complexity/integration tradeoffs.
Recommendation: Option A (Expose SecretStore in FFI) - Proper Matrix SSSS integration that works with the user's existing recovery key.
What Already Exists
Rust SDK (matrix-rust-sdk)
| Component | Location | Status |
|---|---|---|
SecretStore |
crates/matrix-sdk/src/encryption/secret_storage/secret_store.rs |
✅ Full implementation |
put_secret() / get_secret() |
Same file | ✅ Implemented |
open_secret_store(passphrase) |
crates/matrix-sdk/src/encryption/secret_storage/mod.rs |
✅ Implemented |
account_data() / set_account_data() |
bindings/matrix-sdk-ffi/src/client.rs:1385-1400 |
✅ Exposed in FFI |
| SecretStore FFI bindings | - | ❌ NOT exposed |
Kotlin SDK (element-x-android)
| Component | Location | Status |
|---|---|---|
RustMatrixClient.innerClient |
libraries/matrix/impl/.../RustMatrixClient.kt |
✅ Has Client reference |
| Account data wrapper | - | ❌ Not wrapped (but FFI methods available) |
EncryptionService.recover(recoveryKey) |
libraries/matrix/api/.../EncryptionService.kt |
✅ Opens SSSS with recovery key |
| SecretStore wrapper | - | ❌ Does not exist |
Wallet Feature (element-x-android)
| Component | Location | Status |
|---|---|---|
CardanoKeyStorage interface |
features/wallet/api/.../storage/CardanoKeyStorage.kt |
✅ Defined |
CardanoKeyStorageImpl |
features/wallet/impl/.../storage/CardanoKeyStorageImpl.kt |
✅ Android Keystore encryption |
WalletSetupPresenter |
features/wallet/impl/.../setup/WalletSetupPresenter.kt |
✅ Has TODO for SSSS backup |
SettingsTabView |
features/wallet/impl/.../panel/tabs/SettingsTabView.kt |
✅ Has "Export phrase" button |
| Backup/restore UI | - | ❌ Not implemented |
Implementation Options
Option A: Expose SecretStore in FFI (RECOMMENDED)
This option properly integrates with Matrix's SSSS infrastructure. The wallet mnemonic is encrypted using the same recovery key that protects cross-signing keys.
Pros:
- Follows Matrix spec for secret storage
- Integrates with existing recovery key flow
- User only needs to remember ONE recovery key for everything
- Proper encryption using battle-tested Matrix crypto
Cons:
- Requires Rust SDK changes
- ~2-3 days additional work
Option B: Raw account_data with Custom Encryption
Use the already-exposed account_data() / set_account_data() with our own encryption layer (e.g., PBKDF2 + AES-GCM from passphrase).
Pros:
- No SDK changes needed
- Can ship faster
Cons:
- Doesn't integrate with Matrix recovery key
- User needs to remember separate passphrase
- Rolling our own crypto (risky)
- Not standard Matrix behavior
Detailed Implementation Plan (Option A)
Phase 1: Rust SDK Changes
1.1 Add SecretStore to FFI (bindings/matrix-sdk-ffi/src/client.rs)
// Add to imports
use matrix_sdk::encryption::secret_storage::SecretStore;
// Add to Client impl
#[matrix_sdk_ffi_macros::export]
impl Client {
/// Open the secret store with a recovery key or passphrase.
/// Returns None if the recovery key is invalid.
pub async fn open_secret_store(
&self,
recovery_key: String,
) -> Result<Option<Arc<SecretStoreWrapper>>, ClientError> {
match self.inner.encryption().secret_storage().open_secret_store(&recovery_key).await {
Ok(store) => Ok(Some(Arc::new(SecretStoreWrapper { inner: store }))),
Err(e) => {
// Key validation failed
tracing::warn!("Failed to open secret store: {e}");
Ok(None)
}
}
}
}
1.2 Create SecretStoreWrapper (bindings/matrix-sdk-ffi/src/secret_storage.rs)
Create new file:
use std::sync::Arc;
use matrix_sdk::encryption::secret_storage::SecretStore;
use crate::error::ClientError;
#[derive(uniffi::Object)]
pub struct SecretStoreWrapper {
pub(crate) inner: SecretStore,
}
#[matrix_sdk_ffi_macros::export]
impl SecretStoreWrapper {
/// Store a secret in SSSS.
pub async fn put_secret(
&self,
secret_name: String,
secret: String,
) -> Result<(), ClientError> {
self.inner.put_secret(secret_name.into(), &secret).await?;
Ok(())
}
/// Retrieve a secret from SSSS.
pub async fn get_secret(
&self,
secret_name: String,
) -> Result<Option<String>, ClientError> {
Ok(self.inner.get_secret(secret_name.into()).await?)
}
}
1.3 Update lib.rs
mod secret_storage;
pub use secret_storage::SecretStoreWrapper;
Files to Modify in Rust SDK:
bindings/matrix-sdk-ffi/src/client.rs- Addopen_secret_store()methodbindings/matrix-sdk-ffi/src/secret_storage.rs- NEW FILEbindings/matrix-sdk-ffi/src/lib.rs- Add module exportbindings/matrix-sdk-ffi/src/api.udl- May need updates (check uniffi requirements)
Phase 2: Kotlin SDK Changes
2.1 Add SecretStorage interface (libraries/matrix/api/.../secretstorage/SecretStorage.kt)
package io.element.android.libraries.matrix.api.secretstorage
interface SecretStorage {
/**
* Open the secret store with a recovery key.
* @param recoveryKey The Matrix recovery key (base58 encoded)
* @return SecretStore instance if key is valid, null otherwise
*/
suspend fun openSecretStore(recoveryKey: String): SecretStore?
}
interface SecretStore {
/**
* Store a secret encrypted with SSSS.
* @param secretName The secret identifier (e.g., "com.sulkta.cardano.wallet_seed")
* @param secret The secret value to store
*/
suspend fun putSecret(secretName: String, secret: String): Result<Unit>
/**
* Retrieve a secret from SSSS.
* @param secretName The secret identifier
* @return The decrypted secret, or null if not found
*/
suspend fun getSecret(secretName: String): Result<String?>
}
2.2 Add RustSecretStorage implementation (libraries/matrix/impl/.../secretstorage/RustSecretStorage.kt)
package io.element.android.libraries.matrix.impl.secretstorage
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.matrix.api.secretstorage.SecretStorage
import io.element.android.libraries.matrix.api.secretstorage.SecretStore
import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.Client
import org.matrix.rustcomponents.sdk.SecretStoreWrapper
class RustSecretStorage(
private val client: Client,
private val dispatchers: CoroutineDispatchers,
) : SecretStorage {
override suspend fun openSecretStore(recoveryKey: String): SecretStore? =
withContext(dispatchers.io) {
client.openSecretStore(recoveryKey)?.let { RustSecretStore(it, dispatchers) }
}
}
class RustSecretStore(
private val inner: SecretStoreWrapper,
private val dispatchers: CoroutineDispatchers,
) : SecretStore {
override suspend fun putSecret(secretName: String, secret: String): Result<Unit> =
withContext(dispatchers.io) {
runCatchingExceptions { inner.putSecret(secretName, secret) }
}
override suspend fun getSecret(secretName: String): Result<String?> =
withContext(dispatchers.io) {
runCatchingExceptions { inner.getSecret(secretName) }
}
}
2.3 Expose via MatrixClient
Add to MatrixClient interface:
val secretStorage: SecretStorage
Add to RustMatrixClient:
override val secretStorage: SecretStorage = RustSecretStorage(innerClient, dispatchers)
Files to Modify in Kotlin:
libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/secretstorage/SecretStorage.kt- NEWlibraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/secretstorage/SecretStore.kt- NEWlibraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/secretstorage/RustSecretStorage.kt- NEWlibraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt- Add secretStorage propertylibraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt- Implement secretStorage
Phase 3: Wallet Backup Feature
3.1 Add WalletBackupService (features/wallet/api/.../backup/WalletBackupService.kt)
package io.element.android.features.wallet.api.backup
import io.element.android.libraries.matrix.api.core.SessionId
/**
* Service for backing up wallet seed to Matrix SSSS.
*/
interface WalletBackupService {
/**
* Check if a wallet backup exists in SSSS.
*/
suspend fun hasBackup(sessionId: SessionId): Result<Boolean>
/**
* Backup the wallet seed to SSSS.
* @param sessionId The Matrix session
* @param recoveryKey The Matrix recovery key to encrypt with
*/
suspend fun backupWallet(sessionId: SessionId, recoveryKey: String): Result<Unit>
/**
* Restore wallet from SSSS backup.
* @param sessionId The Matrix session
* @param recoveryKey The Matrix recovery key to decrypt with
* @return The mnemonic words if found, null otherwise
*/
suspend fun restoreWallet(sessionId: SessionId, recoveryKey: String): Result<List<String>?>
}
3.2 Implement WalletBackupServiceImpl (features/wallet/impl/.../backup/WalletBackupServiceImpl.kt)
package io.element.android.features.wallet.impl.backup
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.Inject
import io.element.android.features.wallet.api.backup.WalletBackupService
import io.element.android.features.wallet.api.storage.CardanoKeyStorage
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.SessionId
@ContributesBinding(AppScope::class)
class WalletBackupServiceImpl @Inject constructor(
private val keyStorage: CardanoKeyStorage,
private val matrixClient: MatrixClient,
) : WalletBackupService {
companion object {
const val SECRET_NAME = "com.sulkta.cardano.wallet_seed"
}
override suspend fun hasBackup(sessionId: SessionId): Result<Boolean> = runCatching {
// We can't check without the recovery key, so this checks account data existence
// For now, return false - proper implementation needs recovery key
false
}
override suspend fun backupWallet(sessionId: SessionId, recoveryKey: String): Result<Unit> = runCatching {
val secretStore = matrixClient.secretStorage.openSecretStore(recoveryKey)
?: throw IllegalArgumentException("Invalid recovery key")
val mnemonic = keyStorage.getMnemonic(sessionId).getOrThrow()
val mnemonicString = mnemonic.joinToString(" ")
secretStore.putSecret(SECRET_NAME, mnemonicString).getOrThrow()
}
override suspend fun restoreWallet(sessionId: SessionId, recoveryKey: String): Result<List<String>?> = runCatching {
val secretStore = matrixClient.secretStorage.openSecretStore(recoveryKey)
?: throw IllegalArgumentException("Invalid recovery key")
secretStore.getSecret(SECRET_NAME).getOrNull()?.split(" ")
}
}
3.3 Update WalletSetupPresenter
Add backup prompt after wallet creation:
// In handleEvent, after ConfirmBackup:
WalletSetupEvent.BackupToCloud -> {
step = SetupStep.BACKUP_TO_CLOUD
}
is WalletSetupEvent.ConfirmCloudBackup -> {
scope.launch {
walletBackupService.backupWallet(sessionId, event.recoveryKey)
.onSuccess {
Timber.i("Wallet backed up to SSSS")
step = SetupStep.COMPLETE
}
.onFailure { e ->
error = "Backup failed: ${e.message}"
}
}
}
3.4 Add Restore Flow on Login
In the session initialization flow, check for existing backup:
// When initializing wallet for a session
suspend fun initializeWallet(sessionId: SessionId, recoveryKey: String?) {
if (keyStorage.hasWallet(sessionId)) {
// Already have local wallet
return
}
if (recoveryKey != null) {
val restoredMnemonic = walletBackupService.restoreWallet(sessionId, recoveryKey).getOrNull()
if (restoredMnemonic != null) {
keyStorage.importWallet(sessionId, restoredMnemonic)
return
}
}
// No backup found - user will need to create new wallet
}
Files to Create/Modify:
features/wallet/api/src/main/kotlin/.../backup/WalletBackupService.kt- NEWfeatures/wallet/impl/src/main/kotlin/.../backup/WalletBackupServiceImpl.kt- NEWfeatures/wallet/impl/src/main/kotlin/.../setup/WalletSetupPresenter.kt- Add backup flowfeatures/wallet/impl/src/main/kotlin/.../setup/WalletSetupState.kt- Add backup statesfeatures/wallet/impl/src/main/kotlin/.../setup/WalletSetupView.kt- Add backup UIfeatures/wallet/impl/src/main/kotlin/.../panel/tabs/SettingsTabView.kt- Add "Backup to Cloud" option
UX Flow
Wallet Creation Flow (Updated)
1. Welcome → Create New Wallet
2. Generating... (create mnemonic, store locally)
3. Show Address
4. Backup Prompt: "Write down your recovery phrase"
5. ✨ NEW: "Backup to Matrix" option
- If user has recovery key set up → prompt to enter it
- If not → skip, just local backup
6. Complete
Wallet Restore Flow (New)
1. Login to Matrix account
2. Sync completes
3. Check if wallet exists in SSSS
4. If yes: "Wallet backup found. Enter recovery key to restore"
5. Enter recovery key
6. Wallet restored!
Settings Flow (Updated)
Wallet Settings:
- Address (copy)
- Network (Mainnet/Testnet)
- Export Recovery Phrase
- ✨ NEW: Backup to Matrix (if not already backed up)
- Delete Wallet
Encryption Approach
Using Matrix SSSS (Option A):
- User's Matrix recovery key (48-character base58) derives an AES-256-GCM key
- Secrets are stored in account data events like
com.sulkta.cardano.wallet_seed - Content is encrypted per Matrix SSSS spec (m.secret_storage.v1.aes-hmac-sha2)
- Same key protects cross-signing keys and message backup key
Benefits:
- Battle-tested Matrix crypto
- User only needs ONE recovery key
- Works across all Matrix clients (if they implement it)
- Follows Matrix spec exactly
Estimated Complexity
| Phase | Effort | Risk |
|---|---|---|
| Phase 1: Rust SDK FFI changes | 1-2 days | Low (straightforward wrapping) |
| Phase 2: Kotlin SDK wrappers | 0.5-1 day | Low |
| Phase 3: Wallet backup feature | 2-3 days | Medium (UI/UX decisions) |
| Testing & polish | 1-2 days | - |
| Total | 5-8 days | Medium |
Comparison to sendRaw() Phase 2 work:
- Similar complexity to the FFI part
- More UI work for the backup/restore flows
- Overall: slightly harder due to UX considerations
Blockers & Gotchas
Potential Issues
-
Recovery Key UX: User may not have set up Matrix recovery key yet. Need graceful handling.
-
Passphrase vs Recovery Key: Some users set up SSSS with a passphrase, others with a generated key. The
open_secret_store()method accepts both, but UX needs to handle this. -
Key Format Validation: Recovery keys are base58 with specific format. Need proper validation before calling SDK.
-
Sync Timing: SSSS operations require sync to be running. Need to handle offline scenarios.
-
Error Messages: Matrix SDK errors are technical. Need user-friendly error translation.
-
Testing: Need test Matrix account with SSSS set up. Can use Element Web to create recovery key.
Not Blockers (Already Handled)
- ✅ Android Keystore integration exists
- ✅ Mnemonic generation/validation works
- ✅ Settings UI structure exists
- ✅ FFI pattern established (see sendRaw())
Testing Plan
-
Unit Tests
- SecretStoreWrapper (Rust)
- RustSecretStorage (Kotlin)
- WalletBackupServiceImpl (Kotlin)
-
Integration Tests
- Create wallet → Backup → Clear local → Restore
- Invalid recovery key handling
- Network failure during backup
-
Manual Tests
- Full flow on device with real Matrix account
- Test with both passphrase and generated recovery key
- Test restore on new device
Summary
The SSSS wallet backup feature requires:
- 3 new Rust files (or 1 file + 2 modifications)
- 4-5 new Kotlin files
- 3-4 modified Kotlin files
- ~5-8 days of work
The key insight is that the Rust SDK already has full SSSS support - we just need to expose it through the FFI layer. The Kotlin side is straightforward wrapping, and the wallet feature work is mostly UI/UX.
Recommendation: Proceed with Option A for proper Matrix integration. Users will thank us for not making them remember another password.