javascriptnode.jsbrowsercryptographywebcrypto-api

Implementing encryption/decryption in Typescript using Web Crypto API


I'm having a security requirement not to send data as plain-text to the server and the credentials shouldn't be visible in the network tab of the browser. I could have used hashing but the catch is we already have old stored passwords with hash and salting applied. Now, not to disturb the old database, I was thinking to simply encrypt the data and decrypt it on server side (node).

Henceforth, I'm trying to use Web Crypto API to achieve the desired outcome. Below is a simple script I was trying to write before exporting the logic to both client and server environments :

function base64ToArrayBuffer(base64): ArrayBuffer {
    return Uint8Array.from(atob(base64), c => c.charCodeAt(0))
}

function ArrayBufferToBase64(buffer): string {
    return btoa(String.fromCharCode(...new Uint8Array(buffer)));
}

async function encryptString(privateKey: string, publicKey: string, data: string) {

    let params: AesGcmParams = {
        name: 'AES-GCM',
        iv: base64ToArrayBuffer(publicKey)
    }

    let keyImported: CryptoKey = await window.crypto.subtle.importKey("raw", base64ToArrayBuffer(privateKey), "AES-GCM", true, ["encrypt", "decrypt"]);
    let dataInstance: ArrayBuffer = new TextEncoder().encode(data);
    return Promise.resolve(await window.crypto.subtle.encrypt(params, keyImported, dataInstance));
}

async function decryptString(privateKey: string, publicKey: string, data: string) {

    let params: AesGcmParams = {
        name: 'AES-GCM',
        iv: base64ToArrayBuffer(publicKey)
    }

    let keyImported: CryptoKey = await window.crypto.subtle.importKey("raw", base64ToArrayBuffer(privateKey), "AES-GCM", true, ["encrypt", "decrypt"]);
    let dataInstance: ArrayBuffer = new TextEncoder().encode(data);
    return Promise.resolve(await window.crypto.subtle.decrypt(params, keyImported, dataInstance));
}

// Key for encryption/decryption process
let PRIVATE_KEY: string = 'Fdz2z54vAwIjq0tginHyzgyL62q+f4sBlMi5IOOXJ7c=';
let PUBLIC_KEY: string = btoa(String.fromCharCode(...window.crypto.getRandomValues(new Uint8Array(12))));

// Password to be encrypted before being sent to server
let password: string = "Test@123";

(async () => {

    let encrypted_data: string = ArrayBufferToBase64(await encryptString(PRIVATE_KEY, PUBLIC_KEY, password));
    console.log(encrypted_data);

    let decrypted_data: string = ArrayBufferToBase64(await decryptString(PRIVATE_KEY, PUBLIC_KEY, encrypted_data));
    console.log(decrypted_data);

})();


I'm able to encrypt the string stored by the password variable but not able to decrypt it.
Getting following error message :

PromiseĀ {<pending>}
VM98:33 FSWTBSgdQNVgYOelgRpU5JZm/4G0TRynIyr2jg==
VM98:36 Uncaught (in promise) Error
await (async)
(anonymous) @ VM98:36

Also, attaching screenshot for details not pasted under logs: enter image description here

Need help to understand the root cause of this error alongwith the fix I can add to the script to successfully encrypt/decrypt some data.

Incase if someone's aware of a library that I can easily use on both client/server side, that would also be appreciated as an answer.


Solution

  • Below is the script that worked for me as per inputs provided from the community:

    
    
        function base64ToArrayBuffer(base64: string): ArrayBuffer {
            return Uint8Array.from(atob(base64), c => c.charCodeAt(0))
        }
        
        function ArrayBufferToBase64(buffer: ArrayBuffer): string {
            return btoa(String.fromCharCode(...new Uint8Array(buffer)));
        }
        
        async function encryptString(privateKey: string, publicKey: string, data: string) {
        
            let params: AesGcmParams = {
                name: 'AES-GCM',
                iv: base64ToArrayBuffer(publicKey)
            }
        
            let keyImported: CryptoKey = await window.crypto.subtle.importKey("raw", base64ToArrayBuffer(privateKey), "AES-GCM", true, ["encrypt", "decrypt"]);
            let dataInstance: ArrayBuffer = new TextEncoder().encode(data);
            return window.crypto.subtle.encrypt(params, keyImported, dataInstance);
        }
        
        async function decryptString(privateKey: string, publicKey: string, data: string) {
        
            let params: AesGcmParams = {
                name: 'AES-GCM',
                iv: base64ToArrayBuffer(publicKey)
            }
        
            let keyImported: CryptoKey = await window.crypto.subtle.importKey("raw", base64ToArrayBuffer(privateKey), "AES-GCM", true, ["encrypt", "decrypt"]);
            return window.crypto.subtle.decrypt(params, keyImported, base64ToArrayBuffer(data));
        }
        
        // Key for encryption/decryption process
        let PRIVATE_KEY: string = 'Fdz2z54vAwIjq0tginHyzgyL62q+f4sBlMi5IOOXJ7c=';
        let PUBLIC_KEY: string = btoa(String.fromCharCode(...window.crypto.getRandomValues(new Uint8Array(12))));
        
        // Password to be encrypted before being sent to server
        let password: string = "Test@123";
        
        (async () => {
        
            let encrypted_data: string = ArrayBufferToBase64(await encryptString(PRIVATE_KEY, PUBLIC_KEY, password));
            console.log(encrypted_data);
        
            let decrypted_data: any = new TextDecoder().decode(await decryptString(PRIVATE_KEY, PUBLIC_KEY, encrypted_data));
            console.log(decrypted_data);
        
        })();