Secure Secrets: Client-Side Encryption with the Web Crypto API in React js
by iamvkr on
Table of contents
- Encrypting Stuff in React with the Web Crypto API
- First Off, What Is the Web Crypto API?
- Setting Up the React App
- Behind the Scenes: cryptohandler.jsx
- Quick Gotchas
- Final code: cryptohandler.jsx
- Wrap-Up
🔐 Encrypting Stuff in React with the Web Crypto API
Ever wanted to hide some sensitive info in your React app? Maybe you’re building a password manager, or just trying to keep your super-secret to-do list encrypted from snooping eyes. Whatever your reason, let’s talk about how you can use the Web Crypto API to do that – all inside a friendly React app.
🌐 First Off, What Is the Web Crypto API?
The Web Crypto API is like your browser’s built-in security toolkit. It lets you do cryptographic operations like hashing, encrypting, decrypting, and generating keys – all using native browser code. That means no shady third-party libraries, and it’s pretty fast and secure.
🛠️ Setting Up the React App
Here’s a basic React app with two key functions: encrypting some text and then decrypting it using a password.
The React Component (App.jsx)
import { useState } from 'react'
import './App.css'
import { decryptData, encryptData } from './cryptohandler';
function App() {
const [plainText, setplainText] = useState("");
const [password, setpassword] = useState("");
const [Encrypted, setEncrypted] = useState("");
const [Decrypted, setDecrypted] = useState("");
const handleEncrypt = async () => {
const encrypted_text = await encryptData(password, plainText);
if (encrypted_text.status) {
setEncrypted(encrypted_text.encryptedText)
} else {
setEncrypted("ERROR ENC")
}
}
const handleDecrypt = async () => {
const decrepted_text = await decryptData(password, Encrypted);
if (decrepted_text.status) {
setDecrypted(decrepted_text.decryptedText)
} else {
setDecrypted("ERROR DEC")
}
}
return (
<>
<div>
{/* master password */}
<p>
<input type="text" placeholder='Password' value={password} onChange={(e) => { setpassword(e.target.value) }} />
</p>
</div>
{/* encrypt */}
<div>
<textarea name="plainInput" placeholder='Enter plain text'
value={plainText}
onChange={(e) => { setplainText(e.target.value) }}></textarea>
<p>
<button onClick={handleEncrypt}>Encrypt</button>
</p>
<p>
{Encrypted}
</p>
</div>
{/* decrypt */}
<div>
<textarea name="plainInput" placeholder='Enter cipher text' value={Encrypted}></textarea>
<p>
<button onClick={handleDecrypt}>Decrypt</button>
</p>
<p>
{Decrypted}
</p>
</div>
</>
)
}
export default App
🔑 What’s Going On?
- You type a password and some text.
- Click “Encrypt” → it scrambles your message.
- Copy-paste that encrypted gibberish and click “Decrypt” → if your password’s correct, the original message comes back!
🔍 Behind the Scenes: cryptohandler.jsx
This is where the actual crypto magic happens.
Step 1: Creating a Key from Password
We don’t use the password directly to encrypt. Instead, we turn it into a key using PBKDF2 (a fancy algorithm that makes brute-forcing harder), with a unique salt to make things even more secure.
const generateKeyFromPassword = async (password, salt) => {
const textEncoder = new TextEncoder();
const passwordKey = await window.crypto.subtle.importKey(
'raw',
textEncoder.encode(password),
{ name: 'PBKDF2' },
false,
['deriveKey']
);
return await window.crypto.subtle.deriveKey(
{
name: 'PBKDF2',
salt: salt,
iterations: 10000,
hash: 'SHA-256',
},
passwordKey,
{ name: 'AES-GCM', length: 256 },
true,
['encrypt', 'decrypt']
);
};
Step 2: Encrypting the Text
export const encryptData = async (password, plaintext) => {
...
const salt = window.crypto.getRandomValues(new Uint8Array(16));
const iv = window.crypto.getRandomValues(new Uint8Array(12));
const key = await generateKeyFromPassword(password, salt);
...
const encryptedData = await window.crypto.subtle.encrypt(
{ name: 'AES-GCM', iv: iv },
key,
textEncoder.encode(plaintext)
);
...
};
🔒 What’s Happening?
- We generate random salt and iv (initialization vector – think of it as a random start).
- We encrypt using AES-GCM, which is fast, secure, and supports authentication.
- Finally, we mash everything (salt + iv + encrypted text) together and base64 it so it can be stored/transmitted easily.
Step 3: Decryption
export const decryptData = async (password, ciphertext) => {
...
const combinedData = Uint8Array.from(atob(ciphertext), (c) => c.charCodeAt(0));
const salt = combinedData.slice(0, 16);
const iv = combinedData.slice(16, 16 + 12);
const encryptedData = combinedData.slice(28);
...
const decryptedBuffer = await window.crypto.subtle.decrypt(
{ name: 'AES-GCM', iv: iv },
key,
encryptedData
);
...
};
🔓 Decryption Logic
- We pull the salt and iv back out.
- Derive the same key from the password + salt.
- Decrypt using the key and iv. If all goes well, your original text returns!
⚠️ Quick Gotchas
- If you change the password even slightly, decryption fails.
- Web Crypto API only works in secure contexts (HTTPS or localhost).
- If you store the encrypted data, make sure you don’t lose the salt or iv (we’re handling that by bundling it all together).
Final code: cryptohandler.jsx
const generateKeyFromPassword = async (password, salt) => {
const textEncoder = new TextEncoder();
const passwordKey = await window.crypto.subtle.importKey(
'raw',
textEncoder.encode(password),
{ name: 'PBKDF2' },
false,
['deriveKey']
);
return await window.crypto.subtle.deriveKey(
{
name: 'PBKDF2',
salt: salt,
iterations: 10000,
hash: 'SHA-256',
},
passwordKey,
{ name: 'AES-GCM', length: 256 },
true,
['encrypt', 'decrypt']
);
};
export const encryptData = async (password, plaintext) => {
if (!password || !plaintext) {
return {
status: false,
message: 'Please enter a password and plaintext.'
};
}
try {
const salt = window.crypto.getRandomValues(new Uint8Array(16));
const iv = window.crypto.getRandomValues(new Uint8Array(12));
const key = await generateKeyFromPassword(password, salt);
const textEncoder = new TextEncoder();
const data = textEncoder.encode(plaintext);
const encryptedData = await window.crypto.subtle.encrypt(
{ name: 'AES-GCM', iv: iv },
key,
data
);
// Combine salt, IV, and ciphertext for storage/transmission
const combinedData = new Uint8Array([...salt, ...iv, ...new Uint8Array(encryptedData)]);
const base64Ciphertext = btoa(String.fromCharCode(...combinedData));
return {
status: true,
encryptedText: base64Ciphertext
};
} catch (e) {
return {
status: false,
message: `Encryption error: ${e.message}`
};
}
};
export const decryptData = async (password, ciphertext) => {
if (!password || !ciphertext) {
return {
status: false,
message: 'Please enter a password and ciphertext.'
}
}
try {
const combinedData = Uint8Array.from(atob(ciphertext), (c) => c.charCodeAt(0));
const salt = combinedData.slice(0, 16);
const iv = combinedData.slice(16, 16 + 12);
const encryptedData = combinedData.slice(16 + 12);
const key = await generateKeyFromPassword(password, salt);
const decryptedBuffer = await window.crypto.subtle.decrypt(
{ name: 'AES-GCM', iv: iv },
key,
encryptedData
);
const textDecoder = new TextDecoder();
const decodedTxt = textDecoder.decode(decryptedBuffer);
return {
status: true,
decryptedText: decodedTxt
}
} catch (e) {
return {
status: false,
message: `Encryption error: ${e.message}`
};
}
};
🚀 Wrap-Up
Using the Web Crypto API in React is surprisingly straightforward once you get a hang of it. With just a few lines, you can add solid encryption to your app – no sketchy libraries needed!
So next time you need to store something private on the client side, consider giving this approach a shot. It’s secure, native, and works like magic
Hope you enjoyed reading this post. Comment your thoughts below and share it if you liked reading the post!