Search code examples
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 C# code derives not only the key but also the IV from the key material, while the JavaScript code uses a random IV. One possible fix is to use deriveBits() to derive a 48 byte sequence, of which the first 32 bytes are the raw key (to be imported with importKey()) and the last 16 bytes are the IV.
    • The C# code uses SHA-1 by default for the key derivation, while SHA-256 is applied in the JavaScript code. As fix, SHA-1 must be used in the JavaScript code as well.
    • The C# code uses UTF-16LE (called Unicode in .NET) to encode the data, while the JavaScript code applies UTF-8. As fix, UTF-16LE must also be used in the JavaScript code, see e.g. here (for code points within the BMP).

    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).