AES Text Encryption ToolsteemCreated with Sketch.

in #coding4 days ago

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.

2026-08-15_133314.png

<!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.&lt;Base64URL&gt;</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:  
Loading...
Loading...