encryptionblazorprogressive-web-appswebassembly

how encrypt on blazor wasm wpa using Aes and Rfc2898


I'm not good at cryptography. I received this code used on server rest for cripting a string and I need to replicate it on my blazor wasm pwa.

the key, for example: key = "4fh!31ay*36S#@w!$%";

public string PasswordEncrypt(string inText, string key)
{
    byte[] bytesBuff = Encoding.Unicode.GetBytes(inText);
    using (Aes aes = Aes.Create())
    {
        var crypto = new Rfc2898DeriveBytes(key, new byte[] { 0x43, 0x71, 0x61, 0x6E, 0x20, 0x4D, 0x65, 0x64, 0x76, 0x65, 0x64, 0x65, 0x76 });
        aes.Key = crypto.GetBytes(32);
        aes.IV = crypto.GetBytes(16);
        using (MemoryStream mStream = new MemoryStream())
        {
            using (CryptoStream cStream = new CryptoStream(mStream, aes.CreateEncryptor(), CryptoStreamMode.Write))
            {
                cStream.Write(bytesBuff, 0, bytesBuff.Length);
                cStream.Close();
            }
            inText = Convert.ToBase64String(mStream.ToArray());
        }
    }
    return inText;
}

But on blazor wasm pwa the Aes and the Rfc2898DeriveBytes not are implemented. So I need to translate using javascript or the Crypto library or wathever.

I tryed to do with javascript this but I don't receive the same result.

// wwwroot/js/cryptoHelper.js

async function EncryptText(inText, key) {
    const encoder = new TextEncoder();
    const data = encoder.encode(inText);
    const salt = new Uint8Array([0x43, 0x71, 0x61, 0x6E, 0x20, 0x4D, 0x65, 0x64, 0x76, 0x65, 0x64, 0x65, 0x76]);
    const iterations = 1000;
    const keyMaterial = await window.crypto.subtle.importKey(
        'raw',
        encoder.encode(key),
        { name: 'PBKDF2' },
        false,
        ['deriveKey']
    );

    const derivedKey = await window.crypto.subtle.deriveKey(
        {
            name: 'PBKDF2',
            salt: salt,
            iterations: iterations,
            hash: 'SHA-256'
        },
        keyMaterial,
        { name: 'AES-CBC', length: 256 },
        false,
        ['encrypt']
    );

    const iv = new Uint8Array(16);
    window.crypto.getRandomValues(iv);

    const encrypted = await window.crypto.subtle.encrypt(
        {
            name: 'AES-CBC',
            iv: iv
        },
        derivedKey,
        data
    );

    const encryptedArray = new Uint8Array(encrypted);
    const ivBase64 = btoa(String.fromCharCode(...iv));
    const encryptedBase64 = btoa(String.fromCharCode(...encryptedArray));

    return `${ivBase64}:${encryptedBase64}`;
}

What I'm making wrong? Thanks


Solution

  • The JavaScript code differs from the C# code in three respects:

    The posted JavaScript code can be adapted e.g. as follows so that it is compatible with the C# code:

    async function EncryptText(inText, key) {
        const encoder = new TextEncoder();
        const data = encodeUtf16le(inText);   
        const salt = new Uint8Array([0x43, 0x71, 0x61, 0x6E, 0x20, 0x4D, 0x65, 0x64, 0x76, 0x65, 0x64, 0x65, 0x76]);
        const iterations = 1000;
        const keyMaterial = await window.crypto.subtle.importKey(
            'raw',
            encoder.encode(key),
            { name: 'PBKDF2' },
            false,
            ['deriveBits']   
        );
    
        const derivedBits = await window.crypto.subtle.deriveBits(
            {
                name: 'PBKDF2',
                salt: salt,
                iterations: iterations,
                hash: 'SHA-1'
            },
            keyMaterial,
            256 + 128
        );
        const rawKey = derivedBits.slice( 0, 32 );
        const iv = derivedBits.slice( 32 );
        
        const derivedKey = await window.crypto.subtle.importKey( 
            'raw',
            rawKey,
            'AES-CBC',
            false,
            ['encrypt', 'decrypt']   
        );
        
        const encrypted = await window.crypto.subtle.encrypt(
            {
                name: 'AES-CBC',
                iv: iv
            },
            derivedKey,
            data
        );
        const encryptedArray = new Uint8Array(encrypted);
        const encryptedBase64 = btoa(String.fromCharCode(...encryptedArray));
    
        return encryptedBase64;    
    }
    
    function encodeUtf16le(text){
        var encoded = new Uint8Array(text.length * 2);
            for (var i = 0; i < text.length; i++) {
                encoded[i*2] = text.charCodeAt(i) // & 0xff;
                encoded[i*2+1] = text.charCodeAt(i) >> 8 // & 0xff;
            }
        return encoded;
    }
    
    (async () => {
        console.log(await EncryptText("The quick brown fox jumps over the lazy dog", "4fh!31ay*36S#@w!$%"));
    })();

    This produces the same ciphertext as the C# code with identical input data.

    Note that it is a vulnerability to use a static salt. Normally a random salt is applied. This is not secret and is passed with the ciphertext (usually concatenated).