Некриптографическое использование SubtleCrypto
В этой статье мы сосредоточимся на использовании метода digest интерфейса SubtleCrypto. Многие другие методы в API веб-криптографии имеют очень специфические криптографические применения, но создание хешей содержимого (что делает метод digest) имеет множество полезных применений.
В этой статье не рассматриваются криптографические применения интерфейса SubtleCrypto. Важно помнить из этой статьи: не используйте этот API для криптографических задач в продакшене, потому что он мощный и низкоуровневый. Для его правильного использования вам потребуются многие контекстно-специфичные шаги для правильного выполнения криптографических задач. Если любой из этих шагов будет выполнен неправильно, в лучшем случае ваш код не будет работать, а в худшем — он будет работать, и вы неосознанно подвергнете пользователей риску с небезопасным продуктом.
Возможно, вам вообще не нужно использовать API веб-криптографии. Многие задачи, для которых вы хотели бы использовать криптографию, уже решены и являются частью веб-платформы. Например, если вы беспокоитесь о нападениях «человек посередине», таких как точки доступа Wi-Fi, читающие информацию между клиентом и сервером, это решается путем обеспечения правильного использования HTTPS. Вы хотите безопасно отправлять информацию между пользователями? Тогда вы можете настроить соединение с передачей данных между пользователями с использованием каналов данных WebRTC, которое шифруется как часть стандарта.
Интерфейс SubtleCrypto предоставляет низкоуровневые примитивы для работы с криптографией, но реализация системы с помощью этих инструментов — сложная задача. Ошибки трудно заметить, и результаты могут означать, что данные ваших пользователей не так безопасны, как вы думаете. Это может иметь катастрофические последствия, если ваши пользователи обмениваются конфиденциальными или ценными данными.
Если вы сомневаетесь, не пытайтесь делать это самостоятельно, наймите человека с опытом и убедитесь, что ваш программный продукт прошел аудит у эксперта по безопасности.
Хеширование файла
Это самое простое полезное действие, которое вы можете выполнить с помощью API веб-криптографии. Оно не включает в себя генерацию ключей или сертификатов и состоит из одного шага.
Хеширование — это техника преобразования большой строки байтов в меньшую строку, где небольшие изменения в длинной строке приводят к большим изменениям в меньшей строке. Эта техника полезна для идентификации двух одинаковых файлов без проверки каждого байта обоих файлов. Это очень полезно, так как вам нужна простая строка для сравнения. Для ясности, хеширование — это одностороннее действие. Вы не можете получить исходную строку байтов из хеша.
Если два сгенерированных хеша одинаковы, но файлы, которые использовались для их генерации, отличаются, это известно как столкновение хешей, что чрезвычайно маловероятно по случайности и почти невозможно создать для безопасной хеш-функции, такой как SHA256. Таким образом, если две строки одинаковы, вы можете быть относительно уверены, что два исходных файла идентичны.
На момент публикации SHA256 — это обычный выбор для хеширования файлов, но в интерфейсе SubtleCrypto доступны функции хеширования более высокого порядка. Наиболее распространённое представление хеша SHA256 — это строка из 64 шестнадцатеричных цифр. Шестнадцатеричное представление означает, что используются только символы от 0 до 9 и от a до f, представляющие 4 бита информации. Короче говоря, хеш SHA256 преобразует данные любой длины в почти уникальные 256 бит данных.
Эта техника часто используется сайтами, которые позволяют загружать исполняемые файлы, чтобы убедиться, что загруженный файл соответствует файлу, задуманному автором. Это гарантирует, что пользователи не устанавливают вредоносные программы. Наиболее распространённый способ сделать это:
- Запишите имя файла и предоставленную веб-сайтом контрольную сумму SHA256.
- Загрузите исполняемый файл.
- Запустите
sha256sum /path/to/the/fileв терминале, чтобы сгенерировать свой собственный код. Если вы используете Mac, возможно, вам придётся установить его отдельно. - Сравните две строки — они должны совпадать, если файл не был взломан.
Метод digest() интерфейса SubtleCrypto полезен для этого. Чтобы сгенерировать контрольную сумму файла, можно сделать так:
Сначала добавим некоторые HTML-элементы для загрузки файлов и отображения выходных данных SHA-256:
<h3>Demonstration of hashing a file with SHA256</h3> <label >Choose file(s) to hash <input type="file" id="file" name="file" multiple /></label> <output style="display:block;font-family:monospace;"></output>
Далее мы используем интерфейс SubtleCrypto для их обработки. Это работает следующим образом:
- Чтение файлов в
ArrayBufferс помощью методаFileобъектаarrayBuffer(). - Использовать
crypto.subtle.digest('SHA-256', arrayBuffer)для хеширования ArrayBuffer - Преобразование полученного хеша (ещё один ArrayBuffer) в строку для отображения
const output = document.querySelector("output");
const file = document.getElementById("file");
// Run the hashing function when the user selects one or more file
file.addEventListener("change", hashTheseFiles);
// The digest function is asynchronous, it returns a promise
// We use the async/await syntax to simplify the code.
async function fileHash(file) {
const arrayBuffer = await file.arrayBuffer();
// Use the subtle crypto API to perform a SHA256 Sum of the file's
// Array Buffer. The resulting hash is stored in an array buffer
const hashAsArrayBuffer = await crypto.subtle.digest("SHA-256", arrayBuffer);
// To display it as a string we will get the hexadecimal value of
// each byte of the array buffer. This gets us an array where each byte
// of the array buffer becomes one item in the array
const uint8ViewOfHash = new Uint8Array(hashAsArrayBuffer);
// We then convert it to a regular array so we can convert each item
// to hexadecimal strings, where characters of 0-9 or a-f represent
// a number between 0 and 15, containing 4 bits of information,
// so 2 of them is 8 bits (1 byte).
const hashAsString = Array.from(uint8ViewOfHash)
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
return hashAsString;
}
async function hashTheseFiles(e) {
let outHTML = "";
// iterate over each file in file select input
for (const file of this.files) {
// calculate its hash and list it in the output element.
outHTML += `${file.name} ${await fileHash(file)}\n`;
}
output.innerText = outHTML;
}
Где это можно использовать?
На этом этапе вы можете подумать: «Я могу использовать это на своем собственном сайте, поэтому, когда пользователи загружают файл, мы можем убедиться, что хеши совпадают, чтобы заверить пользователя, что его загрузка безопасна». К сожалению, это вызывает две проблемы:
-
Загрузка исполняемых файлов всегда должна выполняться по HTTPS. Это предотвращает вмешательство третьих сторон в такие атаки, поэтому это будет излишним.
-
Если злоумышленник может заменить загруженный файл на исходном сервере, то он также может просто заменить код, вызывающий интерфейс SubtleCrypto, чтобы обойти его и просто заявить, что всё в порядке. Возможно, что-то хитрое, например, замена строгого равенства, что может быть сложно заметить в вашем собственном коде:
--- if (checksum === correctCheckSum) return true; +++ if (checksum = correctCheckSum) return true;
Одно место, где это может быть полезно, — это тестирование файла из стороннего источника загрузки, которым вы не управляете. Это было бы так, если бы место загрузки имело включенные заголовки CORS, чтобы вы могли просканировать файл перед предоставлением его пользователям. К сожалению, на многих серверах CORS по умолчанию не включён.
Что такое «соление хеша»?
Вы могли слышать фразу «соление хеша». Это не напрямую относится к рассматриваемым нами темам, но полезно об этом знать.
Примечание: Этот раздел касается безопасности паролей, а функции хеширования, предоставляемые SubtleCrypto, не подходят для этого случая. Для этих целей требуются дорогие медленные хеш-функции, такие как scrypt и bcrypt. SHA разработан для высокой скорости и эффективности, что делает его непригодным для хеширования паролей. Этот раздел предоставлен только для вашего интереса — не используйте API веб-криптографии для хеширования паролей на стороне клиента.
Популярное применение хеширования — это пароли. Вы никогда не должны хранить пароли пользователей в открытом виде, это просто плохая идея. Вместо этого вы храните хеш пароля пользователя, чтобы исходный пароль нельзя было восстановить, если хакер получит вашу базу данных с именами пользователей и паролями. Те, у кого зоркий глаз, могут заметить, что вы всё ещё можете узнать исходные пароли, сравнивая хеши из списков известных паролей с полученным списком хешей паролей. Добавление строки к паролям меняет хеш, поэтому он больше не соответствует. Это называется солением. Ещё одна сложная проблема состоит в том, что если вы используете один и тот же соль для каждого пароля, то пароли с совпадающими хешами также будут иметь одинаковый исходный пароль. Таким образом, если вы знаете один, то вы знаете все соответствующие пароли.
Чтобы решить эту проблему, выполняется так называемое соление хеша. Для каждого пароля вы генерируете соль (случайную строку символов) и объединяете её с парольной строкой. Затем вы храните хеш и соль в одной базе данных, чтобы вы могли проверить совпадение, когда пользователь пытается войти позже. Это означает, что если два пользователя используют один и тот же пароль, хеши будут разными. Именно поэтому вам нужна дорогая криптографическая функция, чтобы сделать слишком трудоёмким использование списков общих паролей для выяснения исходных паролей.
Хеш-таблицы с SHA
Вы можете использовать SHA1 для быстрого создания некриптографически безопасных хешей. Это невероятно полезно для преобразования произвольных данных в ключ, который можно использовать для поиска позже.
Например, если вам нужна база данных, которая включает большой блок данных в качестве одного из полей в строке. Это снижает эффективность вашей базы данных, потому что одно из полей должно быть либо переменной длины, либо достаточно большим для хранения самого большого возможного блока. Альтернативное решение — сгенерировать хеш блока и хранить его в отдельной таблице поиска, используя хеш в качестве индекса. Затем вы можете хранить только хеш в вашей исходной базе данных, что является хорошей фиксированной длиной.
Возможные варианты хеша SHA1 невероятно многочисленны. Настолько, что случайно получить два блока с одинаковым хешем SHA1 практически невозможно. Возможно намеренно создать два файла с одинаковым хешем SHA1, потому что SHA1 не является криптографически безопасным. Поэтому злонамеренный пользователь теоретически мог сгенерировать блок данных, который заменит оригинальный в базе данных, что останется незамеченным, потому что хеш такой же. Это вектор атаки, о котором стоит знать.
Как Git хранит файлы
Git использует хэши SHA1 и является отличным примером здесь, он использует хэши двумя интересными способами. Когда файлы хранятся в git, они ссылаются на свой хэш SHA1. Это позволяет git быстро находить данные и восстанавливать файлы.
Однако, он использует не только содержимое файла для хэша, но и добавляет строку UTF8 "blob ", за которой следует размер файла в байтах, записанный в десятичном виде, а затем нулевой символ (который в JavaScript можно записать "\0"). Вы можете использовать интерфейс TextEncoder API кодирования Encoding API для кодирования текста UTF8, так как строки в JavaScript являются UTF16.
Приведённый ниже код, подобно нашему примеру SHA256, может использоваться для генерации этих хэшей из файлов. HTML для загрузки файлов остаётся тем же, но мы выполняем дополнительную работу для добавления информации о размере таким же образом, как это делает git.
<h3>Demonstration of how git uses SHA1 for files</h3> <label >Choose file(s) to hash <input type="file" id="file" name="file" multiple /></label> <output style="display:block;font-family:monospace;"></output>
const output = document.querySelector("output");
const file = document.getElementById("file");
file.addEventListener("change", hashTheseFiles);
async function fileHash(file) {
const arrayBuffer = await file.arrayBuffer();
// Git prepends the null terminated text 'blob 1234' where 1234
// represents the file size before hashing so we are going to reproduce that
// first we work out the Byte length of the file
const uint8View = new Uint8Array(arrayBuffer);
const length = uint8View.length;
// Git in the terminal uses UTF8 for its strings; the Web uses UTF16.
// We need to use an encoder because different binary representations
// of the letters in our message will result in different hashes
const encoder = new TextEncoder();
// Null-terminated means the string ends in the null character which
// in JavaScript is '\0'
const view = encoder.encode(`blob ${length}\0`);
// We then combine the 2 Array Buffers together into a new Array Buffer.
const newBlob = new Blob([view.buffer, arrayBuffer], {
type: "text/plain",
});
const arrayBufferToHash = await newBlob.arrayBuffer();
// Finally we perform the hash this time as SHA1 which is what Git uses.
// Then we return it as a string to be displayed.
return hashToString(await crypto.subtle.digest("SHA-1", arrayBufferToHash));
}
function hashToString(arrayBuffer) {
const uint8View = new Uint8Array(arrayBuffer);
return Array.from(uint8View)
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
}
// like before we iterate over the files
async function hashTheseFiles(e) {
let outHTML = "";
for (const file of this.files) {
outHTML += `${file.name} ${await fileHash(file)}\n`;
}
output.innerText = outHTML;
}
Обратите внимание, как он использует API кодирования Encoding API для создания заголовка, который конкатенируется с исходным ArrayBuffer для создания строки, которая будет хэшироваться.
Как git генерирует хэши коммитов
Интересно, что git также генерирует хэши коммитов аналогичным образом, основываясь на нескольких фрагментах информации. К ним могут относиться хэш предыдущего коммита и сообщение коммита, которые вместе образуют новый хэш. Это можно использовать для ссылки на коммиты, основанные на нескольких уникальных идентификаторах.
Команда терминала: (printf "commit %s\0" $(git --no-replace-objects cat-file commit HEAD | wc -c); git cat-file commit HEAD) | sha1sum
Источник: Как формируется хэш SHA1 коммита git
В сущности, это строка UTF8 (нулевой символ записывается как \0):
commit [size in bytes as decimal of this info]\0tree [tree hash] parent [parent commit hash] author [author info] [timestamp] committer [committer info] [timestamp] commit message
Это замечательно, потому что ни одно из отдельных полей не гарантирует уникальность, но когда они объединены вместе, они дают уникальную ссылку на один коммит. Однако, вся строка слишком длинная и неудобная для использования. Поэтому, хэшированием её, вы получаете новую уникальную строку, достаточно короткую, чтобы её удобно было использовать из нескольких полей.
Вот почему хэш изменяется, если вы когда-либо изменяли свой коммит, даже если вы не вносите никаких изменений в сообщение. Отметка времени коммита изменилась, и даже одно изменение символа достаточно, чтобы полностью изменить новый хэш.
Вывод из этого заключается в том, что когда вы хотите добавить ключ к данным, но ни один фрагмент информации недостаточно уникален, тогда конкатенация нескольких строк вместе и их хэширование - отличный способ сгенерировать полезный ключ.
Надеюсь, эти примеры убедили вас взглянуть на этот новый мощный API. Помните, не пытайтесь самостоятельно создавать криптографические вещи. Достаточно знать, что эти инструменты существуют, и некоторые из них, такие как функция crypto.digest(), являются полезными инструментами для ежедневного развития.
© 2005–2024 MDN contributors.
Licensed under the Creative Commons Attribution-ShareAlike License v2.5 or later.
https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API/Non-cryptographic_uses_of_subtle_crypto