From 7215ebb5093effd6dc446321f875de148d40f4a6 Mon Sep 17 00:00:00 2001 From: Kayos Date: Thu, 26 Mar 2026 19:56:08 -0700 Subject: [PATCH] Add Matrix SSSS technical review for Cardano wallet storage Deep technical analysis of Matrix Secret Storage for storing Cardano wallet seeds encrypted in user account data. Covers: - Complete SSSS technical walkthrough - matrix-rust-sdk API surface with code examples - Step-by-step storage and restore flows - Security analysis and recommendations - Spec gaps and limitations --- MATRIX-KEY-STORAGE-REVIEW.md | 824 +++++++++++++++++++++++++++++++++++ 1 file changed, 824 insertions(+) create mode 100644 MATRIX-KEY-STORAGE-REVIEW.md diff --git a/MATRIX-KEY-STORAGE-REVIEW.md b/MATRIX-KEY-STORAGE-REVIEW.md new file mode 100644 index 0000000000..bc2faef967 --- /dev/null +++ b/MATRIX-KEY-STORAGE-REVIEW.md @@ -0,0 +1,824 @@ +# Matrix Secret Storage (SSSS) Technical Review + +**Purpose**: Evaluate Matrix's Secret Storage and Sharing Service (SSSS) for storing a Cardano wallet seed encrypted in a user's Matrix account data, enabling automatic restore on new device verification. + +**Goal**: Matrix account = wallet backup. No seed phrase entry, no separate backup. + +--- + +## Table of Contents +1. [SSSS Technical Overview](#1-ssss-technical-overview) +2. [Can You Store Custom Secrets?](#2-can-you-store-custom-secrets) +3. [Account Data Event Format](#3-account-data-event-format) +4. [Secret Sharing on New Device Verification](#4-secret-sharing-on-new-device-verification) +5. [matrix-rust-sdk API Surface](#5-matrix-rust-sdk-api-surface) +6. [Offline Device Recovery](#6-offline-device-recovery) +7. [Encryption Details](#7-encryption-details) +8. [Security Analysis](#8-security-analysis) +9. [Precedents for Non-Matrix Secrets](#9-precedents-for-non-matrix-secrets) +10. [Wallet Storage Flow](#10-wallet-storage-flow) +11. [Wallet Restore Flow](#11-wallet-restore-flow) +12. [Spec Gaps and Limitations](#12-spec-gaps-and-limitations) +13. [Recommendations](#13-recommendations) + +--- + +## 1. SSSS Technical Overview + +Matrix Secret Storage (SSSS) is an encrypted key/value store backed by [account data events](https://spec.matrix.org/latest/client-server-api/#client-config) on the homeserver. + +### Step-by-Step: How SSSS Works + +#### 1.1 Secret Storage Key Generation + +Two methods to create a Secret Storage Key: + +**Method A: Random Key** +1. Generate 32 random bytes as the secret key material +2. Generate a random key ID (32 alphanumeric characters) +3. Derive a verification key using HKDF-SHA-256: + - Input: 32-byte secret key + - Salt: 32 zero bytes + - Info: empty string (`""`) + - Output: 64 bytes → split into AES key (32 bytes) + MAC key (32 bytes) +4. Encrypt 32 zero bytes with derived key, store IV + MAC in key metadata + +**Method B: Passphrase-Based** +1. Generate random 32-character salt +2. Run PBKDF2-HMAC-SHA512: + - Input: passphrase + - Salt: generated salt + - Iterations: 500,000 (default) + - Output: 32 bytes +3. Continue as Method A, but store passphrase info in key metadata + +#### 1.2 Key Storage on Server + +The Secret Storage Key metadata is uploaded as a global account data event: + +``` +Event Type: m.secret_storage.key. +``` + +Content structure: +```json +{ + "algorithm": "m.secret_storage.v1.aes-hmac-sha2", + "iv": "", // 16 bytes, for MAC verification + "mac": "", // 32 bytes, for key verification + "passphrase": { // Optional, only if passphrase-derived + "algorithm": "m.pbkdf2", + "salt": "", + "iterations": 500000 + } +} +``` + +#### 1.3 Default Key Designation + +A separate event marks which key is default: + +``` +Event Type: m.secret_storage.default_key +``` + +Content: +```json +{ + "key": "" +} +``` + +#### 1.4 Encrypting a Secret + +For each secret with name ``: + +1. Derive per-secret encryption keys using HKDF-SHA-256: + - Input: 32-byte storage key + - Salt: 32 zero bytes + - Info: secret name as UTF-8 bytes (e.g., `"m.cross_signing.master"`) + - Output: 64 bytes → AES key (first 32) + MAC key (last 32) + +2. Generate random 16-byte IV (with bit 63 cleared for implementation compatibility) + +3. Encrypt secret using AES-256-CTR with derived AES key + +4. Compute HMAC-SHA-256 over ciphertext using derived MAC key + +5. Store as account data event with type = secret name + +#### 1.5 Decrypting a Secret + +1. Fetch account data event for secret name +2. Look up encrypted data for your key ID +3. Derive per-secret keys using HKDF (same as encryption) +4. Verify MAC before decryption +5. Decrypt ciphertext using AES-256-CTR + +--- + +## 2. Can You Store Custom Secrets? + +**Yes, absolutely.** SSSS is designed for arbitrary secrets. + +The `SecretName` type in Ruma/matrix-rust-sdk accepts any string: + +```rust +let my_secret_name = SecretName::from("com.sulkta.cardano.wallet_seed"); +``` + +Convention for custom secrets: +- Use reverse-domain notation: `com.yourorg.secret_type` +- The secret name becomes the account data event type +- No registration required—just use it + +**Well-known secrets defined by Matrix:** +- `m.cross_signing.master` +- `m.cross_signing.self_signing` +- `m.cross_signing.user_signing` +- `m.megolm_backup.v1` (backup recovery key) + +**Your custom secret would be:** +- `com.sulkta.cardano.wallet_seed` or similar + +--- + +## 3. Account Data Event Format + +### 3.1 Secret Event Type + +Each secret is stored at: +``` +Event Type: +``` + +### 3.2 Secret Event Content + +```json +{ + "encrypted": { + "": { + "iv": "", // 16 bytes + "ciphertext": "", // encrypted secret + "mac": "" // 32 bytes HMAC + } + } +} +``` + +Example for a Cardano wallet seed: +```json +{ + "type": "com.sulkta.cardano.wallet_seed", + "content": { + "encrypted": { + "abc123": { + "iv": "gH2iNpiETFhApvW6/FFEJQ==", + "ciphertext": "lCRSSA1lChONEXj/8RyogsgAa8ouQwYDnLr4XBCheRikrZykLRzPCx3doCE=", + "mac": "NXeV1dZaOe2JLvQ6Hh6tFto7AgFFdaQnY0l9pruwdtE=" + } + } + } +} +``` + +### 3.3 Multiple Key Support + +The `encrypted` map can contain entries for multiple storage keys. This allows: +- Key rotation without re-fetching secrets +- Multiple authorized keys (though uncommon) + +--- + +## 4. Secret Sharing on New Device Verification + +### 4.1 The Secret Sharing Protocol + +When a new device verifies via cross-signing, it can request secrets from existing devices: + +**Step 1: New device sends `m.secret.request`** +```json +{ + "type": "m.secret.request", + "content": { + "action": "request", + "name": "com.sulkta.cardano.wallet_seed", + "requesting_device_id": "NEWDEVICE", + "request_id": "unique-request-id" + } +} +``` +- Sent as **unencrypted** to-device event to all user's devices + +**Step 2: Existing device sends `m.secret.send`** +```json +{ + "type": "m.secret.send", + "content": { + "request_id": "unique-request-id", + "secret": "" + } +} +``` +- Sent as **Olm-encrypted** to-device event +- Only sent to verified devices (cross-signed by owner) + +### 4.2 Verification Requirements + +Before sharing secrets, the existing device MUST verify: +1. The requesting device is owned by the same user +2. The requesting device is **verified** (cross-signed) +3. The Olm session was established with the expected device + +### 4.3 Limitations + +**Critical for your use case:** +- Secret sharing requires **at least one other device to be online** +- If no other devices are online, the new device must: + 1. Enter the recovery key/passphrase manually + 2. Decrypt secrets from account data directly + +--- + +## 5. matrix-rust-sdk API Surface + +### 5.1 Opening the Secret Store + +```rust +use matrix_sdk::Client; +use ruma::events::secret::request::SecretName; + +// Open with recovery key (base58) or passphrase +let secret_store = client + .encryption() + .secret_storage() + .open_secret_store("EsTj 3yST y93F SLpB...") // or passphrase + .await?; +``` + +### 5.2 Storing a Secret + +```rust +let wallet_seed = "your 24 word mnemonic here"; +let secret_name = SecretName::from("com.sulkta.cardano.wallet_seed"); + +secret_store.put_secret(secret_name, wallet_seed).await?; +``` + +### 5.3 Retrieving a Secret + +```rust +let secret_name = SecretName::from("com.sulkta.cardano.wallet_seed"); + +let wallet_seed = secret_store.get_secret(secret_name).await?; +match wallet_seed { + Some(seed) => println!("Restored wallet seed!"), + None => println!("No wallet seed found in SSSS"), +} +``` + +### 5.4 Creating a New Secret Store + +```rust +// Creates new storage key, uploads to server, re-encrypts existing secrets +let secret_store = client + .encryption() + .secret_storage() + .create_secret_store() + .await?; + +// Optionally with passphrase +let secret_store = client + .encryption() + .secret_storage() + .create_secret_store() + .with_passphrase("user's chosen passphrase") + .await?; + +// Get the recovery key for backup +let recovery_key = secret_store.secret_storage_key(); +println!("Save this recovery key: {}", recovery_key); +``` + +### 5.5 Recovery on New Device + +```rust +// User enters recovery key or passphrase +let recovery = client.encryption().recovery(); +recovery.recover("EsTj 3yST y93F SLpB...").await?; + +// Now secret store is available +let secret_store = client + .encryption() + .secret_storage() + .open_secret_store("EsTj 3yST y93F SLpB...") + .await?; + +// Retrieve wallet seed +let wallet_seed = secret_store + .get_secret(SecretName::from("com.sulkta.cardano.wallet_seed")) + .await?; +``` + +### 5.6 Importing All Secrets (Built-in Recovery Flow) + +```rust +// This imports cross-signing keys + backup key, verifies device +secret_store.import_secrets().await?; + +// For custom secrets, call get_secret() separately +let wallet_seed = secret_store.get_secret(my_secret_name).await?; +``` + +--- + +## 6. Offline Device Recovery + +**Question**: What if no other devices are online when verifying a new device? + +### 6.1 SSSS Retrieval Still Works + +Secrets in SSSS are stored as **account data events on the homeserver**. They can be retrieved anytime: + +1. User enters recovery key/passphrase +2. Client fetches `m.secret_storage.key.<key_id>` event +3. Client derives secret storage key from passphrase (or decodes base58) +4. Client fetches secret's account data event +5. Client decrypts locally + +**No other devices needed for SSSS retrieval.** + +### 6.2 Limitations + +What **does** require other devices online: +- Interactive verification (emoji comparison, QR scan) +- Secret *sharing* via `m.secret.request`/`m.secret.send` + +What **doesn't** require other devices: +- Recovery via passphrase/recovery key entry +- Decrypting secrets from account data + +### 6.3 Implication for Wallet Recovery + +Your wallet seed recovery flow: +1. User logs into new device +2. User enters Matrix recovery key OR passphrase +3. Client opens secret store, retrieves wallet seed +4. Wallet is restored + +**This works even if no other devices are online.** + +--- + +## 7. Encryption Details + +### 7.1 Algorithm: `m.secret_storage.v1.aes-hmac-sha2` + +| Component | Algorithm | Parameters | +|-----------|-----------|------------| +| Key Derivation (passphrase) | PBKDF2-HMAC-SHA512 | 500,000 iterations, 32-byte output | +| Per-Secret Key Derivation | HKDF-SHA256 | 32 zero-byte salt, secret name as info, 64-byte output | +| Encryption | AES-256-CTR | 16-byte IV, bit 63 cleared | +| Authentication | HMAC-SHA256 | 32-byte MAC | + +### 7.2 IV Handling + +```rust +// Generate 16 random bytes +let mut iv = [0u8; 16]; +rng.fill_bytes(&mut iv); + +// Clear bit 63 for cross-implementation compatibility +// (Some implementations have issues with high counter values) +let mut iv = u128::from_be_bytes(iv); +iv &= !(1 << 63); +iv.to_be_bytes() +``` + +### 7.3 Key Derivation Flow + +``` +Passphrase → PBKDF2 → 32-byte Master Key + ↓ + HKDF(master_key, salt=zeros, info=secret_name) + ↓ + 64 bytes output + / \ + AES Key (32) MAC Key (32) +``` + +### 7.4 Encrypt-then-MAC + +1. Encrypt plaintext with AES-CTR → ciphertext +2. HMAC(ciphertext) → MAC +3. Store: {iv, ciphertext, mac} + +On decrypt: +1. Verify MAC first (constant-time comparison) +2. Only if MAC valid, decrypt ciphertext + +--- + +## 8. Security Analysis + +### 8.1 Is Storing a Cardano Wallet Seed Safe? + +**Answer: Yes, with caveats.** + +#### What Makes It Safe + +1. **Strong Encryption**: AES-256-CTR + HMAC-SHA256 is a well-established authenticated encryption scheme + +2. **Good Key Derivation**: + - PBKDF2 with 500K iterations provides solid protection against offline brute-force + - HKDF provides proper domain separation per secret + +3. **Server Never Sees Plaintext**: Secrets are encrypted client-side before upload + +4. **MAC Verification**: Ciphertext integrity is verified before decryption + +5. **Zeroization**: matrix-rust-sdk zeroizes key material after use + +#### What Must Be True for Safety + +1. **Strong Recovery Key/Passphrase**: + - Base58 recovery key: 256 bits of entropy (excellent) + - Passphrase: security depends entirely on passphrase strength + - **Recommendation**: Always use the generated recovery key, not a passphrase + +2. **Homeserver Trust Model**: + - Homeserver sees encrypted blobs only + - Homeserver cannot decrypt without recovery key + - But homeserver could: + - Delete your secrets (availability attack) + - Serve old/modified encrypted data (but MAC will fail) + - **For high-value wallets**: Consider self-hosted homeserver + +3. **Client Security**: + - Recovery key must never be logged, displayed insecurely, or transmitted + - Device must be free of malware at secret creation time + +4. **No Key Escrow**: + - Matrix recovery key is the ONLY way to decrypt + - Lose recovery key = lose access to encrypted secrets + - **Critical**: User must securely back up recovery key + +### 8.2 Threat Model + +| Threat | Protection | Notes | +|--------|------------|-------| +| Homeserver compromise | AES-256-CTR + HMAC | Attacker needs recovery key | +| Network eavesdropping | TLS + encrypted at rest | Double protection | +| Weak passphrase | PBKDF2 500K iterations | Still vulnerable to offline attack with weak passphrase | +| Malicious client | None | Must trust client code | +| Lost recovery key | None | Permanent loss of secrets | + +### 8.3 Comparison to Hardware Wallet + +| Factor | SSSS | Hardware Wallet | +|--------|------|-----------------| +| Key never leaves device | ❌ (stored encrypted on server) | ✅ | +| Survives device loss | ✅ | ❌ (unless backed up) | +| Requires extra hardware | ❌ | ✅ | +| Single point of failure | Recovery key | Physical device | +| Brute-force resistant | Depends on passphrase | Very high | + +### 8.4 Security Recommendations + +1. **Use generated recovery key, not passphrase** for wallet seeds +2. **Store recovery key offline** (paper, metal backup, safety deposit) +3. **Never store recovery key digitally** in cloud services +4. **Self-host Matrix** if storing significant value +5. **Consider additional encryption layer** (encrypt wallet seed before storing in SSSS) +6. **Inform users clearly**: Recovery key loss = permanent wallet loss + +--- + +## 9. Precedents for Non-Matrix Secrets + +### 9.1 Existing Non-E2EE Secrets in SSSS + +Matrix itself stores non-E2EE-specific secrets: +- `m.megolm_backup.v1` - Key backup recovery key + +This demonstrates SSSS is designed for arbitrary cryptographic material. + +### 9.2 No Known External Precedents + +Research didn't find documented cases of: +- Cryptocurrency wallets using SSSS +- Third-party applications storing secrets in Matrix + +**You would be pioneering this use case.** + +### 9.3 Design Considerations from Spec Authors + +From Matrix spec rationale: +> "The secret storage mechanism provides a way to store arbitrary secrets on the homeserver encrypted by a key that only the user has access to." + +The design explicitly anticipates custom secrets. + +--- + +## 10. Wallet Storage Flow + +### Step-by-Step: Storing Cardano Wallet Seed + +``` +1. User creates new wallet in Element X Ada + └── Generate 24-word mnemonic (BIP-39) + +2. Check if SSSS is set up + ├── If yes: Open existing secret store + └── If no: Prompt user to set up recovery + └── Create new secret storage key + └── Save recovery key for user + +3. Encrypt and store wallet seed + └── secret_store.put_secret("com.sulkta.cardano.wallet_seed", mnemonic).await? + +4. Account data uploaded: + Event: com.sulkta.cardano.wallet_seed + Content: { + "encrypted": { + "<key_id>": { + "iv": "...", + "ciphertext": "...", + "mac": "..." + } + } + } + +5. User sees: "Wallet backed up to your Matrix account" +``` + +### Code Example + +```rust +use matrix_sdk::Client; +use ruma::events::secret::request::SecretName; + +const WALLET_SECRET_NAME: &str = "com.sulkta.cardano.wallet_seed"; + +pub async fn store_wallet_seed( + client: &Client, + mnemonic: &str, +) -> Result<(), Error> { + let secret_storage = client.encryption().secret_storage(); + + // Check if recovery is set up + if !secret_storage.is_enabled().await? { + return Err(Error::RecoveryNotSetUp); + } + + // User must enter recovery key to unlock secret store + let recovery_key = prompt_user_for_recovery_key(); + let secret_store = secret_storage + .open_secret_store(&recovery_key) + .await?; + + // Store the wallet seed + let secret_name = SecretName::from(WALLET_SECRET_NAME); + secret_store.put_secret(secret_name, mnemonic).await?; + + Ok(()) +} +``` + +--- + +## 11. Wallet Restore Flow + +### Step-by-Step: Restoring on New Device + +``` +1. User logs into Matrix on new device + └── Standard Matrix login flow + +2. Element X Ada detects no local wallet + └── Check for SSSS secret: com.sulkta.cardano.wallet_seed + +3. User enters recovery key + └── Open secret store with recovery key + +4. Retrieve and decrypt wallet seed + └── secret_store.get_secret("com.sulkta.cardano.wallet_seed") + +5. Restore Cardano wallet from mnemonic + └── BIP-39 → BIP-32 derivation + +6. User sees: "Wallet restored from your Matrix account" +``` + +### Code Example + +```rust +pub async fn restore_wallet_seed( + client: &Client, + recovery_key: &str, +) -> Result<Option<String>, Error> { + let secret_storage = client.encryption().secret_storage(); + + // Open secret store with user's recovery key + let secret_store = secret_storage + .open_secret_store(recovery_key) + .await?; + + // Retrieve wallet seed + let secret_name = SecretName::from(WALLET_SECRET_NAME); + let wallet_seed = secret_store.get_secret(secret_name).await?; + + Ok(wallet_seed) +} + +pub async fn auto_restore_on_verification(client: &Client) { + // After cross-signing verification succeeds, try to restore + let recovery = client.encryption().recovery(); + + // If recovery is already set up and we have the key... + if recovery.state() == RecoveryState::Enabled { + // Secret sharing: request from other devices + // This only works if other devices are online + + // Fallback: prompt user for recovery key + let key = prompt_for_recovery_key(); + if let Ok(seed) = restore_wallet_seed(&client, &key).await { + initialize_cardano_wallet(seed); + } + } +} +``` + +--- + +## 12. Spec Gaps and Limitations + +### 12.1 No Secret Deletion + +**Problem**: Account data events cannot be deleted, only overwritten. + +**Workaround**: To "delete" a secret, overwrite with empty encrypted content: +```json +{ + "encrypted": {} +} +``` + +**Implication**: Old encrypted versions may persist in homeserver history/backups. + +### 12.2 Secret Sharing Not Automatic for Custom Secrets + +**Problem**: `import_secrets()` only imports well-known secrets (cross-signing, backup key). + +**Workaround**: After recovery, explicitly call `get_secret()` for your custom secret. + +### 12.3 No Push Notification for Secret Availability + +**Problem**: New device doesn't know if secrets exist until it tries to fetch them. + +**Workaround**: Check for secret existence as part of onboarding flow. + +### 12.4 Race Conditions on Secret Update + +**Problem**: Concurrent updates from multiple devices can overwrite each other. + +**Mitigation**: matrix-rust-sdk uses a lock, but only within single client instance. + +**For wallets**: Wallet creation is typically one-time; updates are rare. + +### 12.5 No Versioning + +**Problem**: No built-in version tracking for secrets. + +**Workaround**: Include version in secret content: +```json +{ + "version": 1, + "mnemonic": "word word word..." +} +``` + +### 12.6 Secret Size Limits + +**Problem**: Account data events have practical size limits (varies by homeserver). + +**Wallet seed**: 24 words ≈ 200 bytes. Well under any reasonable limit. + +### 12.7 Recovery Key UX + +**Problem**: Users must save recovery key separately. Loss = permanent loss. + +**Mitigation**: +- Clear user education +- Multiple recovery methods (passphrase + recovery key) +- Recovery key QR code for easier backup + +--- + +## 13. Recommendations + +### 13.1 Implementation Recommendations + +1. **Use reverse-domain secret name**: `com.sulkta.cardano.wallet_seed` + +2. **Store structured data**: + ```json + { + "version": 1, + "format": "bip39", + "mnemonic": "word word word...", + "created_at": "2024-01-01T00:00:00Z" + } + ``` + +3. **Always require recovery key entry for wallet operations**: + - Don't cache the secret store key + - Re-prompt for recovery key when accessing wallet seed + +4. **Integrate with existing recovery flow**: + - Piggyback on Matrix recovery setup + - Don't create separate "wallet backup" flow + +5. **Handle missing SSSS gracefully**: + - If user hasn't set up recovery, prompt them to do so + - Allow manual seed export as fallback + +### 13.2 Security Recommendations + +1. **Strongly encourage recovery key over passphrase** +2. **Warn users about recovery key loss consequences** +3. **Consider additional application-layer encryption** +4. **Audit matrix-rust-sdk integration thoroughly** +5. **Self-hosting recommendation for high-value wallets** + +### 13.3 UX Recommendations + +1. **Unified recovery flow**: One recovery key for Matrix + wallet +2. **Clear onboarding**: Explain that Matrix account = wallet backup +3. **Recovery key export**: QR code, downloadable file, printable +4. **Regular recovery key verification prompts** + +--- + +## Appendix A: Event Schema Summary + +### m.secret_storage.key.\<key_id\> +```yaml +algorithm: "m.secret_storage.v1.aes-hmac-sha2" +iv: Base64 (16 bytes) # Optional, for key verification +mac: Base64 (32 bytes) # Optional, for key verification +passphrase: # Optional + algorithm: "m.pbkdf2" + salt: String + iterations: Integer +``` + +### m.secret_storage.default_key +```yaml +key: String # The key_id of the default key +``` + +### \<secret_name\> (e.g., com.sulkta.cardano.wallet_seed) +```yaml +encrypted: + <key_id>: + iv: Base64 (16 bytes) + ciphertext: Base64 + mac: Base64 (32 bytes) +``` + +--- + +## Appendix B: Relevant Source Files (matrix-rust-sdk) + +| File | Purpose | +|------|---------| +| `crates/matrix-sdk/src/encryption/secret_storage/mod.rs` | High-level SecretStorage API | +| `crates/matrix-sdk/src/encryption/secret_storage/secret_store.rs` | SecretStore: put_secret, get_secret | +| `crates/matrix-sdk/src/encryption/recovery/mod.rs` | Recovery flows | +| `crates/matrix-sdk-crypto/src/secret_storage.rs` | Low-level SecretStorageKey, encryption | +| `crates/matrix-sdk-crypto/src/ciphers.rs` | AES-CTR + HMAC implementation | +| `crates/matrix-sdk-crypto/src/gossiping/mod.rs` | Secret request/send handling | + +--- + +## Conclusion + +**Matrix SSSS is technically suitable for storing a Cardano wallet seed.** + +Strengths: +- Strong encryption (AES-256-CTR + HMAC-SHA256) +- Clean API for custom secrets +- Works offline (no other devices needed for recovery) +- Unified with Matrix identity/key management + +Risks to mitigate: +- Recovery key loss = permanent wallet loss +- Homeserver trust (for availability, not confidentiality) +- User education on security model + +Recommended approach: +1. Integrate wallet seed storage into existing Matrix recovery flow +2. Use generated recovery key (not passphrase) for wallet secrets +3. Clear user communication that recovery key is critical +4. Consider additional encryption layer for defense-in-depth