cryptographyaesmsal.jswebcrypto-apihkdf

How to decrypt MSAL auth artifact, encrypted with AES-GCM and a HKDF-derived the key?


I use @azure/msal-browser in my project.

Account information are stored in the local storage and they were exploitable until recently.
But it changed in their v4 release:

if you are using the localStorage cache location, auth artifacts will be encrypted with AES-GCM using HKDF to derive the key

My question is how to decrypt those auth artifacts?

What I have:

What I've tested so far:

function toArrayBuffer(inputStr) {
    const len = inputStr.length;
    const bytes = new Uint8Array(len);
    for (var i = 0; i < len; i++)        {
        bytes[i] = inputStr.charCodeAt(i);
    }
    return bytes.buffer;
}

async function getCryptoKey(inputRawKey) {
    const keyBuffer = toArrayBuffer(inputRawKey);
    return await window.crypto.subtle.importKey(
        "raw",
        keyBuffer,
        { name: "HKDF" },
        false,
        ["deriveKey"]
    );
}

async function decryptData(inputCryptoKey, inputData) {
    const encryptedArrayBuffer = toArrayBuffer(inputData);
    return await window.crypto.subtle.decrypt(
        {
            name: "AES-GCM",
            iv: encryptedArrayBuffer.slice(0, 12),
            tag: encryptedArrayBuffer.slice(-16), // authentication tag
        },
        inputCryptoKey,
        encryptedArrayBuffer.slice(12, -16)
    );
}

const cryptoKey = await getCryptoKey(rawKey);
const res = await decryptData(cryptoKey, data); // fails

But it leads me to this error:

Uncaught InvalidAccessError: key.algorithm does not match that of operation

which seems logical...
I guess that my issue comes from the fact that I don't know how to "restore" the HKDF-derived key properly.
I should somehow use both "AES-GCM" and "HKDF" in my getCryptoKey function...

Also, it is pretty obvious that I'll have to use the nonce value somewhere, probably in place of the iv parameter
(but I am not at that point yet)

What I've looked:


Solution

  • Finally, we can simply look at the MSAL source code (cf. commit 94877715).

    // storage.js
    export async function getEncryptionParams() {
        const encryptionCookieObject = await window.cookieStore.get('msal.cache.encryption');
        const encryptionCookieEncodedRawValue = encryptionCookieObject.value;
        const encryptionCookieRawValue = decodeURIComponent(encryptionCookieEncodedRawValue);
        const mainEncryptionParams = JSON.parse(encryptionCookieRawValue)
        const { key: rawKey } = mainEncryptionParams;
    
        const accountRefsLocalStorageRawValue = window.localStorage.getItem('msal.account.keys');
        const accountRefs = JSON.parse(accountRefsLocalStorageRawValue);
        const accountRef = accountRefs[0];
    
        const accountRawValue = window.localStorage.getItem(accountRef);
        const accountEncryptionParams = JSON.parse(accountRawValue);
        const { data: encryptedData, nonce } = accountEncryptionParams;
    
        return { rawKey, encryptedData, nonce };
    }
    
    // utils.js
    export function base64DecToArr(base64String) {
        let encodedString = base64String.replace(/-/g, "+").replace(/_/g, "/");
        switch (encodedString.length % 4) {
            case 0:
                break;
            case 2:
                encodedString += "==";
                break;
            case 3:
                encodedString += "=";
                break;
        }
        const binString = atob(encodedString);
        return Uint8Array.from(binString, (m) => m.codePointAt(0) || 0);
    }
    
    // main.js
    const { rawKey, encryptedData, nonce } = await getEncryptionParams();
    
    const AES_GCM = "AES-GCM";
    const HKDF = "HKDF";
    
    const cryptoKey = await window.crypto.subtle.importKey(
        'raw',
        base64DecToArr(rawKey),
        HKDF,
        false,
        ['deriveKey']
    );
    
    const derivedKey = await window.crypto.subtle.deriveKey(
        {
            name: HKDF,
            salt: base64DecToArr(nonce),
            hash: 'SHA-256',
            info: new Uint8Array(),
        },
        cryptoKey,
        { name: AES_GCM, length: 256 },
        false,
        ['encrypt', 'decrypt']
    );
    
    const encodedData = base64DecToArr(encryptedData);
    const decryptedData = await window.crypto.subtle.decrypt(
        { name: AES_GCM, iv: new Uint8Array(12) },
        derivedKey,
        encodedData
    );
    
    const dataStr =  new TextDecoder().decode(decryptedData);
    const data = JSON.parse(dataStr);
    console.log(data);