HashFS is a production-ready Vue 3 composable that provides industry-standard encrypted file storage directly in the browser. It combines content-addressable storage, Ed25519 signatures, and cryptographic hash chains to create a zero-trust file vault with complete privacy - no servers, no tracking, no data leaks.
- π Zero-leak privacy - Everything encrypted client-side, nothing leaves your browser
- π Hash chain integrity - Cryptographic verification of entire file history
- ποΈ Ed25519 signatures - Tamper-proof authenticity for every version
- π¦ Content addressing - BLAKE3 deduplication with automatic compression
- β±οΈ Version control - Immutable history with configurable retention and undo/redo
- β‘ Offline-first - Works completely offline using IndexedDB
- π¨ Vue 3 reactive - Seamless two-way binding with auto-save
- π‘οΈ Zero dependencies - Self-contained security, no external services
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>#FS test</title>
<script type="importmap">
{ "imports": { "vue": "https://esm.sh/vue" } }
</script>
</head>
<body>
<div id="app" style="display:flex; flex-direction: column; gap: 1em;">
<input type="text" id="input" style="width:80svw;" />
<textarea style="width:80svw;height:80svh" id="text" disabled></textarea>
</div>
<script type="module">
import { ref, watch } from "vue";
import { useHashFS, useFile } from "./lib/index.js";
const md = useFile("readme.md", "## Initial content");
const input = document.getElementById("input");
input.addEventListener("change", (e) => {
const fs = useHashFS(e?.target?.value);
});
const textarea = document.getElementById("text");
watch(
md.loading,
(l) => {
if (!l) {
textarea.disabled = false;
}
},
{ immediate: true }
);
watch(
md.text,
(t) => {
textarea.innerText = t;
},
{ immediate: true }
);
textarea.addEventListener("change", (e) => {
md.text.value = e?.target?.value;
md.save();
});
</script>
</body>
</html>
Each file maintains an immutable chain where every version references the previous:
Genesis β Hash(v1) β Hash(v2) β Hash(v3) β Current
β β β β
Sign(v1) Sign(v2) Sign(v3) Sign(current)
This creates an unforgeable history where any tampering breaks the entire chain.
Passphrase β scrypt(N=2^17, r=8, p=1) β 32-byte Master Key
ββ HKDF-SHA256(..., "signing") β Signing Key (32b) β Ed25519
ββ HKDF-SHA256(..., "encryption") β Encrypt Key (32b) β AES-256-GCM
ββ BLAKE3(pubKey)[0..15] β Vault namespace (dbName)
Content β BLAKE3 (content-address) β Chain link metadata β JSON chain β DEFLATE (fflate) β BLAKE3(compressed) β Ed25519 sign(compressed hash) β AES-GCM encrypt(compressed bytes) β IndexedDB (payload + signature)
npm install hashfs
The new API introduces a dual-composable design:
useHashFS(passphrase)
- Manages the secure vault and global file indexuseFile(vault, name, mime)
- Binds to a specific file for easy reactive read/write
This allows you to directly work with a file as a reactive resource, while still retaining access to full vault management.
<script setup>
import { ref } from "vue";
import { useHashFS } from "hashfs";
const passphrase = ref("correct horse battery staple");
// Unlock the vault
const vault = useHashFS(passphrase.value);
// Create or open a text file
const notes = vault.useFile(vault, "notes.md", { mime: "text/markdown" });
// Reactive text content
notes.text.value = "Hello, secure world!";
// Persist change
await notes.save();
// Later, read it back
console.log(notes.text.value); // "Hello, secure world!"
</script>
<script setup>
import { ref } from "vue";
import { useHashFS } from "hashfs";
const passphrase = ref("my-photo-vault");
// Unlock vault
const vault = useHashFS(passphrase.value);
// Work with an image file
const avatar = vault.useFile("avatar.png");
// Import from an `<input type="file">`
const handleFile = async (event) => {
const file = event.target.files[0];
await avatar.import(file); // Encrypted & stored
};
// Export and display as object URL
const showImage = async () => {
const blob = await avatar.export();
const url = URL.createObjectURL(blob);
document.querySelector("#preview").src = url;
};
</script>
<template>
<input type="file" accept="image/*" @change="handleFile" />
<button @click="showImage">Show Stored Image</button>
<img id="preview" />
</template>
const vault = useHashFS(passphrase);
// State
vault.auth; // Ref<boolean> - Vault unlocked status
vault.loading; // Ref<boolean> - Operation in progress
vault.files; // Ref<FileInfo[]> - File index
vault.stats; // ComputedRef - aggregate stats (sizes, compression ratio, vault metrics)
// Operations
await vault.importAll(fileList, onProgress); // Bulk import File[] from an <input>
await vault.exportZip(onProgress); // Export vault contents as a zip (Uint8Array)
await vault.importZip(arrayBuffer, onProgress); // Import vault contents from zip
await vault.downloadVault(filename, onProgress); // Trigger browser download of vault zip
await vault.getVaultSizes(); // Get detailed vault size information
await vault.wipeVault(); // Wipe vault and close
vault.close(); // Close and terminate internal worker/session
// Note: `useFile` is provided as a separate composable (re-exported by the package). Use `useFile(name, defaultContent)` to bind to a single file resource.
HashFS provides three distinct size measurements to help you understand your storage usage:
- Original Size - Sum of current file contents (what you'd see if you downloaded all files)
- Compressed Size - Size of vault when exported as ZIP (latest versions only, no version history)
- Vault Size - Total IndexedDB storage including all versions, chains, and metadata
Files (8)
Original: 2.9 MB β Current file contents
Compressed: 803.0 KB β ZIP export size (72.5% smaller!)
Vault size: 10.9 MB β Full encrypted IndexedDB storage
Saved: 72.5%
Text Files (Markdown, HTML, JSON):
- Typically compress 70-90% (amazing ratios!)
Binary Files (Images, PDFs, Videos):
- Already compressed formats may show modest savings or slight growth
- Growth can occur due to ZIP compression headers on small files
- Overall vault compression usually more than compensates
const vault = useHashFS(passphrase);
// Get detailed size information
const sizes = await vault.getVaultSizes();
// Returns: { vaultSize: number, vaultCompressedSize: number }
// Access via stats computed property
console.log(vault.stats.value);
// Contains: original size, compressed size, vault size, compression ratio
const file = useFile("document.md", "# Hello");
// Instance shape (returns a singleton per filename)
file.loading; // Ref<boolean> - load/save operation in progress
file.filename; // string - the file name (read-only on instance)
file.mime; // Ref<string> - MIME type
file.text; // ComputedRef<string> - UTF-8 text view (getter decodes bytes, setter encodes & marks dirty)
file.bytes; // Ref<Uint8Array> - raw binary content
file.dirty; // Ref<boolean> - unsaved changes
file.currentVersion; // Ref<number> - currently loaded version number
file.availableVersions; // Ref<{min:number,max:number}> - range of available versions
file.canUndo; // ComputedRef<boolean> - whether undo is possible
file.canRedo; // ComputedRef<boolean> - whether redo is possible
// Methods (all async when performing IO)
await file.load((version = null)); // Load latest or specified version
await file.save(); // Persist current bytes to the vault
await file.import(fileBlob); // Import from a Blob/File (reads bytes, sets mime and saves)
file.export(); // Triggers a browser download of the file (no return value)
await file.rename(newName); // Rename file in vault
await file.delete(); // Delete file from vault
await file.undo(); // Load previous version
await file.redo(); // Load next version
// Options
useFile(name, initialContent, {
autoSave: true | false,
autoSaveDelay: milliseconds,
mime,
passphrase,
});
// - autoSave: enabled by default; autoSaveDelay defaults to 3000 ms
// - initialContent: if provided and not authenticated, it initializes the in-memory bytes
// - passphrase: optional per-file init fallback (attempts WM.init)
Each entry in vault.files
contains:
{
name: "document.md", // File name
mime: "text/markdown", // MIME type
versions: 3, // Number of versions
size: 2048, // Original content size
compressedSize: 1024, // Storage size
modified: 1703123456789, // Last modified timestamp
active: true // Currently selected
}
// Each version forms a link in the cryptographic chain
{
version: 3, // Sequential version number
hash: "abc123...", // BLAKE3 of content (content-address)
sig: "def456...", // Ed25519 signature over the compressed chain bytes' hash
key: "sk_789...", // Storage key / content identifier
size: 1024, // Original content size
ts: 1703123456789, // Creation timestamp
parentHash: "xyz999..." // Links to previous version
}
// HashFS automatically verifies:
1. Content matches its BLAKE3 content-address (integrity)
2. Chain authenticity via Ed25519 signature (signatures over chain hash)
3. Chain integrity via binary hash concatenation with domain separation
4. Individual version signatures and hashes
5. Automatic recovery from corrupted versions
// Implementation notes:
// - Chain JSON is serialized and DEFLATE-compressed, then the compressed bytes are hashed (BLAKE3) and signed with Ed25519.
// - Chain hash is computed using binary concatenation of version hashes with domain separation ('HashFS-Chain-v6').
// - The compressed bytes are then encrypted with AES-GCM and stored in IndexedDB together with the signature field.
// - On load the encrypted payload is decrypted, the compressed bytes' hash is verified against the stored signature, and finally the JSON is inflated and parsed.
// - Legacy chains without chain hash are automatically migrated to the new format.
// Any verification failure prevents access to the chain.
- No network requests - Everything stays in your browser
- No telemetry - Zero tracking or analytics
- No plaintext - All content encrypted at rest
- No metadata leaks - Even file names are encrypted
- No key escrow - Only your passphrase can decrypt
- AES-256-GCM - Industry-standard authenticated encryption
- Ed25519 - State-of-the-art elliptic curve signatures
- BLAKE3 - Fast, secure content addressing and hashing
- scrypt - Memory-hard key derivation (N=2^17, r=8, p=1)
- HKDF - Key separation for signing and encryption
- Random IVs - Fresh entropy for every encryption
- Hash chains - Detect any tampering with version history
- Content addressing - Impossible to modify without changing hash
- Cryptographic signatures - Prove authenticity of every change
- Atomic transactions - Prevent corruption from interrupted operations
HashFS protects against:
- β Data breaches (encrypted at rest)
- β Content tampering (hash chain verification)
- β History rewriting (cryptographic signatures)
- β Unauthorized access (strong key derivation)
- β Man-in-the-middle (client-side only)
- β Passphrase attacks - Use strong, unique passphrases (20+ chars)
- β Browser vulnerabilities - Keep browser updated
- β Physical device access - Browser may cache decrypted data
- β Side-channel attacks - JavaScript crypto has limitations
- Strong Passphrases - Use unique 20+ character passphrases
- HTTPS Required - WebCrypto API needs secure context
- Regular Backups - Export data with
exportAll()
periodically - Browser Security - Keep browser and extensions updated
- Private Mode - Consider for highly sensitive data
- Physical Security - Lock your device when not in use
Browser Environment
ββ IndexedDB
β ββ files/ (encrypted content blobs)
β ββ meta/ (encrypted file metadata)
β ββ chains/ (encrypted version chains)
ββ Memory
ββ Vue reactive state
ββ LRU chain cache
ββ Derived cryptographic keys
@noble/curves (Ed25519 signatures)
@noble/hashes (BLAKE3, scrypt, HKDF)
@noble/ciphers (AES-256-GCM)
fflate (Deflate compression)
Composition API
ββ Reactive state management
ββ Computed property bindings
ββ Auto-save with debouncing
ββ Lifecycle cleanup
git clone https://github.com/yourusername/hashfs
cd hashfs
pnpm install
pnpm run dev
pnpm run lib
pnpm run build
This project is licensed under the MIT License.
Built on audited cryptographic primitives:
- @noble/curves - Secure, audited Ed25519 signatures
- @noble/hashes - Fast, secure BLAKE3 and scrypt implementations
- @noble/ciphers - Industry-standard AES-GCM encryption
- Vue.js - Reactive framework foundation
- fflate - Fast, reliable compression
- IndexedDB - Browser-native storage
π Security Notice: HashFS provides strong cryptographic protection, but no system is perfect. Always follow security best practices and consider your specific threat model when storing sensitive data. The zero-leak design means lost passphrases cannot be recovered - keep secure backups.