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
This commit is contained in:
parent
34798f58a4
commit
7215ebb509
1 changed files with 824 additions and 0 deletions
824
MATRIX-KEY-STORAGE-REVIEW.md
Normal file
824
MATRIX-KEY-STORAGE-REVIEW.md
Normal file
|
|
@ -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.<key_id>
|
||||
```
|
||||
|
||||
Content structure:
|
||||
```json
|
||||
{
|
||||
"algorithm": "m.secret_storage.v1.aes-hmac-sha2",
|
||||
"iv": "<base64>", // 16 bytes, for MAC verification
|
||||
"mac": "<base64>", // 32 bytes, for key verification
|
||||
"passphrase": { // Optional, only if passphrase-derived
|
||||
"algorithm": "m.pbkdf2",
|
||||
"salt": "<string>",
|
||||
"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": "<key_id>"
|
||||
}
|
||||
```
|
||||
|
||||
#### 1.4 Encrypting a Secret
|
||||
|
||||
For each secret with name `<secret_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: <secret_name>
|
||||
```
|
||||
|
||||
### 3.2 Secret Event Content
|
||||
|
||||
```json
|
||||
{
|
||||
"encrypted": {
|
||||
"<key_id>": {
|
||||
"iv": "<base64>", // 16 bytes
|
||||
"ciphertext": "<base64>", // encrypted secret
|
||||
"mac": "<base64>" // 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": "<plaintext secret value>"
|
||||
}
|
||||
}
|
||||
```
|
||||
- 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue