Skip to main content Link Search Menu Expand Document (external link)
async function generateECDHKeyPair(): Promise<CryptoKeyPair> {
    // Generate an ECDH key pair using the P-256 curve
    return await crypto.subtle.generateKey(
        {
            name: "ECDH",
            namedCurve: "P-256", // Can also use P-384, P-521
        },
        true, // Extractable (true) means the key can be exported
        ["deriveKey"] // The key is usable for deriving keys
    );
}

async function deriveAESKey(privateKey: CryptoKey, publicKey: CryptoKey): Promise<CryptoKey> {
    // Derive the shared secret using ECDH
    return await crypto.subtle.deriveKey(
        {
            name: "ECDH",
            public: publicKey, // The public key of the other party
        },
        privateKey, // Your own private key
        {
            name: "AES-GCM", // Specify AES-GCM for the derived key
            length: 256, // Use AES-256
        },
        true, // Extractable (true) means the key can be exported
        ["encrypt", "decrypt"] // The key is usable for encryption and decryption
    );
}


// Function to pad plaintext to be a multiple of the block size (16 bytes)
function padText(text: Uint8Array): Uint8Array {
    const blockSize = 16;
    const paddingLength = blockSize - (text.length % blockSize);
    const padding = new Uint8Array(paddingLength).fill(paddingLength);
    return new Uint8Array([...text, ...padding]);
}

// Function to remove padding from decrypted text
function unpadText(text: Uint8Array): Uint8Array {
    const paddingLength = text[text.length - 1];
    return text.slice(0, -paddingLength);
}

async function encryptAES(plaintext: string, privateKey: CryptoKey, publicKey: CryptoKey): Promise<string> {
    // Convert the plaintext to ArrayBuffer
    const plaintextBuffer = new TextEncoder().encode(plaintext);

    // Derive the AES key
    const cryptoKey = await deriveAESKey(privateKey, publicKey);

    // Generate a random 12-byte IV (AES-GCM standard)
    const iv = crypto.getRandomValues(new Uint8Array(12));

    // Encrypt the data using AES-GCM
    const encryptedBuffer = await crypto.subtle.encrypt(
        {
            name: 'AES-GCM',
            iv: iv,
        },
        cryptoKey,
        plaintextBuffer
    );

    // Convert the encrypted data and IV to base64
    const encryptedData = btoa(String.fromCharCode(...new Uint8Array(encryptedBuffer)));
    const ivBase64 = btoa(String.fromCharCode(...iv));

    // Return the IV and encrypted data concatenated with a colon
    return `${ivBase64}:${encryptedData}`;
}


async function decryptAES(encryptedData: string, privateKey: CryptoKey, publicKey: CryptoKey): Promise<string> {
    // Split the input into IV and encrypted data
    const [ivBase64, encryptedBase64] = encryptedData.split(':');

    // Decode the base64 strings
    const iv = new Uint8Array(atob(ivBase64).split('').map(c => c.charCodeAt(0)));
    const encryptedBuffer = new Uint8Array(atob(encryptedBase64).split('').map(c => c.charCodeAt(0)));

    // Derive the AES key
    const cryptoKey = await deriveAESKey(privateKey, publicKey);

    // Decrypt the data using AES-GCM
    const decryptedBuffer = await crypto.subtle.decrypt(
        {
            name: 'AES-GCM',
            iv: iv,
        },
        cryptoKey,
        encryptedBuffer
    );

    // Convert the decrypted data from ArrayBuffer to string
    return new TextDecoder().decode(decryptedBuffer);
}


// Example usage
(async () => {
    const clientKeyPair = await generateECDHKeyPair();
    const publicKeyFromServer = /* Obtain server public key as CryptoKey */;
    const plaintext = 'Hello, this is a secret message!';

    try {
        const encrypted = await encryptAES(plaintext, clientKeyPair.privateKey, publicKeyFromServer);
        console.log('Encrypted:', encrypted);

        const decrypted = await decryptAES(encrypted, clientKeyPair.privateKey, publicKeyFromServer);
        console.log('Decrypted:', decrypted);
    } catch (error) {
        console.error('Error:', error);
    }
})();