global update and global fix
This commit is contained in:
121
apps/frontend/lib/e2e-crypto.ts
Normal file
121
apps/frontend/lib/e2e-crypto.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user