Получите ключи свойств как от объектов crypto, так и от crypto.subtle

Возникла следующая проблема:

Я создал модуль для проверки целостности Web Crypto API, и если все в порядке, он становится безопасным (и crypto, и crypto.subtle замораживаются). Однако в качестве одной из проверок целостности я намеревался получить ключи как от crypto, так и от crypto.subtle и проверить, присутствуют ли все ожидаемые свойства, являются ли они тем, что следует ожидать от немодифицированного API и (в случае элементы объекта SubtleCrypto) являются собственными функциями. К сожалению, Object.keys(crypto); и Object.keys(crypto.subtle); возвращают пустые массивы, поэтому очевидно, что свойства обоих объектов не перечислимы.
Обход через JSON.stringify(crypto); или JSON.stringify(crypto.subtle); также не работает, вместо этого возвращается пустой JSON.

Прямо сейчас, в качестве (неоптимального) обходного пути, я создал список ожидаемых ключей вручную и сразу перешел к проверке, являются ли это фактическими функциями, однако исходный код должен был получить ключи из crypto.subtle, а затем проверить, есть ли они двенадцать ключей в массиве, и если да, то все ли ожидаемые свойства действительно присутствуют.

Вот что у меня было изначально:

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\{\n\s{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;
  };

При этом, поскольку Object.keys(crypto.subtle); явно дает сбой, все, что зависит от этих мер, также обязательно потерпит неудачу, и вместо сообщения о том, что защита установлена, предупреждающие сообщения записываются в консоль.

Теперь возникает вопрос: есть ли способ обойти эти ограничения, и если да, то как это сделать?

Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
В настоящее время производительность загрузки веб-сайта имеет решающее значение не только для удобства пользователей, но и для ранжирования в...
Безумие обратных вызовов в javascript [JS]
Безумие обратных вызовов в javascript [JS]
Здравствуйте! Юный падаван 🚀. Присоединяйся ко мне, чтобы разобраться в одной из самых запутанных концепций, когда вы начинаете изучать мир...
Система управления парковками с использованием HTML, CSS и JavaScript
Система управления парковками с использованием HTML, CSS и JavaScript
Веб-сайт по управлению парковками был создан с использованием HTML, CSS и JavaScript. Это простой сайт, ничего вычурного. Основная цель -...
JavaScript Вопросы с множественным выбором и ответы
JavaScript Вопросы с множественным выбором и ответы
Если вы ищете платформу, которая предоставляет вам бесплатный тест JavaScript MCQ (Multiple Choice Questions With Answers) для оценки ваших знаний,...
0
0
64
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Я думаю, именно поэтому Object.keys(crypto.subtle) не работает должным образом, и вы получаете пустой массив.
Попробуйте использовать набор предопределенных методов и проверьте их существование. Вот мой пример.

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;
};

Этот подход моего примера кода напрямую проверяет наличие определенных методов, не полагаясь на Object.keys.

*** Имейте в виду, что при этом не проверяются внутренние свойства этих методов, но гарантируется наличие ожидаемых методов в API. ***

Это то, что я делаю прямо сейчас, но мне бы хотелось найти метод, который позволил бы мне проверить, могу ли я а) найти ключи, которые я ожидаю, и б) есть ли какие-либо ключи, которые не предполагается быть здесь. К сожалению, этот метод не может обнаружить никаких посторонних свойств.

Robidu 26.02.2024 22:35

Я провел еще несколько исследований по этому вопросу и, наконец, понял, в чем ошибся: вам нужно выбрать прототип, а не реальный объект, и вы сможете выполнять проверки, как и предполагалось.

Robidu 01.03.2024 08:28
Ответ принят как подходящий

После долгих исследований и доработок мне удалось выяснить, что искать и где применять исправления, чтобы сделать Web Crypto API защищенным от несанкционированного доступа.

Прежде всего, волшебство заключается не в объектах, к которым можно получить доступ через crypto и crypto.subtle, а нужно взглянуть на их прототипы. После того, как вы получили соответствующие объекты-прототипы, вы можете получить от них ключи и выполнить соответствующие проверки, чтобы выяснить, что-то не так, а также вы можете проверить функции, чтобы увидеть, были ли они подделаны или нет.

Что вам все равно нужно сделать, так это проверить сами объекты, не были ли перезаписаны какие-либо унаследованные методы, и удалить их, если применимо. В конце концов, вам нужен доступ к исходным методам, предоставленным (немодифицированными) прототипами.

Дальнейшие расследования также позволили мне понять, что необходимо выполнить множество дополнительных проверок, чтобы оценить целостность API и, в зависимости от того, где обнаружен компрометация, определить, может ли попытка восстановления быть успешной.

При этом решение проблемы выглядит так:

// 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\{\n\s{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 !!!');
  }

Эти проверки теперь возвращают одно из трех состояний:

  • Web Crypto API работает нормально (возвращаемое значение: 1).
  • API Web Crypto, похоже, был изменен, но его, вероятно, можно восстановить (возвращаемое значение: 0).
  • API Web Crypto был изменен в его основных блоках (возвращаемое значение: -1).

Если вы заключите это в замыкание, вы можете вставить объект в DOM (например, в объект навигатора), который предоставляет геттер, чтобы проверить, установлено ли для crypto_sane значение 1 (возврат true) или нет (возврат false). Это может использоваться другими модулями, чтобы определить, безопасен ли API веб-криптографии. Однако необходимо убедиться, что этот объект-индикатор также не может быть изменен.

Другие вопросы по теме