Работа с байтовыми строками в JavaScript

В JavaScript отсутствует отдельный тип «байтовая строка» в классическом смысле, однако работа с байтами является фундаментальной частью криптографии, сетевых протоколов, бинарных форматов и низкоуровневых операций. В экосистеме криптографических библиотек, включая Jsrsasign, байтовое представление данных становится ключевым элементом при формировании хэшей, цифровых подписей, ASN.1 структур и кодировании ключей.

Для работы с байтами используется набор встроенных механизмов, объединённых вокруг концепции ArrayBuffer и представлений (views).

ArrayBuffer как основа бинарных данных

ArrayBuffer представляет собой фиксированный блок памяти, содержащий «сырые» байты.

const buffer = new ArrayBuffer(16);

Сам по себе ArrayBuffer не предоставляет способов чтения или записи данных. Он является контейнером, поверх которого строятся представления.

Uint8Array как основная байтовая строка

Наиболее близким аналогом байтовой строки является Uint8Array — массив 8-битных беззнаковых целых.

const bytes = new Uint8Array([0x48, 0x65, 0x6c, 0x6c, 0x6f]);

Каждый элемент массива представляет один байт (0–255). Именно этот тип используется в большинстве криптографических операций.

Ключевая особенность:

  • значения всегда нормализуются в диапазон 0–255
  • операции выполняются над «сырыми» байтами
  • отсутствует кодировка символов

Различие строк и байтовых массивов

Строка JavaScript — это UTF-16 последовательность символов, тогда как байтовый массив — это сырые данные.

const str = "Hello";
const bytes = new Uint8Array([72, 101, 108, 108, 111]);

Эти представления не эквивалентны. Прямое преобразование требует кодировщиков.

Кодирование текста в байты

Для преобразования строк в байты используется TextEncoder.

UTF-8 кодирование

const encoder = new TextEncoder();
const bytes = encoder.encode("Привет");

Результат — Uint8Array, содержащий UTF-8 представление строки.

Особенность UTF-8:

  • символы ASCII занимают 1 байт
  • кириллица — 2 байта
  • сложные символы — до 4 байт

Декодирование байтов в строку

Обратная операция выполняется через TextDecoder.

const decoder = new TextDecoder("utf-8");
const str = decoder.decode(bytes);

Важно учитывать корректность исходных данных: повреждённые байты приводят к заменяющим символам.

Шестнадцатеричное представление байтов

В криптографии часто используется hex-формат.

Преобразование Uint8Array → hex

function toHex(bytes) {
  return Array.from(bytes)
    .map(b => b.toString(16).padStart(2, "0"))
    .join("");
}

Преобразование hex → Uint8Array

function fromHex(hex) {
  const bytes = new Uint8Array(hex.length / 2);
  for (let i = 0; i < bytes.length; i++) {
    bytes[i] = parseInt(hex.substr(i * 2, 2), 16);
  }
  return bytes;
}

Hex часто используется в Jsrsasign для представления:

  • ключей
  • подписей
  • хэшей

Base64 как представление байтов

Base64 применяется для передачи бинарных данных в текстовых протоколах.

Кодирование Uint8Array в Base64

function toBase64(bytes) {
  let binary = "";
  bytes.forEach(b => binary += String.fromCharCode(b));
  return btoa(binary);
}

Декодирование Base64

function fromBase64(base64) {
  const binary = atob(base64);
  const bytes = new Uint8Array(binary.length);
  for (let i = 0; i < binary.length; i++) {
    bytes[i] = binary.charCodeAt(i);
  }
  return bytes;
}

Base64 часто используется в формате PEM, который поддерживается Jsrsasign.

Байтовые строки в криптографических операциях

Jsrsasign активно работает с бинарными представлениями данных при:

  • вычислении SHA-хэшей
  • генерации RSA/ECDSA подписей
  • обработке ключей PEM/DER
  • ASN.1 сериализации

Хэширование как пример работы с байтами

const hash = KJUR.crypto.Util.sha256("message");

