项目文件夹

文件
wehub-resource-sync 98e40dac97
CLI Smoke Test / smoke-test-linux (20) (push) Has been cancelled
CLI Smoke Test / smoke-test-linux (24) (push) Has been cancelled
CLI Smoke Test / smoke-test-windows (20) (push) Has been cancelled
CLI Smoke Test / smoke-test-windows (24) (push) Has been cancelled
Expo App TypeScript typecheck / typecheck (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:40:49 +08:00

126 行
4.5 KiB
TypeScript

import { decryptBox, decryptSecretBox, encryptBox, encryptSecretBox } from "@/encryption/libsodium";
import { encodeBase64, decodeBase64 } from "@/encryption/base64";
import sodium from '@/encryption/libsodium.lib';
import { decodeUTF8, encodeUTF8 } from "@/encryption/text";
import { decryptAESGCMString, encryptAESGCMString } from "@/encryption/aes";
//
// IMPORTANT: Right now there is a bug in the AES implementation and it works only with a normal strings converted to Uint8Array.
// Any abnormal string might break encoding and decoding utf8.
//
export interface Encryptor {
encrypt(data: any[]): Promise<Uint8Array[]>;
}
export interface Decryptor {
decrypt(data: Uint8Array[]): Promise<(any | null)[]>;
}
export class SecretBoxEncryption implements Encryptor, Decryptor {
private readonly secretKey: Uint8Array;
constructor(secretKey: Uint8Array) {
this.secretKey = secretKey;
}
async decrypt(data: Uint8Array[]): Promise<(any | null)[]> {
// Process as batch, not Promise.all - more efficient
const results: (any | null)[] = [];
for (const item of data) {
results.push(decryptSecretBox(item, this.secretKey));
}
return results;
}
async encrypt(data: any[]): Promise<Uint8Array[]> {
// Process as batch, not Promise.all - more efficient
const results: Uint8Array[] = [];
for (const item of data) {
results.push(encryptSecretBox(item, this.secretKey));
}
return results;
}
}
export class BoxEncryption implements Encryptor, Decryptor {
private readonly privateKey: Uint8Array;
private readonly publicKey: Uint8Array;
constructor(seed: Uint8Array) {
// Use the seed to generate a proper keypair
const keypair = sodium.crypto_box_seed_keypair(seed);
this.privateKey = keypair.privateKey;
this.publicKey = keypair.publicKey;
}
async encrypt(data: any[]): Promise<Uint8Array[]> {
// Process as batch, not Promise.all - more efficient
const results: Uint8Array[] = [];
for (const item of data) {
results.push(encryptBox(encodeUTF8(JSON.stringify(item)), this.publicKey));
}
return results;
}
async decrypt(data: Uint8Array[]): Promise<(any | null)[]> {
// Process as batch, not Promise.all - more efficient
const results: (any | null)[] = [];
for (const item of data) {
let decrypted = decryptBox(item, this.privateKey);
if (!decrypted) {
results.push(null);
continue;
}
results.push(JSON.parse(decodeUTF8(decrypted)));
}
return results;
}
}
export class AES256Encryption implements Encryptor, Decryptor {
private readonly secretKey: Uint8Array;
private readonly secretKeyB64: string;
constructor(secretKey: Uint8Array) {
this.secretKey = secretKey;
this.secretKeyB64 = encodeBase64(secretKey);
}
async encrypt(data: any[]): Promise<Uint8Array[]> {
// Process as batch, not Promise.all - more efficient
const results: Uint8Array[] = [];
for (const item of data) {
// Serialize to JSON string first
const encrypted = decodeBase64(await encryptAESGCMString(JSON.stringify(item), this.secretKeyB64));
let output = new Uint8Array(encrypted.length + 1);
output[0] = 0;
output.set(encrypted, 1);
results.push(output);
}
return results;
}
async decrypt(data: Uint8Array[]): Promise<(any | null)[]> {
// Decrypt items concurrently. The previous implementation used a
// sequential for-await loop, which serialised every AES-GCM call on
// the JS thread. For a 1000-message session that meant ~1000
// serialised crypto operations before the UI could display anything.
// Promise.all schedules them on the microtask queue, allowing the
// crypto subtle backend (and any native bridge work) to interleave.
return Promise.all(data.map(async (item) => {
try {
if (item[0] !== 0) {
return null;
}
const decryptedString = await decryptAESGCMString(encodeBase64(item.slice(1)), this.secretKeyB64);
if (!decryptedString) {
return null;
}
return JSON.parse(decryptedString);
} catch (error) {
return null;
}
}));
}
}