Уязвимости безопасности в сквозном шифровании E2EE
Анализ уязвимостей E2EE с использованием ECDH и AES-GCM. Риски и рекомендации по улучшению безопасности.
Каковы потенциальные уязвимости в безопасности при использовании сквозного шифрования (E2EE) в веб-приложении, где сервер контролируется разработчиком, но считается недоверенным лицом? В частности, какие риски существуют в предложенной реализации, использующей ECDH для обмена ключами и AES-GCM для шифрования, с учетом следующего JS-кода на стороне клиента?
class msgCrypt {
constructor(publicKeyJwk, privateKeyJwk) {
this.publicKeyJwk = publicKeyJwk;
this.privateKeyJwk = privateKeyJwk;
}
static async generateKeyPair() {
const keyPair = await window.crypto.subtle.generateKey(
{
name: "ECDH",
namedCurve: "P-256",
},
true,
["deriveKey", "deriveBits"]
);
const publicKeyJwk = await window.crypto.subtle.exportKey(
"jwk",
keyPair.publicKey
);
const privateKeyJwk = await window.crypto.subtle.exportKey(
"jwk",
keyPair.privateKey
);
return {publicKeyJwk, privateKeyJwk};
}
async deriveKey() {
const publicKeyJwk = this.publicKeyJwk;
const privateKeyJwk = this.privateKeyJwk;
const publicKey = await window.crypto.subtle.importKey(
"jwk",
publicKeyJwk,
{
name: "ECDH",
namedCurve: "P-256",
},
true,
[]
);
const privateKey = await window.crypto.subtle.importKey(
"jwk",
privateKeyJwk,
{
name: "ECDH",
namedCurve: "P-256",
},
true,
["deriveKey", "deriveBits"]
);
return await window.crypto.subtle.deriveKey(
{name: "ECDH", public: publicKey},
privateKey,
{name: "AES-GCM", length: 256},
true,
["encrypt", "decrypt"]
);
};
async encrypt(text) {
const derivedKey = await this.deriveKey();
const encodedText = new TextEncoder().encode(text);
const iv = window.crypto.getRandomValues(new Uint8Array(12));
const encryptedData = await window.crypto.subtle.encrypt(
{name: "AES-GCM", iv: iv},
derivedKey,
encodedText
);
const uintArray = new Uint8Array(encryptedData);
const string = String.fromCharCode.apply(null, uintArray);
const iv_string = String.fromCharCode.apply(null, iv);
const bDATA = btoa(string);
const bIV = btoa(iv_string);
return JSON.stringify({bDATA, bIV});
};
async decrypt(messageJSON) {
try {
const derivedKey = await this.deriveKey();
const message = JSON.parse(messageJSON);
const text = message.bDATA;
const initializationVector = message.bIV;
const iv_string = atob(initializationVector);
const iv = new Uint8Array(
[...iv_string].map((char) => char.charCodeAt(0))
);
const string = atob(text);
const uintArray = new Uint8Array(
[...string].map((char) => char.charCodeAt(0))
);
const algorithm = {
name: "AES-GCM",
iv: iv,
};
const decryptedData = await window.crypto.subtle.decrypt(
algorithm,
derivedKey,
uintArray
);
return new TextDecoder().decode(decryptedData);
} catch (e) {
return `error decrypting message: ${e}`;
}
};
}
Какие угрозы безопасности остаются, если исключить компрометацию устройства пользователя? Какие улучшения можно внести в этот код для повышения безопасности?
Система сквозного шифрования (E2EE) с использованием ECDH и AES-GCM содержит критические уязвимости даже при условии контроля сервера разработчиком. Основные риски связаны с отсутствием проверки открытых ключей, потенциальным повторным использованием IV в AES-GCM и небезопасной производной ключей, что может привести к атакам подстановки данных и компрометации конфиденциальности.
Содержание
- Анализ архитектуры E2EE
- Уязвимости в протоколе ECDH
- Проблемы реализации AES-GCM
- Риски в производной ключей
- Уязвимости в коде клиента
- Рекомендации по улучшению безопасности
- Заключение
Анализ архитектуры E2EE
Представленная система использует ECDH для обмена ключами и AES-GCM для шифрования данных. Хотя архитектура обеспечивает базовый уровень конфиденциальности, её реализация содержит критические недостатки. Согласно OWASP Web Security Testing Guide, системы E2EE с доверенными серверами всё равно подверж атакам, связанным с криптографическими протоколами, а не только с компрометацией устройств.
В текущей реализации сервер контролируется разработчиком, но считается недоверенным. Это означает, что злоумышленник, получивший доступ к серверу, может манипулировать ключами и шифрованными данными. Основные угрозы исходят от слабостей в протоколе ECDH и использовании AES-GCM без должной защиты от повторного использования IV.
Уязвимости в протоколе ECDH
Ключевая проблема кода — отсутствие проверки открытых ключей ECDH. Как указано в RFC 8446, валидация открытых ключей обязательна для предотвращения атак на малые подгруппы и некорректные кривые. В текущей реализации:
const publicKey = await window.crypto.subtle.importKey(
"jwk", publicKeyJwk,
{name: "ECDH", namedCurve: "P-256"},
true, []
);
- Нет проверки принадлежности точки кривой P-256
- Отсутствует проверка порядка открытого ключа
- Не предотвращается атака через некорректную кривую
По данным NIST SP 800-56A Rev 3, это позволяет злоумышленнику:
- Принудить генерацию слабого общего секрета
- Выполнить атаку через малую подгруппу
- Получить предсказуемые ключи для AES-GCM
Проблемы реализации AES-GCM
В методе encrypt() генерируется случайный IV, но нет защиты от повторного использования:
const iv = window.crypto.getRandomValues(new Uint8Array(12));
Как подчеркивается в NIST SP 800-38D, повторное использование IV с тем же ключом в AES-GCM катастрофоопасно. В текущей реализации:
- Нет отслеживания использованных IV
- Нет механизма предотвращения коллизий IV
- Отсутствует nonce management
Это приводит к:
- Полной компрометации ранее зашифрованных сообщений
- Возможности подделки аутентификационных тегов
- Раскрытию ключей шифрования
Риски в производной ключей
Метод deriveKey() производит ключ AES-GCM без контекстной информации:
return await window.crypto.subtle.deriveKey(
{name: "ECDH", public: publicKey},
privateKey,
{name: "AES-GCM", length: 256},
true, ["encrypt", "decrypt"]
);
Согласно NIST SP 800-56A Rev 3, в производные ключи должны включаться контекстные данные. Отсутствие этого в реализации:
- Снижает энтропию ключей
- Упрощает криптоанализ с использованием нескольких сессий
- Не обеспечивает изоляцию ключей для разных контекстов
Уязвимости в коде клиента
Дополнительные проблемы в предоставленном коде:
-
Обработка ошибок в
decrypt():javascriptcatch (e) { return `error decrypting message: ${e}`; }Раскрытие информации об ошибках может помочь атакующему в анализе системы.
-
Временные метки ключей:
В методеgenerateKeyPair()нет отслеживания времени жизни ключей, что увеличивает окно для атак. -
Уязвимость к replay-атакам:
Отсутствует защита против повторной отправки сообщений или ключей.
Рекомендации по улучшению безопасности
1. Валидация открытых ключей ECDH
Добавить проверку открытых ключей:
// Перед импортом ключа
const publicKeyJwk = addPublicKeyValidation(publicKeyJwk);
function addPublicKeyValidation(jwk) {
// Проверка принадлежности кривой
if (jwk.crv !== "P-256") throw new Error("Invalid curve");
// Проверка порядка ключа
const x = BigInt(jwk.x);
const y = BigInt(jwk.y);
const p = BigInt("0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF");
const n = BigInt("0FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC");
if (x >= p || y >= p) throw new Error("Invalid coordinate");
if (!isValidPointOnCurve(x, y, p)) throw new Error("Point not on curve");
if (multiplyPoint(x, y, n, p) !== null) throw new Error("Invalid key order");
return jwk;
}
2. Защита от повторного использования IV
Реализовать управление nonce:
class msgCrypt {
constructor(publicKeyJwk, privateKeyJwk) {
// ... существующий код
this.usedIVs = new Set(); // Для отслеживания используемых IV
}
async encrypt(text) {
// ... существующий код до генерации IV
// Проверка уникальности IV
while (this.usedIVs.has(ivString)) {
iv = window.crypto.getRandomValues(new Uint8Array(12));
ivString = String.fromCharCode.apply(null, iv);
}
this.usedIVs.add(ivString);
// ... остальной код
}
}
3. Контекстная производная ключей
Добавить контекстные данные:
async deriveKey(context = "") {
// ... существующий код
const contextBits = new TextEncoder().encode(context);
const salt = window.crypto.getRandomValues(new Uint8Array(16));
return await window.crypto.subtle.deriveKey(
{
name: "ECDH",
public: publicKey,
salt: salt,
info: contextBits
},
privateKey,
{name: "AES-GCM", length: 256},
true,
["encrypt", "decrypt"]
);
}
4. Улучшение обработки ошибок
Заменить раскрытие информации на безопасную обработку:
async decrypt(messageJSON) {
try {
// ... существующий код
} catch (e) {
// Логирование на сервере (без раскрытия пользователю)
console.error("Decryption failed", e);
return "Ошибка обработки сообщения";
}
}
Заключение
Система сквозного шифрования с текущей реализацией ECDH и AES-GCM содержит критические уязвимости в области валидации ключей, управления nonce и производных ключей. Основные риски связаны с возможностью атак через некорректные открытые ключи, повторным использованием IV и отсутствием контекстной информации в производных ключах. Для повышения безопасности необходимо внедрить проверку открытых ключей ECDH, реализовать систему управления nonce, добавить контекстные данные при производной ключей и улучшить обработку ошибок. Эти изменения значительно снизят риски компрометации систем E2EE при контролируемом, но недоверенном сервере.