Sichere Wallet-Speicherung: Private Keys verschlüsseln mit Orion in Rust
Eine tiefgehende Anleitung zur sicheren Speicherung von Solana-Wallets in Rust. Warum Klartext ein No-Go ist, warum Orion und XChaCha20-Poly1305 die beste Wahl sind und wie man eine passwortbasierte Authenticated Encryption (AEAD) robust implementiert – perfekt für Multi-Chain-Apps wie Symbiose.
Bei der Entwicklung von Symbiose, einer Anwendung, die mit mehreren Blockchains interagiert, ist die sichere Verwaltung von Private Keys (Wallets) eine zentrale Herausforderung. Eine der grundlegendsten Fragen lautet dabei: Wie können diese Schlüssel sicher gespeichert werden?
Ein Private Key ist der ultimative Zugang zu allen Vermögenswerten auf einer Blockchain. Wer den Schlüssel besitzt, kontrolliert die zugehörigen Assets. Eine unsichere Speicherung ist daher fahrlässig.
Am Beispiel von Solana wird dies deutlich. Die offizielle solana-cli speichert Keypairs standardmäßig als unverschlüsseltes JSON-Array von Bytes:
solana-keygen new --outfile ~/.config/solana/id.json
# Ausgabe: Wrote new keypair to /home/user/.config/solana/id.json
cat ~/.config/solana/id.json
# Ausgabe: [123, 45, 67, ..., 201, 89] <-- PRIVATE KEY IM KLARTEXT
Das Problem: Jeder Prozess, jede Malware und jeder Benutzer mit Lesezugriff auf das Dateisystem kann diese Datei öffnen, den Schlüssel extrahieren und die Wallet leeren.
Die Lösung: Der Private Key muss verschlüsselt werden, bevor er auf die Festplatte geschrieben wird, und zwar so, dass nur eine Person mit einem geheimen Passwort ihn wieder entschlüsseln kann.
Warum Authenticated Encryption (AEAD)?
Verschlüsselung allein genügt nicht. Es muss sichergestellt werden, dass die verschlüsselten Daten während der Speicherung oder Übertragung nicht manipuliert wurden (Integrität) und tatsächlich vom erwarteten Ursprung stammen (Authentizität).
Hier kommt Authenticated Encryption with Associated Data (AEAD) ins Spiel. Ein AEAD-Algorithmus bietet gleichzeitig:
- Vertraulichkeit (Confidentiality): Nur der Inhaber des korrekten Schlüssels kann den Klartext lesen.
- Authentizität (Authenticity): Der Empfänger kann verifizieren, dass die Nachricht vom Inhaber des Schlüssels stammt.
- Integrität (Integrity): Der Empfänger kann sicherstellen, dass die Nachricht seit ihrer Erstellung nicht verändert wurde.
Der Versuch, einen manipulierten oder mit einem falschen Schlüssel verschlüsselten Ciphertext zu entschlüsseln, führt bei einem AEAD-Schema zu einem Authentifizierungsfehler, anstatt unsinnige Daten zurückzugeben. Dies verhindert eine ganze Klasse kryptographischer Angriffe, wie z.B. Padding-Oracle-Angriffe.
Unsere Wahl: Orion & XChaCha20-Poly1305
Für die Implementierung in Rust wurden die Bibliothek Orion und der Algorithmus XChaCha20-Poly1305 gewählt. Die Gründe dafür sind:
Warum XChaCha20-Poly1305?
- Modern und Sicher: Es handelt sich um einen von der IETF standardisierten (RFC 8439), modernen AEAD-Algorithmus.
- Nonce-Misuse-Resistant: Im Gegensatz zu AES-GCM (das eine 96-Bit-Nonce verwendet), nutzt XChaCha20 eine 192-Bit-Nonce. Dies macht eine versehentliche Wiederverwendung derselben Nonce mit demselben Schlüssel extrem unwahrscheinlich, selbst bei zufälliger Generierung. Eine Nonce-Wiederverwendung bei AES-GCM kann katastrophale Folgen haben und den Schlüssel kompromittieren. XChaCha20 ist hier weitaus robuster.
- Software-Performance: Auf CPUs ohne spezielle AES-Hardwarebeschleunigung (AES-NI) ist ChaCha20 oft performanter als AES.
Warum Orion?
- Auditiert und Sicher: Orion basiert auf den bewährten Primitiven von
libsodium, ist jedoch in reinem, sicherem Rust geschrieben und wurde extern auditiert. - Benutzerfreundlichkeit: Die API ist darauf ausgelegt, eine falsche Anwendung zu erschweren ("misuse-resistant"). Sie erzwingt beispielsweise den korrekten Umgang mit Nonces und Authentifizierungs-Tags.
- Fokus: Die Bibliothek konzentriert sich auf moderne, empfohlene Algorithmen (XChaCha20, BLAKE2b, Argon2id) und verzichtet auf veraltete kryptographische Primitive.
Der kryptographische Prozess
Ziel ist die Implementierung einer passwortbasierten Verschlüsselung. Da ein Benutzerpasswort in der Regel eine geringere Entropie als ein kryptographischer Schlüssel hat, muss ein sicherer Ableitungsprozess verwendet werden.
Verschlüsselung (Speichern):
- Salt generieren: Ein zufälliger, kryptographischer Salt (32 Bytes) wird erzeugt. Dieser verhindert Rainbow-Table-Angriffe.
- Schlüssel ableiten (KDF): Eine Key Derivation Function (KDF) – hier Argon2id – wird verwendet, um aus dem Passwort und dem Salt einen starken 256-Bit-Verschlüsselungsschlüssel abzuleiten. Argon2id ist bewusst speicher- und rechenintensiv, was Brute-Force-Angriffe erheblich erschwert.
- Nonce generieren: Eine zufällige Nonce (192 Bit bzw. 24 Bytes) für XChaCha20 wird erzeugt.
- Verschlüsseln (AEAD): Der abgeleitete Schlüssel und die Nonce werden verwendet, um den Solana Private Key mit XChaCha20-Poly1305 zu verschlüsseln. Das Ergebnis ist der Ciphertext plus ein Authentifizierungs-Tag (16 Bytes).
- Datenpaket erstellen: Alle Komponenten werden zu einer einzigen Datei zusammengefügt:
[Salt (32B)] + [Nonce (24B)] + [Ciphertext + Tag].
Entschlüsselung (Laden):
- Datenpaket analysieren: Die Datei wird gelesen und in ihre Bestandteile zerlegt: Salt, Nonce und Ciphertext+Tag.
- Schlüssel erneut ableiten: Mithilfe des vom Benutzer eingegebenen Passworts und des extrahierten Salts wird mit derselben KDF (Argon2id) exakt derselbe Verschlüsselungsschlüssel reproduziert.
- Entschlüsseln (AEAD): Der abgeleitete Schlüssel und die extrahierte Nonce werden verwendet, um den Ciphertext zu entschlüsseln.
- Authentifizierung prüfen: Der AEAD-Algorithmus prüft automatisch, ob das Tag zum Ciphertext passt. Bei Erfolg wird der entschlüsselte Private Key zurückgegeben. Bei einem Fehlschlag (z.B. falsches Passwort, manipulierte Daten) wird ein Fehler ausgelöst.
Implementierung in Rust
Abhängigkeiten
Folgende Crates werden in der Cargo.toml benötigt:
[dependencies]
orion = "0.17"
solana-sdk = "1.18"
getrandom = { version = "0.2", features = ["std"] }
base58 = "0.2"
anyhow = "1.0"
zeroize = { version = "1.7", features = ["derive"] }
Wallet vorbereiten
Das Solana Keypair wird für die Verschlüsselung in einen Base58-String serialisiert.
use solana_sdk::signature::Keypair;
use base58::ToBase58;
use zeroize::Zeroize;
// Ein Wrapper, um sensible Daten beim Verlassen des Gültigkeitsbereichs automatisch zu nullen.
#[derive(Zeroize)]
#[zeroize(drop)]
struct SensitiveData(Vec<u8>);
// Beispiel: Wallet erstellen und für die Verschlüsselung vorbereiten
let wallet = Keypair::new();
let wallet_base58_string = wallet.to_base58_string();
let mut wallet_bytes = SensitiveData(wallet_base58_string.into_bytes());
Schlüsselableitung (KDF) mit Argon2
Dies ist der entscheidende Schritt zur Absicherung gegen Brute-Force-Angriffe.
use orion::{
kdf::{derive_key, Password, Salt},
hazardous::stream::chacha20::CHACHA_KEYSIZE,
};
use getrandom::getrandom;
// Generiert einen 32-Byte Salt
fn generate_salt() -> Result<Salt, anyhow::Error> {
let mut salt_bytes = [0u8; 32];
getrandom(&mut salt_bytes)?;
Ok(Salt::from_slice(&salt_bytes)?)
}
// Leitet einen 256-Bit-Verschlüsselungsschlüssel aus Passwort und Salt ab
fn derive_secret_key(password: &str, salt: &Salt) -> Result<orion::hazardous::aead::xchacha20poly1305::SecretKey, anyhow::Error> {
let pw = Password::from_slice(password.as_bytes())?;
// Argon2id-Parameter (z.B. Iterationen, Speicher) werden hier auf sichere Standardwerte gesetzt.
let derived_key = derive_key(&pw, salt, 15, 1024, CHACHA_KEYSIZE as u32)?;
Ok(orion::hazardous::aead::xchacha20poly1305::SecretKey::from_slice(derived_key.unprotected_as_bytes())?)
}
Verschlüsselung
Die orion::aead::seal-Funktion wird für die Verschlüsselung verwendet.
use orion::aead::seal;
use orion::hazardous::stream::xchacha20::XCHACHA_NONCESIZE; // 24 Bytes
use orion::hazardous::mac::poly1305::POLY1305_OUTSIZE; // 16 Bytes
fn encrypt_wallet(wallet_data: &SensitiveData, password: &str) -> Result<Vec<u8>, anyhow::Error> {
// 1. Salt generieren und Schlüssel ableiten
let salt = generate_salt()?;
let key = derive_secret_key(password, &salt)?;
// 2. Nonce generieren
let mut nonce_bytes = [0u8; XCHACHA_NONCESIZE];
getrandom(&mut nonce_bytes)?;
let nonce = orion::hazardous::aead::xchacha20poly1305::Nonce::from_slice(&nonce_bytes)?;
// 3. Puffer für Ciphertext und Tag vorbereiten
let tag_size = POLY1305_OUTSIZE;
let mut ciphertext_buffer = vec![0u8; wallet_data.0.len() + tag_size];
// 4. Verschlüsseln
seal(&key, &nonce, &wallet_data.0, None, &mut ciphertext_buffer)?;
// 5. Daten zusammenfügen: [Salt] + [Nonce] + [Ciphertext + Tag]
let mut final_output = Vec::new();
final_output.extend_from_slice(salt.as_ref());
final_output.extend_from_slice(nonce.as_ref());
final_output.extend_from_slice(&ciphertext_buffer);
Ok(final_output)
}
Entschlüsselung
Der Prozess wird umgekehrt, wobei die Datenstrukturen (Salt, Nonce, Ciphertext) präzise aus dem Byte-Slice extrahiert werden müssen.
use orion::aead::open;
fn decrypt_wallet(encrypted_data: &[u8], password: &str) -> Result<Keypair, anyhow::Error> {
const SALT_LEN: usize = 32;
const NONCE_LEN: usize = XCHACHA_NONCESIZE;
if encrypted_data.len() < SALT_LEN + NONCE_LEN + POLY1305_OUTSIZE {
anyhow::bail!("Verschlüsselte Daten sind unvollständig oder beschädigt.");
}
// 1. Daten extrahieren
let salt_bytes = &encrypted_data[..SALT_LEN];
let nonce_bytes = &encrypted_data[SALT_LEN..SALT_LEN + NONCE_LEN];
let ciphertext_with_tag = &encrypted_data[SALT_LEN + NONCE_LEN..];
// 2. Typen wiederherstellen
let salt = Salt::from_slice(salt_bytes)?;
let nonce = orion::hazardous::aead::xchacha20poly1305::Nonce::from_slice(nonce_bytes)?;
// 3. Schlüssel erneut ableiten
let key = derive_secret_key(password, &salt)?;
// 4. Entschlüsseln und authentifizieren
let mut plaintext_buffer = vec![0u8; ciphertext_with_tag.len() - POLY1305_OUTSIZE];
open(&key, &nonce, ciphertext_with_tag, None, &mut plaintext_buffer)?;
// 5. Wallet wiederherstellen
let mut plaintext_sensitive = SensitiveData(plaintext_buffer);
let wallet_str = String::from_utf8(plaintext_sensitive.0.clone())?;
let wallet_bytes = base58::from(&wallet_str)?;
let keypair = Keypair::from_bytes(&wallet_bytes)?;
Ok(keypair)
}
Vollständiges Testbeispiel
fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("--- Starte sicheren Wallet-Test ---");
// 1. Wallet erstellen
let original_wallet = Keypair::new();
let wallet_base58 = original_wallet.to_base58_string();
let mut sensitive_data = SensitiveData(wallet_base58.into_bytes());
// 2. Passwort festlegen
let password = "mein-super-geheimes-passwort-2025!";
// 3. Verschlüsseln
let encrypted_blob = encrypt_wallet(&sensitive_data, password)?;
// 4. Entschlüsseln (Erfolgsfall)
let decrypted_wallet = decrypt_wallet(&encrypted_blob, password)?;
assert_eq!(original_wallet.to_bytes(), decrypted_wallet.to_bytes());
println!("✅ Validierung erfolgreich: Wallets sind identisch.");
// 5. Entschlüsseln (Fehlerfall - falsches Passwort)
let wrong_password = "falsches-passwort";
let result = decrypt_wallet(&encrypted_blob, wrong_password);
assert!(result.is_err());
println!("✅ Entschlüsselung mit falschem Passwort wie erwartet fehlgeschlagen.");
// 6. Entschlüsseln (Fehlerfall - manipulierte Daten)
let mut tampered_blob = encrypted_blob.clone();
let last_idx = tampered_blob.len() - 1;
tampered_blob[last_idx] ^= 0xFF; // Ein Byte manipulieren
let result_tampered = decrypt_wallet(&tampered_blob, password);
assert!(result_tampered.is_err());
println!("✅ Entschlüsselung mit manipulierten Daten wie erwartet fehlgeschlagen.");
println!("\n--- Alle Tests abgeschlossen ---");
Ok(())
}
Sicherheitshinweise und Best Practices
- Speicherbereinigung (Zeroize): Sensible Daten wie Klartext-Schlüssel, Passwörter und abgeleitete Schlüssel müssen so kurz wie möglich im Arbeitsspeicher (RAM) verbleiben und anschließend sicher überschrieben werden. Crates wie
zeroizeund die sicheren Typen von Orion (SecretKey,Password) sind hierfür unerlässlich. - Passwortstärke: Die Sicherheit des gesamten Schemas hängt von der Stärke des Benutzerpassworts ab. Anwendungen sollten robuste Passwortrichtlinien durchsetzen oder KDF-Parameter mit höheren Kosten (mehr RAM/CPU) verwenden, um schwächere Passwörter zu kompensieren.
- Sichere Speicherung des Datenpakets: Die verschlüsselte Datei kann sicher an beliebigen Orten (Dateisystem, Cloud-Backup) gespeichert werden, da sie ohne das korrekte Passwort wertlos ist.
- Keine Eigenentwicklungen in der Kryptographie: Es sollte niemals versucht werden, eigene Verschlüsselungsschemata zu entwickeln. Die Verwendung etablierter Standards (AEAD, Argon2id) und auditierter Bibliotheken wie Orion oder
ringist zwingend erforderlich.
Fazit
Die sichere Speicherung von Private Keys ist eine fundamentale Anforderung für jede Blockchain-Anwendung. Sie erfordert Sorgfalt und den Einsatz bewährter kryptographischer Werkzeuge. Die hier vorgestellte Methode, die auf Rust, der Orion-Bibliothek und dem XChaCha20-Poly1305 AEAD-Algorithmus basiert, bietet eine moderne, performante und vor allem sichere Lösung, die weit über die Standardimplementierungen vieler Kommandozeilen-Tools hinausgeht.
Im Rahmen des Projekts Symbiose wird dieser Ansatz verwendet, um die Vermögenswerte der Nutzer über verschiedene Blockchains hinweg zu schützen.
Der vollständige, lauffähige Beispielcode ist im folgenden Repository zu finden: github.com/amariwan/symbiose-wallet-encryption.