global update and global fix

This commit is contained in:
lendry
2026-06-25 23:48:57 +03:00
parent 3880c68d59
commit b0ea87e898
121 changed files with 13759 additions and 663 deletions

View 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);
}