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:
msal.cache.encryption
(session cookie)someGuidA.someGuidB-login.windows.net-someGuidB
(local storage)someGuidA.someGuidB-login.windows.net-someGuidB
tooWhat 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:
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);