惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

博客园 - 司徒正美
月光博客
月光博客
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
美团技术团队
WordPress大学
WordPress大学
罗磊的独立博客
Last Week in AI
Last Week in AI
人人都是产品经理
人人都是产品经理
爱范儿
爱范儿
MongoDB | Blog
MongoDB | Blog
J
Java Code Geeks
H
Hackread – Cybersecurity News, Data Breaches, AI and More
H
Help Net Security
S
SegmentFault 最新的问题
C
Check Point Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
腾讯CDC
Engineering at Meta
Engineering at Meta
The GitHub Blog
The GitHub Blog
F
Fortinet All Blogs
D
DataBreaches.Net
雷峰网
雷峰网
GbyAI
GbyAI
宝玉的分享
宝玉的分享

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
Building a Zero-Knowledge Password Manager with Rust and ...
od1n · 2026-06-05 · via DEV Community

od1n

LastPass was breached in 2022. 33 million encrypted vaults ended up in attackers' hands. The root cause? Your passwords lived on their servers.

I decided to build a password manager where that scenario is architecturally impossible — because there is no server.

This is how I built Vault Local: a local-first, zero-knowledge password manager using Rust, Tauri 2.0, and modern cryptography.

The Architecture

The core principle: your data never leaves your computer. There's no cloud, no sync server, no account creation. Just one encrypted file on your disk.

Master Password
      │
      ▼
  Argon2id (19 MiB RAM, 2 iterations)
      │
      ▼
  HKDF-SHA256
      ├──────────────────┐
      ▼                  ▼
  db_key             enc_key
  (SQLCipher)        (XChaCha20-Poly1305)
      │                  │
      ▼                  ▼
  Encrypts entire    Encrypts individual
  database file      credential fields

Enter fullscreen mode Exit fullscreen mode

Two independent keys derived from the same master password, but cryptographically separated via HKDF with different info strings. Even if SQLCipher were somehow compromised, the field-level encryption remains intact.

Why Rust?

The ownership system prevents the exact class of bugs that plague password managers:

  • No buffer overflows — Rust's borrow checker eliminates them at compile time
  • No use-after-free — the compiler enforces lifetimes
  • Deterministic cleanupDrop runs exactly when a value goes out of scope, which matters when that value is an encryption key

For cryptographic secrets, I use the zeroize and secrecy crates:

use zeroize::{Zeroize, ZeroizeOnDrop};
use secrecy::Secret;

#[derive(Zeroize, ZeroizeOnDrop)]
pub struct EncKey(pub [u8; 32]);

pub struct VaultState {
    pub connection: Connection,
    pub enc_key: Secret<EncKey>,  // Auto-zeroized on drop
}

Enter fullscreen mode Exit fullscreen mode

When the vault locks, VaultState drops, and the encryption key is overwritten with zeros in memory. No garbage collector, no "maybe the runtime will clean it up eventually."

Why Tauri 2.0 over Electron?

Electron Tauri 2.0
Installer size ~150 MB 4.6 MB
Runtime Bundles Chromium Uses system WebView
Backend Node.js (JS) Rust (native)
Security model Full filesystem access Principle of least privilege
IPC Unrestricted Explicit command registration

Tauri's security model is the key differentiator. The web frontend cannot access the filesystem, network, or system APIs. Every capability must be explicitly declared:

{
  "permissions": ["core:default", "dialog:default"]
}

Enter fullscreen mode Exit fullscreen mode

The frontend communicates with the Rust backend exclusively through typed commands:

#[tauri::command]
pub fn get_entry(
    state: tauri::State<'_, AppState>,
    id: String,
) -> Result<Entry, String> {
    // Decrypt and return entry
}

Enter fullscreen mode Exit fullscreen mode

// Frontend — this is the ONLY way to access data
const entry = await invoke<Entry>('get_entry', { id });

Enter fullscreen mode Exit fullscreen mode

The Encryption Stack

Key Derivation: Argon2id

