Возникла следующая проблема:
Я создал модуль для проверки целостности 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); явно дает сбой, все, что зависит от этих мер, также обязательно потерпит неудачу, и вместо сообщения о том, что защита установлена, предупреждающие сообщения записываются в консоль.
Теперь возникает вопрос: есть ли способ обойти эти ограничения, и если да, то как это сделать?



![Безумие обратных вызовов в javascript [JS]](https://i.imgur.com/WsjO6zJb.png)


Я думаю, именно поэтому 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. ***
Я провел еще несколько исследований по этому вопросу и, наконец, понял, в чем ошибся: вам нужно выбрать прототип, а не реальный объект, и вы сможете выполнять проверки, как и предполагалось.
После долгих исследований и доработок мне удалось выяснить, что искать и где применять исправления, чтобы сделать 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 !!!');
}
Эти проверки теперь возвращают одно из трех состояний:
Если вы заключите это в замыкание, вы можете вставить объект в DOM (например, в объект навигатора), который предоставляет геттер, чтобы проверить, установлено ли для crypto_sane значение 1 (возврат true) или нет (возврат false). Это может использоваться другими модулями, чтобы определить, безопасен ли API веб-криптографии. Однако необходимо убедиться, что этот объект-индикатор также не может быть изменен.
Это то, что я делаю прямо сейчас, но мне бы хотелось найти метод, который позволил бы мне проверить, могу ли я а) найти ключи, которые я ожидаю, и б) есть ли какие-либо ключи, которые не предполагается быть здесь. К сожалению, этот метод не может обнаружить никаких посторонних свойств.