slopus--happy
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
126 行
4.5 KiB
TypeScript
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;
|
|
}
|
|
}));
|
|
}
|
|
} |