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}
Где:
Каждая доля вычисляется как точка:
(x_i, y_i) = (x_i, f(x_i))
Для восстановления секрета требуется минимум ( t ) точек.
Критически важный компонент — генерация коэффициентов полинома. В браузере это делается через:
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
Использование модуля предотвращает переполнение и обеспечивает корректность восстановления.
Базовая реализация:
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;
}
Secret sharing сам по себе не обеспечивает конфиденциальность каждой доли. Поэтому часто применяется дополнительное шифрование долей.
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 };
}
Типовая архитектура:
Такое разделение защищает от:
Использование недостаточно большого простого числа
Если модуль ( p ) мал, возможны атаки перебором.
Использование Number вместо BigInt
JavaScript Number не подходит для криптографических операций из-за потери точности.
Повторное использование коэффициентов
Случайные коэффициенты должны генерироваться заново для каждого нового секрета.
Хранение всех долей на одном устройстве
Это полностью нивелирует смысл схемы.
WebCrypto ограничен песочницей:
Поэтому секретное разделение часто комбинируется с:
Типовая схема использования:
Порог восстановления обычно ( t = 2 ) или ( t = 3 ), в зависимости от модели угроз.
Secret sharing используется как слой над WebCrypto в:
WebCrypto при этом обеспечивает:
А логика разделения реализуется полностью на стороне приложения.