This content originally appeared on DEV Community and was authored by Alex Sofroniev
How I built client-side AES-256-GCM encryption for GHOSTVAULT in 72 hours
The Problem Everyone Ignores
When I started building GHOSTVAULT during the World's Largest Hackathon, I had a simple requirement: true zero-knowledge architecture. Not the marketing buzzword kind, but the real deal — where even I, the creator, cannot access user data.
After 72 hours of intense development, I learned that implementing real zero-knowledge encryption isn't just about choosing the right algorithm. It's about solving problems that crypto tutorials never mention:
- How do you derive keys securely from passwords?
- What happens when users choose weak passwords in a zero-knowledge system?
- How do you handle encryption failures gracefully?
- How do you make security user-friendly?
This series covers the real implementation challenges I faced building what became the most advanced cybersecurity platform ever created on Bolt.new.
Why Most "Zero-Knowledge" Apps Aren't
Let me be blunt: most apps claiming "zero-knowledge" are lying.
True zero-knowledge means:
- ❌ No password recovery — if you lose your key, your data is gone forever
- ❌ No server-side decryption — the server never sees unencrypted data
- ❌ No debugging user data — developers can't inspect user information
- ❌ No "forgot password" magic — there's no backdoor, period
If an app offers password recovery or customer support can "help you access your data," it's not zero-knowledge. It's security theater.
The Real Implementation: AES-256-GCM + PBKDF2
Here's what actual zero-knowledge encryption looks like in production:
class ZeroKnowledgeEncryption {
constructor() {
this.algorithm = 'AES-GCM';
this.keyLength = 256;
this.ivLength = 12;
}
async deriveKey(password, salt) {
const encoder = new TextEncoder();
const keyMaterial = await crypto.subtle.importKey(
'raw',
encoder.encode(password),
{ name: 'PBKDF2' },
false,
['deriveBits', 'deriveKey']
);
return crypto.subtle.deriveKey(
{
name: 'PBKDF2',
salt: salt,
iterations: 100000,
hash: 'SHA-256'
},
keyMaterial,
{ name: 'AES-GCM', length: 256 },
false,
['encrypt', 'decrypt']
);
}
generateSalt() {
return crypto.getRandomValues(new Uint8Array(16));
}
generateIV() {
return crypto.getRandomValues(new Uint8Array(this.ivLength));
}
async encrypt(data, key) {
const encoder = new TextEncoder();
const iv = this.generateIV();
const encrypted = await crypto.subtle.encrypt(
{
name: 'AES-GCM',
iv: iv
},
key,
encoder.encode(JSON.stringify(data))
);
const combined = new Uint8Array(iv.length + encrypted.byteLength);
combined.set(iv);
combined.set(new Uint8Array(encrypted), iv.length);
return {
data: combined,
iv: Array.from(iv)
};
}
async decrypt(encryptedData, key) {
try {
const iv = encryptedData.slice(0, this.ivLength);
const data = encryptedData.slice(this.ivLength);
const decrypted = await crypto.subtle.decrypt(
{
name: 'AES-GCM',
iv: iv
},
key,
data
);
const decoder = new TextDecoder();
return JSON.parse(decoder.decode(decrypted));
} catch (error) {
throw new Error('Decryption failed: Invalid key or corrupted data');
}
}
bufferToBase64(buffer) {
return btoa(String.fromCharCode(...new Uint8Array(buffer)));
}
base64ToBuffer(base64) {
const binary = atob(base64);
const buffer = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
buffer[i] = binary.charCodeAt(i);
}
return buffer;
}
}
Why this works:
- PBKDF2 with 100,000 iterations makes brute force attacks computationally expensive
- Random salts prevent rainbow table attacks
- AES-256-GCM provides both encryption and authentication
- Web Crypto API ensures cryptographically secure operations
The Password Security Challenge
The biggest UX challenge in zero-knowledge systems? Password strength vs usability.
Users want simple passwords. Zero-knowledge encryption demands strong ones. There's no middle ground because there's no password recovery.
My solution was building a comprehensive password strength calculator:
function calculatePasswordStrength(password) {
if (!password) return { score: 0, level: 'none', feedback: [] };
let score = 0;
const feedback = [];
// Length is most important for cryptographic security
if (password.length >= 20) {
score += 25;
} else if (password.length >= 16) {
score += 20;
} else if (password.length >= 12) {
score += 15;
} else {
feedback.push('Use at least 12 characters (20+ recommended)');
}
// Character variety checks
if (/[A-Z]/.test(password)) score += 15;
else feedback.push('Add uppercase letters');
if (/[a-z]/.test(password)) score += 15;
else feedback.push('Add lowercase letters');
if (/\d/.test(password)) score += 15;
else feedback.push('Add numbers');
if (/[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(password)) {
score += 20;
} else {
feedback.push('Add special characters');
}
// Determine security level
let level;
if (score >= 80) level = 'strong';
else if (score >= 60) level = 'good';
else if (score >= 40) level = 'fair';
else level = 'weak';
return { score, level, feedback };
}
The visual feedback component:
function PasswordStrengthIndicator({ password }) {
const strength = calculatePasswordStrength(password);
const levelColors = {
'weak': '#ff4444',
'fair': '#ffbb33',
'good': '#00C851',
'strong': '#007E33'
};
return (
<div className="password-strength">
<div className="strength-bars">
{[20, 40, 60, 80].map((threshold, index) => (
<div
key={index}
className={`strength-bar ${strength.score >= threshold ? 'active' : ''}`}
style={{
backgroundColor: strength.score >= threshold
? levelColors[strength.level]
: '#e0e0e0'
}}
/>
))}
</div>
<div className="strength-text">
{strength.level.charAt(0).toUpperCase() + strength.level.slice(1)}
</div>
{strength.feedback.length > 0 && (
<ul className="strength-feedback">
{strength.feedback.map((tip, index) => (
<li key={index}>{tip}</li>
))}
</ul>
)}
</div>
);
}
React Implementation: The Real Demo
Here's how the actual encryption flow works in my demo:
function App() {
const [password, setPassword] = useState('');
const [dataToEncrypt, setDataToEncrypt] = useState('');
const [encryptedResult, setEncryptedResult] = useState('');
const [decryptedResult, setDecryptedResult] = useState('');
const [currentSalt, setCurrentSalt] = useState(null);
const [isProcessing, setIsProcessing] = useState(false);
const [error, setError] = useState('');
const crypto = new ZeroKnowledgeEncryption();
const handleEncrypt = useCallback(async () => {
if (!password || !dataToEncrypt) {
setError('Please enter both password and data to encrypt');
return;
}
setIsProcessing(true);
setError('');
try {
// Generate fresh salt for this encryption session
const salt = crypto.generateSalt();
setCurrentSalt(salt);
// Derive key from password
const key = await crypto.deriveKey(password, salt);
// Encrypt the data
const result = await crypto.encrypt({
message: dataToEncrypt,
timestamp: Date.now(),
type: 'demo_data'
}, key);
// Convert to base64 for display
const base64Result = crypto.bufferToBase64(result.data);
const saltBase64 = crypto.bufferToBase64(salt);
setEncryptedResult(JSON.stringify({
encrypted: base64Result,
salt: saltBase64,
iv: result.iv
}, null, 2));
} catch (err) {
setError('Encryption failed: ' + err.message);
} finally {
setIsProcessing(false);
}
}, [password, dataToEncrypt]);
// Rest of component...
}
Key insights from this implementation:
- Fresh salt per encryption - Never reuse salts, even for the same user
- No persistent key storage - Keys are derived on-demand from passwords
- Explicit error handling - Clear feedback when crypto operations fail
- Base64 encoding - Makes binary data human-readable for debugging
Performance at Scale: Real Numbers
GHOSTVAULT encrypts breach data for 50+ users daily. Here are the real performance metrics:
- Key derivation: ~200ms (PBKDF2 with 100K iterations)
- Encryption rate: ~500 operations/second (small records)
- Browser compatibility: 98% (Web Crypto API support)
- Failure rate: <0.1% (mostly weak passwords)
The Complete Working Demo
I've built a complete demo that shows real zero-knowledge encryption in action:
Security Considerations You Can't Ignore
1. Never Store Keys
// NEVER do this
localStorage.setItem('encryptionKey', key);
// CORRECT - derive keys on-demand
const key = await deriveKey(password, storedSalt);
2. Handle Browser Differences
if (errorMessage.includes('crypto') || errorMessage.includes('subtle')) {
return createError(
ERROR_TYPES.BROWSER_SUPPORT,
'Browser does not support required cryptographic features',
'Please use a modern browser with Web Crypto API support (Chrome, Firefox, Safari, Edge).'
);
}
3. Secure Random Generation
// Use cryptographically secure random
generateSalt() {
return crypto.getRandomValues(new Uint8Array(16));
}
generateIV() {
return crypto.getRandomValues(new Uint8Array(this.ivLength));
}
The UX Challenge: Making Security Usable
The biggest challenge isn't technical—it's making users understand the trade-offs:
const PasswordWarning = ({ password }) => {
const strength = validatePassword(password);
return (
<div className="password-warning">
{!strength.isValid && (
<div className="warning">
⚠️ If you forget this password, your data is PERMANENTLY lost.
We cannot recover it for you.
</div>
)}
<div className="strength-meter">
Strength: {strength.strength}/5
</div>
</div>
);
};
What's Next
This is Part 1 of a 4-part series on implementing zero-knowledge systems:
- Part 1: Client-side AES-256-GCM encryption ← You are here
- Part 2: Memory management with large datasets
- Part 3: Zero-Knowledge Authentication & Multi-Device Sync
- Part 4: Encrypted search that actually works
The Bottom Line
Real zero-knowledge encryption is unforgiving. There's no password recovery, no customer support magic, and no shortcuts.
But when done right, it creates something powerful: software that protects users even from its own creators.
GHOSTVAULT proves this is possible. Built in 72 hours, serving real users, processing real data — all while maintaining true zero-knowledge architecture.
The key is starting with solid crypto fundamentals and building user experience on top of uncompromising security.
Building GHOSTVAULT taught me that the most important security feature isn't what you can do — it's what you can't do. True zero-knowledge means giving up control to give users privacy.
Want to see the complete code? The full working demo with all source code is available in my GitHub repo. Star it if this helped you understand real zero-knowledge implementation.
→ Get the Complete Source Code
Questions about the implementation? Drop them in the comments. I'll be writing detailed follow-ups based on what people want to know most.
This content originally appeared on DEV Community and was authored by Alex Sofroniev

Alex Sofroniev | Sciencx (2025-07-04T13:12:13+00:00) Part 1: Building Zero-Knowledge Encryption That Actually Works. Retrieved from https://www.scien.cx/2025/07/04/part-1-building-zero-knowledge-encryption-that-actually-works/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.