Argon2id won the Password Hashing Competition. It's memory-hard, meaning it requires a fixed amount of RAM to compute — making GPU/ASIC attacks impractical:

let params = Params::new(
    19456,  // 19 MiB of RAM
    2,      // 2 iterations
    1,      // 1 degree of parallelism
    Some(32) // 32 bytes output
)?;

let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
argon2.hash_password_into(password, salt, &mut master_key)?;

Enter fullscreen mode Exit fullscreen mode

Symmetric Encryption: XChaCha20-Poly1305

I chose XChaCha20-Poly1305 over AES-256-GCM for a specific reason: the extended nonce.

  • AES-256-GCM: 96-bit nonce → collision risk after ~2^32 encryptions with the same key
  • XChaCha20-Poly1305: 192-bit nonce → safe for virtually unlimited encryptions

For a long-lived local vault where the same key encrypts thousands of entries over years, the larger nonce margin is significant.

pub fn encrypt(key: &[u8; 32], plaintext: &[u8]) -> Result<Vec<u8>, String> {
    let cipher = XChaCha20Poly1305::new(key.into());
    let mut nonce_bytes = [0u8; 24];
    OsRng.fill_bytes(&mut nonce_bytes);
    let nonce = XNonce::from_slice(&nonce_bytes);

    let ciphertext = cipher.encrypt(nonce, plaintext)?;

    // Store as: nonce (24 bytes) || ciphertext
    let mut result = Vec::with_capacity(24 + ciphertext.len());
    result.extend_from_slice(&nonce_bytes);
    result.extend_from_slice(&ciphertext);
    Ok(result)
}

Enter fullscreen mode Exit fullscreen mode

Database Encryption: SQLCipher

SQLCipher is a fork of SQLite that encrypts the entire database file with AES-256-CBC + HMAC-SHA256. Every page is encrypted independently.

pub fn open_db(db_path: &Path, db_key: &[u8; 32]) -> Result<Connection, String> {
    let conn = Connection::open(db_path)?;
    let hex_key = hex::encode(db_key);
    conn.execute_batch(&format!("PRAGMA key = \"x'{}'\";", hex_key))?;
    // Verify the key works
    conn.execute_batch("SELECT count(*) FROM sqlite_master;")?;
    Ok(conn)
}

Enter fullscreen mode Exit fullscreen mode

Browser Extension via Native Messaging

The browser extension doesn't store any credentials. It communicates with the desktop app through Chrome's Native Messaging protocol:

Extension popup → Native Messaging Host (Node.js) → TCP localhost:51820 → Rust IPC Server

Enter fullscreen mode Exit fullscreen mode

The IPC server only listens on 127.0.0.1 and requires a token that regenerates on every vault unlock. No external network access.

What I Learned

  1. Tauri 2.0 is production-ready for security-sensitive apps. The permission model is genuinely useful.

  2. SQLCipher's bundled-sqlcipher-vendored-openssl feature in rusqlite compiles OpenSSL from source. It requires Perl on Windows (Strawberry Perl). Plan for this in your build pipeline.

  3. The zeroize crate can't clear CPU registers — it's a fundamental limitation. But it handles heap and stack memory correctly, which covers the vast majority of attack surface.

  4. XChaCha20 is constant-time without hardware support, unlike AES which relies on AES-NI for timing-attack resistance. This matters for cross-platform software where you can't guarantee hardware features.

  5. Native Messaging on Windows requires a .bat wrapper — Node.js .js files can't be executed directly as native messaging hosts. A one-line batch file solves it.

Try It

Vault Local is free and open source (MIT):

Features include: password audit with HIBP breach check, TOTP authenticator, SSH agent, import from 8 formats (Chrome, Firefox, Bitwarden, 1Password, LastPass, KeePass), encrypted sync, browser extension, and passkey storage.

Built with ~15,000 lines of Rust + TypeScript across ~70 files. I'd appreciate any feedback, especially on the cryptographic implementation.