Хотя на вход может подаваться строка, внутри происходит:

  1. кодирование в байты (UTF-8)
  2. обработка блоков по 512 бит
  3. получение Uint8Array результата

Подписи и байтовые представления

При создании цифровой подписи важно, чтобы входные данные имели строгую бинарную форму.

const sig = new KJUR.crypto.Signature({ alg: "SHA256withRSA" });
sig.init(privateKey);
sig.updateHex("48656c6c6f"); // hex представление байтов
const signature = sig.sign();

Здесь вход может быть:

  • строкой
  • hex-строкой
  • байтовым массивом (внутренне преобразуется)

ASN.1 и байтовая структура данных

Jsrsasign активно использует ASN.1 для кодирования структур ключей и сертификатов.

ASN.1 всегда сериализуется в байты:

  • INTEGER
  • SEQUENCE
  • BIT STRING

Пример логики:

SEQUENCE
 ├── INTEGER (version)
 ├── SEQUENCE (algorithm)
 └── BIT STRING (key data)

В памяти это представлено как Uint8Array.

DER кодирование

DER (Distinguished Encoding Rules) — бинарный формат ASN.1.

const der = new KJUR.asn1.DERInteger({ int: 123 });
const bytes = der.getByteArray();

Результат — массив байтов, готовый к хранению или передаче.

Работа с PEM и байтами

PEM — это Base64 поверх DER.

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkq...
-----END PUBLIC KEY-----

Процесс обработки:

  1. удаление заголовков
  2. Base64 декодирование
  3. получение Uint8Array (DER)

Jsrsasign выполняет эти шаги автоматически, но логически они всегда опираются на байтовые операции.

Побитовые операции и байты

JavaScript поддерживает побитовые операции только над 32-битными числами, но они часто используются внутри алгоритмов:

const a = 0x12 ^ 0x34;

Однако при работе с криптографией предпочтительнее использовать Uint8Array, чтобы избежать переполнений и потерь данных.

Конкатенация байтовых массивов

function concatBytes(a, b) {
  const result = new Uint8Array(a.length + b.length);
  result.set(a, 0);
  result.set(b, a.length);
  return result;
}

Такая операция часто применяется при формировании сообщений для подписи.

Сравнение байтовых массивов

function equals(a, b) {
  if (a.length !== b.length) return false;
  for (let i = 0; i < a.length; i++) {
    if (a[i] !== b[i]) return false;
  }
  return true;
}

В криптографии важно избегать оптимизаций, зависящих от раннего выхода, если требуется защита от timing-атак.

Нормализация данных перед криптографией

Перед использованием в Jsrsasign данные обычно проходят цепочку преобразований:

  • строка → UTF-8 байты
  • байты → хэш
  • хэш → hex/Base64
  • структура → DER

Любое несоответствие на уровне байтов приводит к различию подписи.

Особенности хранения байтовых данных

В JavaScript байтовые данные:

  • не имеют встроенного формата сериализации
  • требуют явного кодирования (hex, base64)
  • зависят от endianness только при многобайтовых интерпретациях

Использование байтов в Jsrsasign на уровне API

Типичные функции принимают несколько форм входных данных:

  • строка (UTF-8)
  • hex строка
  • Base64
  • Uint8Array

Внутри происходит приведение к единому байтовому представлению.

Типичные ошибки при работе с байтами

Потеря кодировки

"Привет" !== new Uint8Array([...])

Ошибка возникает при попытке трактовать строку как байты напрямую.

Двойное кодирование

Base64 → Base64 → ошибка интерпретации данных.

Неправильный hex

Строка с нечетным количеством символов приводит к некорректному парсингу.

Байтовая модель как основа криптографии

Любая операция в Jsrsasign сводится к последовательности:

  1. нормализация входа в байты
  2. применение математического алгоритма
  3. получение байтового результата
  4. кодирование в удобный формат

Таким образом, байтовые строки являются фундаментом всей криптографической модели JavaScript, несмотря на отсутствие явного примитивного типа в языке.