AES Text Encryption Tool
A simple browser-based tool for secure text encryption and decryption. Uses PBKDF2-SHA-256 and AES-256-GCM without storing passwords or encryption keys.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Text Encryption Tool</title>
<meta name="description" content="Local browser-based text encryption using PBKDF2-SHA-256 and AES-256-GCM.">
<style>
* {
box-sizing: border-box;
}
body {
margin: 0;
padding: 24px;
font-family: Arial, sans-serif;
background: #f4f4f4;
color: #222;
}
#main {
width: 100%;
max-width: 760px;
margin: 0 auto;
background: #fff;
border-radius: 12px;
box-shadow: 0 4px 18px rgba(0, 0, 0, 0.08);
overflow: hidden;
}
header {
padding: 24px 24px 18px;
border-bottom: 1px solid #e6e6e6;
}
h1 {
margin: 0 0 8px;
font-size: 26px;
}
h2 {
margin: 0 0 18px;
font-size: 21px;
}
p {
margin: 6px 0;
line-height: 1.5;
}
.note {
font-size: 14px;
color: #555;
}
.warning {
margin-top: 12px;
padding: 10px 12px;
background: #fff8e5;
border: 1px solid #eed995;
border-radius: 6px;
font-size: 14px;
line-height: 1.45;
}
section {
padding: 24px;
}
section + section {
border-top: 1px solid #e6e6e6;
}
label {
display: block;
margin: 14px 0 6px;
font-weight: 600;
}
input,
textarea {
width: 100%;
border: 1px solid #c9c9c9;
border-radius: 6px;
padding: 11px 12px;
font: inherit;
background: #fff;
color: #222;
transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
input:focus,
textarea:focus {
outline: none;
border-color: #4d7fc7;
box-shadow: 0 0 0 3px rgba(77, 127, 199, 0.13);
}
textarea {
min-height: 150px;
resize: vertical;
line-height: 1.4;
}
textarea[readonly] {
background: #fafafa;
}
.password-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.show-password {
display: flex;
align-items: center;
gap: 8px;
margin-top: 10px;
font-size: 14px;
color: #444;
user-select: none;
}
.show-password input {
width: auto;
margin: 0;
}
.buttons {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 14px;
}
button {
border: 0;
border-radius: 6px;
padding: 10px 17px;
font: inherit;
font-weight: 600;
cursor: pointer;
background: #2367b1;
color: #fff;
}
button.secondary {
background: #666;
}
button.ghost {
background: #e9edf2;
color: #222;
}
button:hover:not(:disabled) {
filter: brightness(0.94);
}
button:disabled {
opacity: 0.55;
cursor: default;
}
.status {
display: none;
margin-top: 14px;
padding: 10px 12px;
border-radius: 6px;
font-size: 14px;
line-height: 1.4;
}
.status.success {
display: block;
background: #eef8ef;
border: 1px solid #b9dfbd;
color: #205b27;
}
.status.error {
display: block;
background: #fff0f0;
border: 1px solid #e4b5b5;
color: #812525;
}
footer {
padding: 16px 24px 22px;
border-top: 1px solid #e6e6e6;
font-size: 13px;
color: #666;
line-height: 1.5;
}
code {
font-family: Consolas, monospace;
}
@media (max-width: 620px) {
body {
padding: 8px;
}
header,
section,
footer {
padding-left: 16px;
padding-right: 16px;
}
.password-row {
grid-template-columns: 1fr;
gap: 0;
}
button {
flex: 1 1 auto;
}
}
</style>
</head>
<body>
<div id="main">
<header>
<h1>Text Encryption Tool</h1>
<p class="note">Encryption and decryption are performed locally in your browser. No data is sent anywhere.</p>
<div class="warning">
Keep your password safe. If you lose it, the encrypted data cannot be recovered.
</div>
</header>
<section>
<h2>Encrypt text</h2>
<div class="password-row">
<div>
<label for="encryptPassword">Password</label>
<input id="encryptPassword" type="password" autocomplete="off" spellcheck="false" placeholder="Enter password">
</div>
<div>
<label for="encryptPasswordConfirm">Confirm password</label>
<input id="encryptPasswordConfirm" type="password" autocomplete="off" spellcheck="false" placeholder="Repeat password">
</div>
</div>
<label class="show-password">
<input id="showEncryptPasswords" type="checkbox">
Show passwords
</label>
<label for="plainText">Text to encrypt</label>
<textarea id="plainText" placeholder="Enter text to encrypt"></textarea>
<label for="encryptedResult">Encrypted result</label>
<textarea id="encryptedResult" readonly spellcheck="false" placeholder="Encrypted data will appear here"></textarea>
<div class="buttons">
<button id="encryptBtn" type="button">Encrypt</button>
<button id="copyEncryptedBtn" class="secondary" type="button">Copy result</button>
<button id="clearEncryptBtn" class="ghost" type="button">Clear</button>
</div>
<div id="encryptStatus" class="status" role="status" aria-live="polite"></div>
</section>
<section>
<h2>Decrypt text</h2>
<label for="decryptPassword">Password</label>
<input id="decryptPassword" type="password" autocomplete="off" spellcheck="false" placeholder="Enter password">
<label class="show-password">
<input id="showDecryptPassword" type="checkbox">
Show password
</label>
<label for="encryptedText">Encrypted data</label>
<textarea id="encryptedText" spellcheck="false" placeholder="Paste ENC1 encrypted data here"></textarea>
<label for="decryptedResult">Decrypted text</label>
<textarea id="decryptedResult" readonly placeholder="Decrypted text will appear here"></textarea>
<div class="buttons">
<button id="decryptBtn" type="button">Decrypt</button>
<button id="copyDecryptedBtn" class="secondary" type="button">Copy result</button>
<button id="clearDecryptBtn" class="ghost" type="button">Clear</button>
</div>
<div id="decryptStatus" class="status" role="status" aria-live="polite"></div>
</section>
<footer>
Format: <code>ENC1.<Base64URL></code><br>
ENC1 uses PBKDF2-HMAC-SHA-256 and AES-256-GCM with a 128-bit authentication tag. The salt, IV and PBKDF2 iteration count are stored inside the encrypted package.
</footer>
</div>
<script>
'use strict';
const FORMAT_PREFIX = 'ENC1';
const PBKDF2_ITERATIONS = 600000;
const MIN_ACCEPTED_ITERATIONS = 100000;
const MAX_ACCEPTED_ITERATIONS = 10000000;
const SALT_LENGTH = 16;
const IV_LENGTH = 12;
const GCM_TAG_LENGTH = 128;
const MIN_PASSWORD_LENGTH = 4;
const textEncoder = new TextEncoder();
const textDecoder = new TextDecoder('utf-8', { fatal: true });
function ensureCryptoAvailable() {
if (!window.crypto || !window.crypto.subtle || !window.crypto.getRandomValues) {
throw new Error('Web Crypto API is not available in this browser.');
}
}
function setStatus(element, message, type) {
element.textContent = message;
element.className = 'status ' + type;
}
function clearStatus(element) {
element.textContent = '';
element.className = 'status';
}
function uint32ToBytes(value) {
const bytes = new Uint8Array(4);
new DataView(bytes.buffer).setUint32(0, value, false);
return bytes;
}
function bytesToUint32(bytes, offset) {
return new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getUint32(offset, false);
}
function concatBytes(...arrays) {
const totalLength = arrays.reduce((sum, array) => sum + array.length, 0);
const result = new Uint8Array(totalLength);
let offset = 0;
for (const array of arrays) {
result.set(array, offset);
offset += array.length;
}
return result;
}
function bytesToBase64Url(bytes) {
let binary = '';
const chunkSize = 0x8000;
for (let i = 0; i < bytes.length; i += chunkSize) {
binary += String.fromCharCode(...bytes.subarray(i, i + chunkSize));
}
return btoa(binary)
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/g, '');
}
function base64UrlToBytes(value) {
if (!/^[A-Za-z0-9_-]+$/.test(value)) {
throw new Error('Encrypted data contains invalid characters.');
}
let base64 = value.replace(/-/g, '+').replace(/_/g, '/');
const remainder = base64.length % 4;
if (remainder === 1) {
throw new Error('Encrypted data has an invalid Base64URL length.');
}
if (remainder > 0) {
base64 += '='.repeat(4 - remainder);
}
let binary;
try {
binary = atob(base64);
} catch {
throw new Error('Encrypted data is not valid Base64URL.');
}
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes;
}
function normalizeEncryptedInput(value) {
// Only whitespace is removed automatically. Any other unexpected
// character is treated as an error instead of being silently deleted.
return value.replace(/\s+/g, '');
}
function makeAdditionalData(iterations) {
return concatBytes(
textEncoder.encode(FORMAT_PREFIX),
uint32ToBytes(iterations)
);
}
async function deriveAesKey(password, salt, iterations) {
const passwordKey = await crypto.subtle.importKey(
'raw',
textEncoder.encode(password),
'PBKDF2',
false,
['deriveKey']
);
return crypto.subtle.deriveKey(
{
name: 'PBKDF2',
salt: salt,
iterations: iterations,
hash: 'SHA-256'
},
passwordKey,
{
name: 'AES-GCM',
length: 256
},
false,
['encrypt', 'decrypt']
);
}
async function encryptText(plainText, password) {
ensureCryptoAvailable();
const salt = crypto.getRandomValues(new Uint8Array(SALT_LENGTH));
const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH));
const iterations = PBKDF2_ITERATIONS;
const key = await deriveAesKey(password, salt, iterations);
const additionalData = makeAdditionalData(iterations);
const encryptedBuffer = await crypto.subtle.encrypt(
{
name: 'AES-GCM',
iv: iv,
additionalData: additionalData,
tagLength: GCM_TAG_LENGTH
},
key,
textEncoder.encode(plainText)
);
// Binary ENC1 payload:
// 4 bytes : PBKDF2 iteration count, unsigned big-endian
// 16 bytes : random salt
// 12 bytes : random AES-GCM IV
// remaining: ciphertext + 16-byte GCM authentication tag
const payload = concatBytes(
uint32ToBytes(iterations),
salt,
iv,
new Uint8Array(encryptedBuffer)
);
return FORMAT_PREFIX + '.' + bytesToBase64Url(payload);
}
async function decryptText(packageText, password) {
ensureCryptoAvailable();
const normalized = normalizeEncryptedInput(packageText);
const expectedPrefix = FORMAT_PREFIX + '.';
if (!normalized.startsWith(expectedPrefix)) {
throw new Error('Unsupported or invalid encrypted data format. Expected ENC1.');
}
if (!/^ENC1\.[A-Za-z0-9_-]+$/.test(normalized)) {
throw new Error('Encrypted data contains invalid characters.');
}
const encodedPayload = normalized.slice(expectedPrefix.length);
const payload = base64UrlToBytes(encodedPayload);
const minimumPayloadLength = 4 + SALT_LENGTH + IV_LENGTH + (GCM_TAG_LENGTH / 8);
if (payload.length < minimumPayloadLength) {
throw new Error('Encrypted data is incomplete or damaged.');
}
const iterations = bytesToUint32(payload, 0);
if (iterations < MIN_ACCEPTED_ITERATIONS || iterations > MAX_ACCEPTED_ITERATIONS) {
throw new Error('Encrypted data contains an unsupported PBKDF2 iteration count.');
}
const saltStart = 4;
const ivStart = saltStart + SALT_LENGTH;
const cipherStart = ivStart + IV_LENGTH;
const salt = payload.slice(saltStart, ivStart);
const iv = payload.slice(ivStart, cipherStart);
const ciphertext = payload.slice(cipherStart);
const key = await deriveAesKey(password, salt, iterations);
const additionalData = makeAdditionalData(iterations);
let decryptedBuffer;
try {
decryptedBuffer = await crypto.subtle.decrypt(
{
name: 'AES-GCM',
iv: iv,
additionalData: additionalData,
tagLength: GCM_TAG_LENGTH
},
key,
ciphertext
);
} catch {
throw new Error('Decryption failed. Check the password and encrypted data.');
}
try {
return textDecoder.decode(decryptedBuffer);
} catch {
throw new Error('Decryption succeeded, but the result is not valid UTF-8 text.');
}
}
async function copyText(value) {
if (!value) {
throw new Error('There is nothing to copy.');
}
if (navigator.clipboard && window.isSecureContext) {
await navigator.clipboard.writeText(value);
return;
}
const temp = document.createElement('textarea');
temp.value = value;
temp.setAttribute('readonly', '');
temp.style.position = 'fixed';
temp.style.opacity = '0';
temp.style.pointerEvents = 'none';
document.body.appendChild(temp);
temp.select();
const copied = document.execCommand('copy');
document.body.removeChild(temp);
if (!copied) {
throw new Error('Copy failed. Select the text and copy it manually.');
}
}
function setButtonBusy(button, busy, busyText, normalText) {
button.disabled = busy;
button.textContent = busy ? busyText : normalText;
}
const encryptPassword = document.getElementById('encryptPassword');
const encryptPasswordConfirm = document.getElementById('encryptPasswordConfirm');
const plainText = document.getElementById('plainText');
const encryptedResult = document.getElementById('encryptedResult');
const encryptBtn = document.getElementById('encryptBtn');
const encryptStatus = document.getElementById('encryptStatus');
const decryptPassword = document.getElementById('decryptPassword');
const encryptedTextInput = document.getElementById('encryptedText');
const decryptedResult = document.getElementById('decryptedResult');
const decryptBtn = document.getElementById('decryptBtn');
const decryptStatus = document.getElementById('decryptStatus');
document.getElementById('showEncryptPasswords').addEventListener('change', function () {
const type = this.checked ? 'text' : 'password';
encryptPassword.type = type;
encryptPasswordConfirm.type = type;
});
document.getElementById('showDecryptPassword').addEventListener('change', function () {
decryptPassword.type = this.checked ? 'text' : 'password';
});
encryptBtn.addEventListener('click', async function () {
clearStatus(encryptStatus);
encryptedResult.value = '';
const password = encryptPassword.value;
const confirmation = encryptPasswordConfirm.value;
const text = plainText.value;
if (!text) {
setStatus(encryptStatus, 'Enter text to encrypt.', 'error');
return;
}
if (!password) {
setStatus(encryptStatus, 'Enter a password.', 'error');
return;
}
if (password.length < MIN_PASSWORD_LENGTH) {
setStatus(encryptStatus, 'Use a password of at least ' + MIN_PASSWORD_LENGTH + ' characters.', 'error');
return;
}
if (password !== confirmation) {
setStatus(encryptStatus, 'The passwords do not match.', 'error');
return;
}
setButtonBusy(encryptBtn, true, 'Encrypting...', 'Encrypt');
try {
encryptedResult.value = await encryptText(text, password);
setStatus(encryptStatus, 'Encryption completed successfully.', 'success');
} catch (error) {
setStatus(encryptStatus, error.message || 'Encryption failed.', 'error');
} finally {
setButtonBusy(encryptBtn, false, 'Encrypting...', 'Encrypt');
}
});
decryptBtn.addEventListener('click', async function () {
clearStatus(decryptStatus);
decryptedResult.value = '';
const password = decryptPassword.value;
const packageText = encryptedTextInput.value;
if (!packageText.trim()) {
setStatus(decryptStatus, 'Paste encrypted data first.', 'error');
return;
}
if (!password) {
setStatus(decryptStatus, 'Enter the password.', 'error');
return;
}
setButtonBusy(decryptBtn, true, 'Decrypting...', 'Decrypt');
try {
decryptedResult.value = await decryptText(packageText, password);
setStatus(decryptStatus, 'Decryption completed successfully.', 'success');
} catch (error) {
setStatus(decryptStatus, error.message || 'Decryption failed.', 'error');
} finally {
setButtonBusy(decryptBtn, false, 'Decrypting...', 'Decrypt');
}
});
document.getElementById('copyEncryptedBtn').addEventListener('click', async function () {
clearStatus(encryptStatus);
try {
await copyText(encryptedResult.value);
setStatus(encryptStatus, 'Encrypted data copied to the clipboard.', 'success');
} catch (error) {
setStatus(encryptStatus, error.message, 'error');
}
});
document.getElementById('copyDecryptedBtn').addEventListener('click', async function () {
clearStatus(decryptStatus);
try {
await copyText(decryptedResult.value);
setStatus(decryptStatus, 'Decrypted text copied to the clipboard.', 'success');
} catch (error) {
setStatus(decryptStatus, error.message, 'error');
}
});
document.getElementById('clearEncryptBtn').addEventListener('click', function () {
encryptPassword.value = '';
encryptPasswordConfirm.value = '';
plainText.value = '';
encryptedResult.value = '';
clearStatus(encryptStatus);
encryptPassword.focus();
});
document.getElementById('clearDecryptBtn').addEventListener('click', function () {
decryptPassword.value = '';
encryptedTextInput.value = '';
decryptedResult.value = '';
clearStatus(decryptStatus);
decryptPassword.focus();
});
try {
ensureCryptoAvailable();
} catch (error) {
setStatus(encryptStatus, error.message, 'error');
setStatus(decryptStatus, error.message, 'error');
encryptBtn.disabled = true;
decryptBtn.disabled = true;
}
</script>
</body>
</html>
Sort: Trending
Loading...
Loading...
