项目文件夹

文件
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

229 行
8.2 KiB
TypeScript

import { deriveKey } from "@/encryption/deriveKey";
import { AES256Encryption, BoxEncryption, SecretBoxEncryption, Encryptor, Decryptor } from "./encryptor";
import { encodeHex } from "@/encryption/hex";
import { EncryptionCache } from "./encryptionCache";
import { SessionEncryption } from "./sessionEncryption";
import { MachineEncryption } from "./machineEncryption";
import { encodeBase64, decodeBase64 } from "@/encryption/base64";
import sodium from '@/encryption/libsodium.lib';
import { decryptBox, encryptBox } from "@/encryption/libsodium";
import { randomUUID } from 'expo-crypto';
export class Encryption {
static async create(masterSecret: Uint8Array) {
// Derive content data key to open session and machine records
const contentDataKey = await deriveKey(masterSecret, 'Happy EnCoder', ['content']);
// Derive content data key keypair
const contentKeyPair = sodium.crypto_box_seed_keypair(contentDataKey);
// Derive anonymous ID
const anonID = encodeHex((await deriveKey(masterSecret, 'Happy Coder', ['analytics', 'id']))).slice(0, 16).toLowerCase();
// Derive master blob key for legacy sessions (those with no per-session dataKey)
const masterBlobKey = await deriveKey(masterSecret, 'Happy Blobs', ['master']);
// Create encryption
return new Encryption(anonID, masterSecret, contentKeyPair, masterBlobKey);
}
private readonly legacyEncryption: SecretBoxEncryption;
private readonly contentKeyPair: sodium.KeyPair;
private readonly masterBlobKey: Uint8Array;
readonly anonID: string;
readonly contentDataKey: Uint8Array;
// Session and machine encryption management
private sessionEncryptions = new Map<string, SessionEncryption>();
private machineEncryptions = new Map<string, MachineEncryption>();
private sessionBlobKeys = new Map<string, Uint8Array>();
private cache: EncryptionCache;
private constructor(anonID: string, masterSecret: Uint8Array, contentKeyPair: sodium.KeyPair, masterBlobKey: Uint8Array) {
this.anonID = anonID;
this.contentKeyPair = contentKeyPair;
this.legacyEncryption = new SecretBoxEncryption(masterSecret);
this.masterBlobKey = masterBlobKey;
this.cache = new EncryptionCache();
this.contentDataKey = contentKeyPair.publicKey;
}
//
// Core encryption opening
//
async openEncryption(dataEncryptionKey: Uint8Array | null): Promise<Encryptor & Decryptor> {
if (!dataEncryptionKey) {
return this.legacyEncryption;
}
return new AES256Encryption(dataEncryptionKey);
}
//
// Session operations
//
/**
* Initialize sessions with their encryption keys
* This should be called once when sessions are loaded
*/
async initializeSessions(sessions: Map<string, Uint8Array | null>): Promise<void> {
for (const [sessionId, dataKey] of sessions) {
// Skip if already initialized
if (this.sessionEncryptions.has(sessionId)) {
continue;
}
// Create appropriate encryptor based on data key
const encryptor = await this.openEncryption(dataKey);
// Create and cache session encryption
const sessionEnc = new SessionEncryption(
sessionId,
encryptor,
this.cache
);
this.sessionEncryptions.set(sessionId, sessionEnc);
// Derive blob key for this session.
// Legacy sessions (null dataKey) use the master blob key.
// Newer sessions derive a subkey from their own dataKey so blobs
// are cryptographically isolated from message encryption.
const blobKey = dataKey
? await deriveKey(dataKey, 'Happy Blobs', ['session'])
: this.masterBlobKey;
this.sessionBlobKeys.set(sessionId, blobKey);
}
}
/**
* Get session encryption if it has been initialized
* Returns null if not initialized (should never happen in normal flow)
*/
getSessionEncryption(sessionId: string): SessionEncryption | null {
return this.sessionEncryptions.get(sessionId) || null;
}
/**
* Remove session encryption from memory when session is deleted
*/
removeSessionEncryption(sessionId: string): void {
this.sessionEncryptions.delete(sessionId);
this.sessionBlobKeys.delete(sessionId);
// Also clear any cached data for this session
this.cache.clearSessionCache(sessionId);
}
/**
* Get the 32-byte NaCl secretbox key for encrypting binary blobs
* (image attachments) in a session. Distinct from the message encryption
* key to maintain cryptographic separation.
* Returns null if the session has not been initialized.
*/
getSessionBlobKey(sessionId: string): Uint8Array | null {
return this.sessionBlobKeys.get(sessionId) ?? null;
}
//
// Machine operations
//
/**
* Initialize machines with their encryption keys
* This should be called once when machines are loaded
*/
async initializeMachines(machines: Map<string, Uint8Array | null>): Promise<void> {
for (const [machineId, dataKey] of machines) {
// Skip if already initialized
if (this.machineEncryptions.has(machineId)) {
continue;
}
// Create appropriate encryptor based on data key
const encryptor = await this.openEncryption(dataKey);
// Create and cache machine encryption
const machineEnc = new MachineEncryption(
machineId,
encryptor,
this.cache
);
this.machineEncryptions.set(machineId, machineEnc);
}
}
/**
* Get machine encryption if it has been initialized
* Returns null if not initialized (should never happen in normal flow)
*/
getMachineEncryption(machineId: string): MachineEncryption | null {
return this.machineEncryptions.get(machineId) || null;
}
/**
* Remove machine encryption from memory when the machine is deleted
*/
removeMachineEncryption(machineId: string): void {
this.machineEncryptions.delete(machineId);
}
//
// Legacy methods for machine metadata (temporary until machines are migrated)
//
async encryptRaw(data: any): Promise<string> {
const encrypted = await this.legacyEncryption.encrypt([data]);
return encodeBase64(encrypted[0], 'base64');
}
async decryptRaw(encrypted: string): Promise<any | null> {
try {
const encryptedData = decodeBase64(encrypted, 'base64');
const decrypted = await this.legacyEncryption.decrypt([encryptedData]);
return decrypted[0] || null;
} catch (error) {
return null;
}
}
//
// Data Encryption Key decryption
//
async decryptEncryptionKey(encrypted: string) {
// Never throw: callers (fetchMachines/fetchSessions/artifacts) iterate
// many keys, and an exception on one malformed/foreign key would
// reject the whole sync and silently drop every item. Always degrade
// to null so the caller can decide per-item.
try {
const encryptedKey = decodeBase64(encrypted, 'base64');
if (encryptedKey[0] !== 0) {
return null;
}
const decrypted = decryptBox(encryptedKey.slice(1), this.contentKeyPair.privateKey);
if (!decrypted) {
return null;
}
return decrypted;
} catch (error) {
console.error('decryptEncryptionKey failed:', error);
return null;
}
}
async encryptEncryptionKey(key: Uint8Array): Promise<Uint8Array> {
// Use public key for encryption (encrypt TO ourselves)
const encrypted = encryptBox(key, this.contentKeyPair.publicKey);
const result = new Uint8Array(encrypted.length + 1);
result[0] = 0; // Version byte
result.set(encrypted, 1);
return result;
}
generateId(): string {
return randomUUID();
}
}