The following problem at hand:
I have built a module for verifying the integrity of the Web Crypto API, and if everything is deemed ok, it gets secured (both crypto and crypto.subtle are frozen). However, as one of the integrity checks I have intended to obtain the keys from both crypto and crypto.subtle and check whether or not all expected properties are there, if they are what is to be expected of an unmodified API and (in case of the elements of the SubtleCrypto object) are native functions.
Unfortunately Object.keys(crypto);
and Object.keys(crypto.subtle);
both return empty arrays so obviously the properties of both objects are not enumerable.
Taking a detour via JSON.stringify(crypto);
or JSON.stringify(crypto.subtle);
doesn’t work, either, and an empty JSON is returned instead.
Right now, as a (suboptimal) workaround I have created the list of expected keys manually and go straight for checking whether or not those are actual functions, however, the original code was supposed to retrieve the keys from crypto.subtle
, then check if there are twelve keys in the array, and if yes, whether all expected properties are actually there.
This is what I have originally had:
function check_crypto()
{
var l_cryptoapi;
var l_protected = true;
var l_crypt_keys;
// If crypto isn't object Crypto or crypto.subtle isn't object SubtleCrypto,
// someone has tampered with the API!
if(crypto.toString() != '[object Crypto]')
l_protected = false;
else
if((l_cryptoapi = crypto.subtle).toString() != '[object SubtleCrypto]')
l_protected = false;
else
l_crypt_keys = Object.keys(crypto.subtle); // THIS DOES NOT WORK!!!
// l_crypt_keys is empty so the next checks inevitably fail!
if(l_protected)
// If there aren't twelve properties in crypto.subtle, someone has tampered
// with the API!
if(l_crypt_keys.length != 12)
l_protected = false;
else
// If we find an unknown key, someone has tampered with the API!
l_protected = l_crypt_keys.every(p_value => [ 'decrypt', 'deriveBits', 'deriveKey', 'digest', 'encrypt', 'exportKey', 'generateKey', 'importKey',
'sign', 'unwrapKey', 'verify', 'wrapKey' ].includes(p_value));
// If any function isn't native code, someone has tampered with the API!
if(l_protected)
l_protected = l_crypt_keys.every(p_value => (typeof l_cryptoapi[p_value] == 'function') && l_cryptoapi[p_value].toString().match(/()s{ns{4}[native code]n}$/));
if(l_protected)
{
Object.freeze(l_cryptoapi);
Object.freeze(crypto);
console.info('protect.js: The cryptographic API is now PROTECTED against polyfill attacks!');
}
else
{
console.error('protect.js: The cryptographic API has been found to have been tampered with!');
console.error('protect.js: Anything that is relying on this API has to be considered insecure!');
console.error('protect.js: !!! WATCH OUT !!! SOMEONE MAY BE DOING SOMETHING REALLY NASTY !!!');
}
return l_protected;
};
That being said, with Object.keys(crypto.subtle);
obviously failing, everything that depends on these measures is bound to fail as well, and instead of getting a message that the protections are in place, the warning messages are logged to the console.
Now the question at hand is, is there a way around these limitations, and if yes, how is it done?
2
Answers
After a lot of investigating and tinkering I managed to find out what to look for and where to apply the patches to make the Web Crypto API tamperproof.
First of all, the magic doesn't reside in the objects that can be accessed via
crypto
andcrypto.subtle
, but you have to have a look at their prototypes. Once you have obtained the respective prototype objects, you can actually retrieve the keys from them and run any appropriate checks to figure out whether or not anything is amiss, and you can also check the functions to see whether or not they have been tampered with.What you still have to do is check the objects themselves whether or not any of the inherited methods have been overwritten and delete those if applicable. After all, you want access to the original methods provided by the (unmodified) prototypes.
Further investigations also made me aware that there are a lot of additional checks that need to be performed to assess the API's integrity, and depending on where the compromise is found, indicate whether or not a recovery attempt could be successful.
That said, the solution to the problem looks like this:
These checks now return one of three states:
If you wrap this up in a closure, you can insert an object into the DOM (e. g. in the navigator object) that provides a getter to check if crypto_sane is set to 1 (return true) or not (return false). This can be used by other modules to determine whether or not the Web Crypto API is safe. However, it must be made sure that this indicator object cannot be tampered with, either.
I think that is why Object.keys(crypto.subtle) does not work as expected, and you are getting an empty array.
Try with using a set of predefined methods and check for their existence. Here is my example.
This approach of my example code is directly checking for the existence of specific methods without relying on Object.keys.
*** Keep in mind that this won’t check for the internal properties of these methods, but it ensures that the expected methods are present in the API. ***