Хеширование строк через TextEncoder

В Web Crypto API все криптографические операции выполняются над бинарными данными. Обычные строки JavaScript не подходят напрямую, так как представлены в виде UTF-16. Для корректной работы с хешированием требуется преобразование строки в массив байтов.

Ключевую роль здесь играет встроенный интерфейс TextEncoder, который преобразует строку в Uint8Array в кодировке UTF-8.

const encoder = new TextEncoder();
const data = encoder.encode("Пример строки");

Результат:

  • data — это Uint8Array, содержащий последовательность байтов
  • кодировка всегда UTF-8 (другие не поддерживаются)

Хеширование через SubtleCrypto.digest

Основной механизм хеширования в Web Crypto API — метод crypto.subtle.digest. Он принимает:

  1. Алгоритм хеширования
  2. Бинарные данные (ArrayBuffer или TypedArray)
const encoder = new TextEncoder();
const data = encoder.encode("Пример строки");

const hashBuffer = await crypto.subtle.digest("SHA-256", data);

Поддерживаемые алгоритмы:

  • "SHA-1" (устаревший, не рекомендуется)
  • "SHA-256"
  • "SHA-384"
  • "SHA-512"

Работа с результатом хеширования

Результат digest — это ArrayBuffer. Для удобства его обычно преобразуют в массив байтов:

const hashArray = Array.from(new Uint8Array(hashBuffer));

Далее часто требуется строковое представление, например в hex-формате:

const hashHex = hashArray
  .map(byte => byte.toString(16).padStart(2, "0"))
  .join("");

Теперь hashHex содержит читаемое представление хеша.


Полный пример

async function hashString(str) {
  const encoder = new TextEncoder();
  const data = encoder.encode(str);

  const hashBuffer = await crypto.subtle.digest("SHA-256", data);
  const hashArray = Array.from(new Uint8Array(hashBuffer));

  return hashArray
    .map(byte => byte.toString(16).padStart(2, "0"))
    .join("");
}

hashString("Hello, WebCrypto!").then(console.log);

Особенности TextEncoder

1. Всегда UTF-8

TextEncoder не поддерживает выбор кодировки. Это гарантирует единообразие, но требует учета при работе с внешними системами.

2. Поддержка Unicode

Корректно обрабатывает:

  • кириллицу
  • эмодзи
  • любые символы Unicode
encoder.encode("?"); 
// корректно преобразуется в UTF-8 последовательность байтов

3. Иммутабельность результата

Каждый вызов encode() возвращает новый массив. Изменения не влияют на исходную строку.


Разница между строками и байтами

Важно понимать, что одинаково выглядящие строки могут иметь разное бинарное представление:

"é" !== "e\u0301"

Обе строки визуально идентичны, но:

  • первая — один символ
  • вторая — комбинация символов

TextEncoder закодирует их по-разному, что приведет к разным хешам.


Нормализация строк перед хешированием

Для предотвращения подобных проблем используется нормализация Unicode:

const normalized = str.normalize("NFC");
const data = encoder.encode(normalized);

Часто применяемые формы:

  • NFC — каноническая композиция
  • NFD — каноническая декомпозиция

Выбор зависит от требований системы, но важно использовать одну форму последовательно.


Производительность и ограничения

  • TextEncoder работает очень быстро и реализован нативно
  • не требует сторонних библиотек
  • безопасен для работы в браузере и Node.js (с поддержкой Web Crypto)

Однако:

  • не поддерживает потоковую обработку (streaming)
  • требует загрузки всей строки в память

Использование в Node.js

В современных версиях Node.js TextEncoder доступен глобально:

const encoder = new TextEncoder();

А также доступен через модуль util (в старых версиях):

const { TextEncoder } = require("util");

Web Crypto API доступен через:

const { subtle } = require("crypto").webcrypto;

Частые ошибки

Передача строки напрямую в digest

// Ошибка
crypto.subtle.digest("SHA-256", "text");

Метод ожидает бинарные данные.


Забытый await

const hash = crypto.subtle.digest("SHA-256", data);
// hash — Promise, а не результат

Неправильная интерпретация результата

ArrayBuffer нельзя напрямую вывести как строку:

console.log(hashBuffer); // нечитаемый вывод

Требуется преобразование в hex или base64.


Альтернативные форматы вывода

Base64:

function bufferToBase64(buffer) {
  return btoa(String.fromCharCode(...new Uint8Array(buffer)));
}

Uint8Array напрямую:

Используется в бинарных протоколах и криптографических операциях без преобразования.


Практические применения

  • хранение паролей (с добавлением соли и алгоритмов типа PBKDF2)
  • проверка целостности данных
  • создание цифровых подписей (в комбинации с другими API)
  • генерация идентификаторов

Связь с другими API

TextEncoder часто используется вместе с:

  • TextDecoder — обратное преобразование
  • SubtleCrypto — криптографические операции
  • Uint8Array и ArrayBuffer — работа с бинарными данными

Минимальный шаблон

const encoder = new TextEncoder();
const data = encoder.encode(inputString);
const hash = await crypto.subtle.digest("SHA-256", data);

Этот паттерн лежит в основе большинства операций хеширования строк в Web Crypto API.