Files
IdP/apps/frontend/lib/e2e-crypto.ts
2026-06-25 23:48:57 +03:00

122 lines
4.6 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const STORAGE_PREFIX = 'lendry-e2e-private:';
const ALGORITHM = { name: 'ECDH', namedCurve: 'P-256' } as const;
const AES = { name: 'AES-GCM', length: 256 } as const;
interface StoredKeyMaterial {
privateJwk: JsonWebKey;
publicKey: string;
}
function toBase64(buffer: ArrayBuffer) {
const bytes = new Uint8Array(buffer);
let binary = '';
bytes.forEach((byte) => {
binary += String.fromCharCode(byte);
});
return btoa(binary);
}
function fromBase64(value: string) {
const binary = atob(value);
const bytes = new Uint8Array(binary.length);
for (let index = 0; index < binary.length; index += 1) {
bytes[index] = binary.charCodeAt(index);
}
return bytes.buffer;
}
async function exportPublicKey(key: CryptoKey) {
const raw = await crypto.subtle.exportKey('spki', key);
return toBase64(raw);
}
async function importPublicKey(base64: string) {
return crypto.subtle.importKey('spki', fromBase64(base64), ALGORITHM, true, []);
}
async function loadStoredKeys(userId: string) {
const stored = localStorage.getItem(`${STORAGE_PREFIX}${userId}`);
if (!stored) return null;
const parsed = JSON.parse(stored) as StoredKeyMaterial;
const privateKey = await crypto.subtle.importKey('jwk', parsed.privateJwk, ALGORITHM, true, ['deriveKey']);
return { privateKey, publicKey: parsed.publicKey };
}
async function saveStoredKeys(userId: string, privateKey: CryptoKey, publicKey: string) {
const privateJwk = await crypto.subtle.exportKey('jwk', privateKey);
localStorage.setItem(
`${STORAGE_PREFIX}${userId}`,
JSON.stringify({ privateJwk, publicKey } satisfies StoredKeyMaterial)
);
}
export async function ensureE2EKeyPair(userId: string) {
const existing = await loadStoredKeys(userId);
if (existing) return existing;
const keyPair = await crypto.subtle.generateKey(ALGORITHM, true, ['deriveKey']);
const publicKey = await exportPublicKey(keyPair.publicKey);
await saveStoredKeys(userId, keyPair.privateKey, publicKey);
return { privateKey: keyPair.privateKey, publicKey };
}
async function deriveAesKey(privateKey: CryptoKey, peerPublicKeyBase64: string) {
const peerPublicKey = await importPublicKey(peerPublicKeyBase64);
return crypto.subtle.deriveKey({ name: 'ECDH', public: peerPublicKey }, privateKey, AES, false, ['encrypt', 'decrypt']);
}
export async function encryptDirectMessage(userId: string, peerPublicKeyBase64: string, plaintext: string) {
const keys = await ensureE2EKeyPair(userId);
const aesKey = await deriveAesKey(keys.privateKey, peerPublicKeyBase64);
const iv = crypto.getRandomValues(new Uint8Array(12));
const encoded = new TextEncoder().encode(plaintext);
const ciphertext = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, aesKey, encoded);
return JSON.stringify({
v: 1,
iv: toBase64(iv.buffer),
ciphertext: toBase64(ciphertext)
});
}
export async function decryptDirectMessage(userId: string, peerPublicKeyBase64: string, payload: string) {
const keys = await loadStoredKeys(userId);
if (!keys) {
return '🔒 Не удалось расшифровать сообщение';
}
try {
const parsed = JSON.parse(payload) as { iv?: string; ciphertext?: string };
if (!parsed.iv || !parsed.ciphertext) {
return payload;
}
const aesKey = await deriveAesKey(keys.privateKey, peerPublicKeyBase64);
const iv = new Uint8Array(fromBase64(parsed.iv));
const decrypted = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, aesKey, fromBase64(parsed.ciphertext));
return new TextDecoder().decode(decrypted);
} catch {
return '🔒 Не удалось расшифровать сообщение';
}
}
export async function encryptBinary(userId: string, peerPublicKeyBase64: string, data: ArrayBuffer) {
const keys = await ensureE2EKeyPair(userId);
const aesKey = await deriveAesKey(keys.privateKey, peerPublicKeyBase64);
const iv = crypto.getRandomValues(new Uint8Array(12));
const ciphertext = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, aesKey, data);
const packed = new Uint8Array(iv.byteLength + ciphertext.byteLength);
packed.set(iv, 0);
packed.set(new Uint8Array(ciphertext), iv.byteLength);
return packed.buffer;
}
export async function decryptBinary(userId: string, peerPublicKeyBase64: string, packed: ArrayBuffer) {
const keys = await loadStoredKeys(userId);
if (!keys) {
throw new Error('Локальный E2E-ключ не найден');
}
const bytes = new Uint8Array(packed);
const iv = bytes.slice(0, 12);
const ciphertext = bytes.slice(12);
const aesKey = await deriveAesKey(keys.privateKey, peerPublicKeyBase64);
return crypto.subtle.decrypt({ name: 'AES-GCM', iv }, aesKey, ciphertext);
}