Секретное разделение ключей (Secret Sharing): обходные пути

Ограничения Web Crypto API в контексте распределения секретов

Web Crypto API предоставляет низкоуровневые криптографические примитивы: генерацию ключей, симметричное и асимметричное шифрование, HMAC, хэш-функции и генерацию криптографически стойкой случайности через crypto.getRandomValues. При этом в стандартном наборе отсутствует прямой механизм секретного разделения ключей между участниками.

Это означает, что задача распределения секрета по схеме, при которой один ключ восстанавливается только при наличии определённого числа участников, решается исключительно на уровне прикладной логики.

На практике используется реализация схемы Шамира (Shamir’s Secret Sharing), построенной поверх базовых возможностей JavaScript и WebCrypto.


Математическая основа схемы Шамира

Секрет ( S ) рассматривается как свободный член полинома степени ( t - 1 ):

S = f(0) = a_0

Полином задаётся как:

f(x) = a_0 + a_1 x + a_2 x^2 + + a_{t-1} x^{t-1}

Где:

  • ( a_0 = S ) — секрет
  • остальные коэффициенты — случайные значения
  • ( t ) — порог восстановления

Каждая доля вычисляется как точка:

(x_i, y_i) = (x_i, f(x_i))

Для восстановления секрета требуется минимум ( t ) точек.


Генерация криптографической случайности через WebCrypto

Критически важный компонент — генерация коэффициентов полинома. В браузере это делается через:

const array = new Uint32Array(1);
crypto.getRandomValues(array);

Для криптографически стойкой схемы недостаточно Math.random(), так как он не обеспечивает предсказуемость.


Представление секрета в числовой форме

WebCrypto работает с бинарными буферами, поэтому секрет необходимо привести к числовому виду.

Пример преобразования строки в число:

function textToBigInt(text) {
  const encoder = new TextEncoder();
  const bytes = encoder.encode(text);
  let result = 0n;

  for (const byte of bytes) {
    result = (result << 8n) + BigInt(byte);
  }

  return result;
}

Ограничение: секрет должен помещаться в модульное поле. В реальных системах используют большое простое число ( p ), например в поле конечной арифметики.


Арифметика в конечном поле

Все операции выполняются по модулю простого числа:

(a + b) p

(a b) p

Использование модуля предотвращает переполнение и обеспечивает корректность восстановления.


Генерация долей (shares)

Базовая реализация:

function generateShares(secret, threshold, sharesCount, prime) {
  const coefficients = [secret];

  for (let i = 1; i < threshold; i++) {
    const rand = crypto.getRandomValues(new Uint32Array(1))[0];
    coefficients.push(BigInt(rand) % prime);
  }

  function evaluate(x) {
    let result = 0n;
    let power = 1n;

    for (const coeff of coefficients) {
      result = (result + coeff * power) % prime;
      power = (power * x) % prime;
    }

    return result;
  }

  const shares = [];

  for (let i = 1n; i <= BigInt(sharesCount); i++) {
    shares.push({
      x: i,
      y: evaluate(i)
    });
  }

  return shares;
}

Восстановление секрета: интерполяция Лагранжа

Для восстановления используется формула:

f(0) = {i=1}^{t} y_i {j i}

В модульной арифметике:

function modInverse(a, p) {
  return modPow(a, p - 2n, p);
}

function modPow(base, exp, mod) {
  let result = 1n;
  base %= mod;

  while (exp > 0n) {
    if (exp % 2n === 1n) {
      result = (result * base) % mod;
    }
    base = (base * base) % mod;
    exp /= 2n;
  }

  return result;
}

Реализация восстановления секрета

function reconstructSecret(shares, prime) {
  let secret = 0n;

  for (let i = 0; i < shares.length; i++) {
    let xi = shares[i].x;
    let yi = shares[i].y;

    let numerator = 1n;
    let denominator = 1n;

    for (let j = 0; j < shares.length; j++) {
      if (i === j) continue;

      let xj = shares[j].x;

      numerator = (numerator * (-xj + prime)) % prime;
      denominator = (denominator * (xi - xj + prime)) % prime;
    }

    const lagrangeCoeff = (numerator * modInverse(denominator, prime)) % prime;

    secret = (prime + secret + yi * lagrangeCoeff) % prime;
  }

  return secret;
}

Усиление безопасности через WebCrypto

Secret sharing сам по себе не обеспечивает конфиденциальность каждой доли. Поэтому часто применяется дополнительное шифрование долей.

Генерация AES-GCM ключа

const key = await crypto.subtle.generateKey(
  { name: "AES-GCM", length: 256 },
  true,
  ["encrypt", "decrypt"]
);

Шифрование долей

async function encryptShare(key, share) {
  const iv = crypto.getRandomValues(new Uint8Array(12));

  const encoded = new TextEncoder().encode(JSON.stringify(share));

  const ciphertext = await crypto.subtle.encrypt(
    { name: "AES-GCM", iv },
    key,
    encoded
  );

  return { iv, ciphertext };
}

Комбинирование секретного разделения и шифрования

Типовая архитектура:

  1. Генерация секрета (ключа)
  2. Разделение через полином Шамира
  3. Шифрование каждой доли AES-GCM
  4. Хранение долей в разных каналах (браузер, сервер, устройство, офлайн-хранилище)

Такое разделение защищает от:

  • компрометации одного хранилища
  • частичного доступа злоумышленника
  • утраты одного носителя

Типичные ошибки реализации

Использование недостаточно большого простого числа

Если модуль ( p ) мал, возможны атаки перебором.

Использование Number вместо BigInt

JavaScript Number не подходит для криптографических операций из-за потери точности.

Повторное использование коэффициентов

Случайные коэффициенты должны генерироваться заново для каждого нового секрета.

Хранение всех долей на одном устройстве

Это полностью нивелирует смысл схемы.


Особенности браузерной среды

WebCrypto ограничен песочницей:

  • отсутствует прямой доступ к файловой системе
  • невозможность безопасного хранения секретов без IndexedDB
  • асинхронный API криптографических операций

Поэтому секретное разделение часто комбинируется с:

  • IndexedDB
  • Service Workers
  • внешними аппаратными ключами (WebAuthn)

Практическая архитектура распределённого ключа

Типовая схема использования:

  • Доля 1: локальное устройство пользователя
  • Доля 2: сервер авторизации
  • Доля 3: резервное хранилище (облако)
  • Доля 4: офлайн-носитель

Порог восстановления обычно ( t = 2 ) или ( t = 3 ), в зависимости от модели угроз.


Применение в реальных системах WebCrypto

Secret sharing используется как слой над WebCrypto в:

  • распределённых системах управления ключами
  • мультисигнальных кошельках
  • корпоративных системах доступа
  • безопасном восстановлении аккаунтов

WebCrypto при этом обеспечивает:

  • генерацию случайности
  • шифрование долей
  • защиту при передаче

А логика разделения реализуется полностью на стороне приложения.