skip to Main Content

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


  1. Chosen as BEST ANSWER

    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 and crypto.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:

    // This spares you the rigmarole of attaching the regex to every property to be tested.
    function is_native(p_object)
      {
    // NOTE: You cannot just check for the presence of [native code] in the function, because that can be fooled by
    //       inserting e. g. a string containing this exact sequence of characters into it. Instead you have to
    //       check for anything that resembles a function's list of parameters and a function body.
      return p_object.toString().match(/()s{ns{4}[native code]n}$/);
      }
    
    function check_crypto()
      {
    var l_crypt_prot = Object.getPrototypeOf(crypto);
    var l_scrypt_prot = Object.getPrototypeOf(crypto.subtle);
    var l_keys;
    
    // First check for fatal compromises.
    // If any of the founding elements of the crypto API are found to have been
    // modified (that is, the constructors and the prototypes), a complete
    // compromise has to be assumed and no recovery will be possible!
      if(!is_native(Crypto))
        return -1;
      if(!is_native(SubtleCrypto))
        return -1;
    
      l_keys = Object.keys(l_crypt_prot);
    
      if(l_keys.length != crypt_keys.length)
        return -1;
      else
        if(!l_keys.every(p_value => crypt_keys.includes(p_value)))
          return -1;
    
      if(!l_keys.every(p_value => {
    var l_descript = Object.getOwnPropertyDescriptor(l_crypt_prot, p_value);
    
        switch(typeof l_descript.value)
          {
          case 'undefined':
            return (typeof l_descript.get == 'function') && is_native(l_descript.get);
    
          case 'function':
            return is_native(l_descript.value);
    
          default:
            return false;
          }
        }))
        return -1;
    
      l_keys = Object.keys(l_scrypt_prot);
    
      if(l_keys.length != scrypt_keys.length)
        return -1;
      else
        if(!l_keys.every(p_value => scrypt_keys.includes(p_value)))
          return -1;
    
      if(!l_keys.every(p_value => {
    var l_descript = Object.getOwnPropertyDescriptor(l_scrypt_prot, p_value);
    
        return (typeof l_descript.value == 'function') && is_native(l_descript.value);
        }))
        return -1;
    
    // Next check for non-fatal compromises.
    // These occur when the actual object has been tampered with, but the
    // founding blocks are still intact. In this case we can attempt to recover
    // from disaster.
      if(!is_native(crypto.constructor))
        return 0;
      if(!is_native(crypto.subtle.constructor))
        return 0;
    
      if(crypto.toString() != '[object Crypto]')
        return 0;
      else
        if(crypto.subtle.toString() != '[object SubtleCrypto]')
          return 0;
    
      if(Object.keys(crypto).length || Object.keys(crypto.subtle).length)
        return 0;
    
      if(!(crypto instanceof Crypto))
        return 0;
      if(!(crypto.subtle instanceof SubtleCrypto))
        return 0;
    
      if(!crypt_keys.every(p_value => {
        if(p_value == 'subtle')
          return typeof crypto[p_value] == 'object';
        else
          return (typeof crypto[p_value] == 'function') && is_native(crypto[p_value]);
        }))
        return 0;
    
      return scrypt_keys.every(p_value => (typeof crypto.subtle[p_value] == 'function') && is_native(crypto.subtle[p_value])) ? 1 : 0;
      };
    
    // Check the Web Crypto API for potential compromises.
    var needed_rec = true;
    
    var crypto_sane = check_crypto();
    if(crypto_sane == 0)
      {
    let l_item;
    let l_descript;
    let l_result;
    
      console.warn('protect.js: Found the crypto API to be modified! Attempting recovery...');
    
      needed_rec = true;
    
    // This is about eliminating any extraneous properties from the crypto object.
      for(l_item of Object.keys(crypto))
        {
        l_result = delete crypto[l_item];
        console.info('protect.js: Deleting extraneous property %s in crypto... %s', l_item, l_result ? 'done.' : 'failed!');
        }
    
    // Check whether our recovery attempt has succeeded...
      crypto_sane = check_crypto();
      }
    if(crypto_sane == 1)
      {
    let l_crypt_prot = Object.getPrototypeOf(crypto);
    let l_scrypt_prot = Object.getPrototypeOf(crypto.subtle);
    let l_descript;
    
    // Freezing the prototypes actually helps make the derived objects immutable!
      Object.freeze(l_crypt_prot);
      Object.freeze(l_scrypt_prot);
    // Protect the crypto property against modification!
    // Note that since it is a getter, you cannot set it to read-only!
    // If you still try to do so, it will be set to a value of undefined, and the crypto object is lost!
      Object.defineProperty(window, 'crypto', { configurable: false });
      if(needed_rec)
        console.info('protect.js: Recovery attempt successful!');
      console.info('protect.js: The cryptographic API is now PROTECTED against polyfill attacks!');
      }
    else
      {
      if(crypto_sane == -1)
        console.error('protect.js: !!! FATAL COMPROMISE DETECTED !!! RECOVERY IMPOSSIBLE !!!');
      if(needed_rec)
        console.error('protect.js: Recovery attempt FAILED!!!');
      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 !!!');
      }
    

    These checks now return one of three states:

    • The Web Crypto API is sane (return value: 1)
    • The Web Crypto API appears to have been modified, but that can probably be recovered from (return value: 0)
    • The Web Crypto API has been tampered with at its founding blocks (return value: -1)

    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.


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

    function check_crypto() {
      var l_cryptoapi;
      var l_protected = true;
    
      // 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 {
        // Check for the existence of specific methods
        const expectedMethods = [
          'decrypt', 'deriveBits', 'deriveKey', 'digest', 'encrypt',
          'exportKey', 'generateKey', 'importKey', 'sign',
          'unwrapKey', 'verify', 'wrapKey'
        ];
    
        for (const method of expectedMethods) {
          if (typeof l_cryptoapi[method] !== 'function') {
            l_protected = false;
            break;
          }
        }
      }
    
      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;
    };
    

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

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search