TASIOMIND.DEV — OPERATIONAL▸▸▸FULL STACK DEVELOPER @ GWQ SERVICEPLUS AG▸▸▸FOUNDER — K8SGPT.AI▸▸▸OPEN SOURCE: ACTIVE▸▸▸DISTRIBUTED SYSTEMS / KUBERNETES / AI▸▸▸RUST + GO + PYTHON▸▸▸FIELD TESTED / STATUS — NOMINAL▸▸▸LOCATION: EUROPE/BERLIN▸▸▸TASIOMIND.DEV — OPERATIONAL▸▸▸FULL STACK DEVELOPER @ GWQ SERVICEPLUS AG▸▸▸FOUNDER — K8SGPT.AI▸▸▸OPEN SOURCE: ACTIVE▸▸▸DISTRIBUTED SYSTEMS / KUBERNETES / AI▸▸▸RUST + GO + PYTHON▸▸▸FIELD TESTED / STATUS — NOMINAL▸▸▸LOCATION: EUROPE/BERLIN▸▸▸
on the bench

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.

Aland Baban · December 2025 · 15 min read
[ click to zoom ]
01 / 03

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:

code
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:

  1. Vertraulichkeit (Confidentiality): Nur der Inhaber des korrekten Schlüssels kann den Klartext lesen.
  2. Authentizität (Authenticity): Der Empfänger kann verifizieren, dass die Nachricht vom Inhaber des Schlüssels stammt.
  3. 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):

  1. Salt generieren: Ein zufälliger, kryptographischer Salt (32 Bytes) wird erzeugt. Dieser verhindert Rainbow-Table-Angriffe.
  2. 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.
  3. Nonce generieren: Eine zufällige Nonce (192 Bit bzw. 24 Bytes) für XChaCha20 wird erzeugt.
  4. 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).
  5. Datenpaket erstellen: Alle Komponenten werden zu einer einzigen Datei zusammengefügt: [Salt (32B)] + [Nonce (24B)] + [Ciphertext + Tag].

Entschlüsselung (Laden):

  1. Datenpaket analysieren: Die Datei wird gelesen und in ihre Bestandteile zerlegt: Salt, Nonce und Ciphertext+Tag.
  2. 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.
  3. Entschlüsseln (AEAD): Der abgeleitete Schlüssel und die extrahierte Nonce werden verwendet, um den Ciphertext zu entschlüsseln.
  4. 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:

code
[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.

code
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.

code
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.

code
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.

code
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

code
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

  1. 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 zeroize und die sicheren Typen von Orion (SecretKey, Password) sind hierfür unerlässlich.
  2. 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.
  3. 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.
  4. 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 ring ist 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.