Виртуальная задача(упрощённо):
Обмен сообщениями(данными) со сквозным шифрованием через WEB.
"Создатель сервера" - недоверенное лицо - но сервер под контролем разработчика.
Решение "с наскока":
- Стандартная авторизация WEB-приложения;
- Генерируем ключи ECDH(если до этого на устройстве их нет - или они невалидные);
- Храним приватный ключ на клиенте(в браузере в данном случае...или пойти дальше - в приложении);
- Публичный - используем как хотим в пределах системы и обмена между участниками;
- При шифровке/дешифровке данных используем derivedKey(общий ключ шифирования для публичного "получателя" и приватный ключ "отправителя" - и наоборот);
- При смене устройства(где хранится приватный ключ пользователя) - предлагать синхронизировать приватные ключи(аля QR) или введя доп. пинкод(который задаётся при регистрации...или который высылается по другой "довереннной" связи);
- Или предлагать "сбросить всё" - в случае - если нет "пин-кода", НО при этом - пройдена авторизация WEB-приложения(тем самым
предыдущая переписка будет недоступна третьим лицам);
- При смене(сброса) публичного ключа - "вторая сторона" будет оповещена об этом - и далее "эта сторона" сама решает о "ликвидности" общения со стороной изменившей "публичный ключ".
Минимальный 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}`;
}
};
}
Остальное - "детали" - если взять за "ядро" - JS выше...
Вопрос:
Какие видятся минусы с точки зрения
безопасности пересылаемых данных?
Если "отбросить" самый главный минус - "скомпрометированное устройство" самого обладателя.