mirror of
https://github.com/BigBodyCobain/Shadowbroker.git
synced 2026-08-19 00:47:22 +02:00
release: prepare v0.9.7
This commit is contained in:
@@ -0,0 +1,410 @@
|
||||
import initPrivacyCore, {
|
||||
wasm_gate_decrypt,
|
||||
wasm_gate_encrypt,
|
||||
wasm_gate_export_state,
|
||||
wasm_gate_import_state,
|
||||
wasm_release_group,
|
||||
wasm_release_identity,
|
||||
wasm_reset_all_state,
|
||||
} from './privacyCoreWasm/privacy_core';
|
||||
import {
|
||||
clearWorkerGateStates,
|
||||
deleteWorkerGateState,
|
||||
probeWorkerGateVaultAvailability,
|
||||
readWorkerGateState,
|
||||
writeWorkerGateState,
|
||||
type WorkerGateStateMember,
|
||||
type WorkerGateStateSnapshot,
|
||||
} from './meshGateWorkerVault';
|
||||
|
||||
export type LocalGateComposeResult = {
|
||||
gate_id: string;
|
||||
epoch: number;
|
||||
ciphertext: string;
|
||||
nonce: string;
|
||||
};
|
||||
|
||||
export type LocalGateDecryptResult = {
|
||||
ok: boolean;
|
||||
gate_id: string;
|
||||
epoch?: number;
|
||||
plaintext?: string;
|
||||
reply_to?: string;
|
||||
detail?: string;
|
||||
identity_scope?: string;
|
||||
};
|
||||
|
||||
export type InlineGateCryptoSupport = {
|
||||
supported: boolean;
|
||||
reason: string;
|
||||
};
|
||||
|
||||
type GateStateImportMapping = {
|
||||
identities: Record<string, number>;
|
||||
groups: Record<string, number>;
|
||||
};
|
||||
|
||||
type ImportedGateState = {
|
||||
snapshot: WorkerGateStateSnapshot;
|
||||
identityHandles: number[];
|
||||
groupHandles: number[];
|
||||
activeGroupHandle: number;
|
||||
members: WorkerGateStateMember[];
|
||||
};
|
||||
|
||||
const GATE_BUCKETS = [192, 384, 768, 1536, 3072, 6144];
|
||||
|
||||
let wasmReady: Promise<void> | null = null;
|
||||
const gateStateCache = new Map<string, ImportedGateState>();
|
||||
|
||||
function normalizeGateId(gateId: string): string {
|
||||
return String(gateId || '').trim().toLowerCase();
|
||||
}
|
||||
|
||||
function bytesToBase64(bytes: Uint8Array): string {
|
||||
return btoa(String.fromCharCode(...bytes));
|
||||
}
|
||||
|
||||
function base64ToBytes(value: string): Uint8Array {
|
||||
const binary = atob(String(value || '').trim());
|
||||
const out = new Uint8Array(binary.length);
|
||||
for (let i = 0; i < binary.length; i += 1) {
|
||||
out[i] = binary.charCodeAt(i);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function padGateCiphertext(raw: Uint8Array): Uint8Array {
|
||||
const prefixed = new Uint8Array(raw.length + 2);
|
||||
const len = Math.min(raw.length, 0xffff);
|
||||
prefixed[0] = (len >> 8) & 0xff;
|
||||
prefixed[1] = len & 0xff;
|
||||
prefixed.set(raw, 2);
|
||||
for (const bucket of GATE_BUCKETS) {
|
||||
if (prefixed.length <= bucket) {
|
||||
const out = new Uint8Array(bucket);
|
||||
out.set(prefixed);
|
||||
return out;
|
||||
}
|
||||
}
|
||||
const lastBucket = GATE_BUCKETS[GATE_BUCKETS.length - 1] || 6144;
|
||||
const target = Math.ceil(prefixed.length / lastBucket) * lastBucket;
|
||||
const out = new Uint8Array(target);
|
||||
out.set(prefixed);
|
||||
return out;
|
||||
}
|
||||
|
||||
function unpadGateCiphertext(padded: Uint8Array): Uint8Array {
|
||||
if (padded.length < 2) return padded;
|
||||
const originalLen = ((padded[0] << 8) | padded[1]) >>> 0;
|
||||
if (originalLen <= 0 || originalLen + 2 > padded.length) {
|
||||
return padded;
|
||||
}
|
||||
return padded.slice(2, 2 + originalLen);
|
||||
}
|
||||
|
||||
function encodeGateCiphertext(raw: Uint8Array): string {
|
||||
return bytesToBase64(padGateCiphertext(raw));
|
||||
}
|
||||
|
||||
function decodeGateCiphertext(ciphertextB64: string): Uint8Array {
|
||||
return unpadGateCiphertext(base64ToBytes(ciphertextB64));
|
||||
}
|
||||
|
||||
function encodeGatePlaintext(plaintext: string, epoch: number, replyTo: string = ''): Uint8Array {
|
||||
const normalizedReplyTo = String(replyTo || '').trim();
|
||||
return new TextEncoder().encode(
|
||||
JSON.stringify({
|
||||
m: plaintext,
|
||||
e: epoch,
|
||||
...(normalizedReplyTo ? { r: normalizedReplyTo } : {}),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
function decodeGatePlaintext(ciphertextOpen: Uint8Array, fallbackEpoch: number): {
|
||||
plaintext: string;
|
||||
epoch: number;
|
||||
reply_to: string;
|
||||
} {
|
||||
const raw = new TextDecoder().decode(ciphertextOpen);
|
||||
try {
|
||||
const parsed = JSON.parse(raw) as { m?: string; e?: number; r?: string };
|
||||
return {
|
||||
plaintext: typeof parsed?.m === 'string' ? parsed.m : raw,
|
||||
epoch: Number.isFinite(parsed?.e) ? Number(parsed.e) : fallbackEpoch,
|
||||
reply_to: typeof parsed?.r === 'string' ? parsed.r : '',
|
||||
};
|
||||
} catch {
|
||||
return { plaintext: raw, epoch: fallbackEpoch, reply_to: '' };
|
||||
}
|
||||
}
|
||||
|
||||
function generateGateNonce(): string {
|
||||
const bytes = new Uint8Array(12);
|
||||
crypto.getRandomValues(bytes);
|
||||
return bytesToBase64(bytes);
|
||||
}
|
||||
|
||||
function gateMemberMatchesActive(
|
||||
snapshot: WorkerGateStateSnapshot,
|
||||
member: WorkerGateStateMember,
|
||||
): boolean {
|
||||
const activeScope = String(snapshot.active_identity_scope || '').trim().toLowerCase();
|
||||
if (activeScope === 'persona') {
|
||||
const activePersonaId = String(snapshot.active_persona_id || '').trim();
|
||||
return Boolean(activePersonaId) && String(member.persona_id || '').trim() === activePersonaId;
|
||||
}
|
||||
const activeNodeId = String(snapshot.active_node_id || '').trim();
|
||||
return (
|
||||
Boolean(activeNodeId) &&
|
||||
String(member.node_id || '').trim() === activeNodeId &&
|
||||
String(member.identity_scope || '').trim().toLowerCase() === 'anonymous'
|
||||
);
|
||||
}
|
||||
|
||||
async function ensureWasm(): Promise<void> {
|
||||
if (!wasmReady) {
|
||||
wasmReady = initPrivacyCore().then(() => undefined);
|
||||
}
|
||||
await wasmReady;
|
||||
}
|
||||
|
||||
function releaseImportedState(imported: ImportedGateState): void {
|
||||
for (const groupHandle of imported.groupHandles) {
|
||||
try {
|
||||
wasm_release_group(BigInt(groupHandle));
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
for (const identityHandle of imported.identityHandles) {
|
||||
try {
|
||||
wasm_release_identity(BigInt(identityHandle));
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function cacheImportedState(imported: ImportedGateState): void {
|
||||
const gateId = normalizeGateId(imported.snapshot.gate_id);
|
||||
const previous = gateStateCache.get(gateId);
|
||||
if (previous) {
|
||||
releaseImportedState(previous);
|
||||
}
|
||||
gateStateCache.set(gateId, imported);
|
||||
}
|
||||
|
||||
function parseImportMapping(json: string): GateStateImportMapping {
|
||||
const parsed = JSON.parse(json) as Partial<GateStateImportMapping>;
|
||||
return {
|
||||
identities: parsed.identities || {},
|
||||
groups: parsed.groups || {},
|
||||
};
|
||||
}
|
||||
|
||||
async function importSnapshot(snapshot: WorkerGateStateSnapshot): Promise<ImportedGateState> {
|
||||
await ensureWasm();
|
||||
const blob = base64ToBytes(snapshot.rust_state_blob_b64);
|
||||
const mapping = parseImportMapping(wasm_gate_import_state(blob));
|
||||
const remappedMembers = (snapshot.members || []).map((member) => {
|
||||
const key = String(member.group_handle);
|
||||
const mapped = Number(mapping.groups[key] || 0);
|
||||
if (!mapped) {
|
||||
throw new Error(`browser_gate_state_mapping_missing_group:${key}`);
|
||||
}
|
||||
return {
|
||||
...member,
|
||||
group_handle: mapped,
|
||||
};
|
||||
});
|
||||
const activeMember = remappedMembers.find((member) => gateMemberMatchesActive(snapshot, member));
|
||||
if (!activeMember?.group_handle) {
|
||||
throw new Error('browser_gate_state_active_member_missing');
|
||||
}
|
||||
return {
|
||||
snapshot: {
|
||||
...snapshot,
|
||||
gate_id: normalizeGateId(snapshot.gate_id),
|
||||
members: remappedMembers,
|
||||
},
|
||||
identityHandles: Object.values(mapping.identities || {}).map((value) => Number(value)),
|
||||
groupHandles: Array.from(
|
||||
new Set(remappedMembers.map((member) => Number(member.group_handle)).filter(Boolean)),
|
||||
),
|
||||
activeGroupHandle: Number(activeMember.group_handle),
|
||||
members: remappedMembers,
|
||||
};
|
||||
}
|
||||
|
||||
async function persistImportedState(imported: ImportedGateState): Promise<void> {
|
||||
await ensureWasm();
|
||||
const blob = wasm_gate_export_state(
|
||||
JSON.stringify(imported.identityHandles),
|
||||
JSON.stringify(imported.groupHandles),
|
||||
);
|
||||
const snapshot: WorkerGateStateSnapshot = {
|
||||
...imported.snapshot,
|
||||
gate_id: normalizeGateId(imported.snapshot.gate_id),
|
||||
rust_state_blob_b64: bytesToBase64(blob),
|
||||
members: imported.members,
|
||||
};
|
||||
imported.snapshot = snapshot;
|
||||
await writeWorkerGateState(snapshot);
|
||||
}
|
||||
|
||||
async function ensureImportedGateState(gateId: string): Promise<ImportedGateState> {
|
||||
const normalized = normalizeGateId(gateId);
|
||||
const cached = gateStateCache.get(normalized);
|
||||
if (cached) return cached;
|
||||
const snapshot = await readWorkerGateState(normalized);
|
||||
if (!snapshot) {
|
||||
throw new Error(`browser_gate_state_resync_required:${normalized}`);
|
||||
}
|
||||
const imported = await importSnapshot(snapshot);
|
||||
cacheImportedState(imported);
|
||||
return imported;
|
||||
}
|
||||
|
||||
export async function isInlineGateCryptoSupported(): Promise<boolean> {
|
||||
const support = await probeInlineGateCryptoSupport();
|
||||
return support.supported;
|
||||
}
|
||||
|
||||
export async function probeInlineGateCryptoSupport(): Promise<InlineGateCryptoSupport> {
|
||||
if (
|
||||
typeof window === 'undefined' ||
|
||||
typeof crypto === 'undefined'
|
||||
) {
|
||||
return { supported: false, reason: 'browser_runtime_unavailable' };
|
||||
}
|
||||
if (!crypto?.subtle) {
|
||||
return { supported: false, reason: 'browser_gate_webcrypto_unavailable' };
|
||||
}
|
||||
if (typeof indexedDB === 'undefined') {
|
||||
return { supported: false, reason: 'browser_gate_indexeddb_unavailable' };
|
||||
}
|
||||
const vault = await probeWorkerGateVaultAvailability();
|
||||
if (!vault.ok) {
|
||||
return { supported: false, reason: vault.reason || 'browser_gate_storage_unavailable' };
|
||||
}
|
||||
try {
|
||||
await ensureWasm();
|
||||
return { supported: true, reason: '' };
|
||||
} catch (error) {
|
||||
return {
|
||||
supported: false,
|
||||
reason:
|
||||
(error instanceof Error ? error.message : String(error || '')).trim() ||
|
||||
'browser_gate_wasm_unavailable',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function adoptInlineGateState(snapshot: WorkerGateStateSnapshot): Promise<WorkerGateStateSnapshot> {
|
||||
const imported = await importSnapshot(snapshot);
|
||||
cacheImportedState(imported);
|
||||
await persistImportedState(imported);
|
||||
return imported.snapshot;
|
||||
}
|
||||
|
||||
export async function composeInlineGateMessage(
|
||||
gateId: string,
|
||||
plaintext: string,
|
||||
replyTo: string = '',
|
||||
): Promise<LocalGateComposeResult> {
|
||||
const imported = await ensureImportedGateState(gateId);
|
||||
const encodedPlaintext = encodeGatePlaintext(
|
||||
plaintext,
|
||||
Number(imported.snapshot.epoch || 0),
|
||||
replyTo,
|
||||
);
|
||||
const rawCiphertext = wasm_gate_encrypt(BigInt(imported.activeGroupHandle), encodedPlaintext);
|
||||
await persistImportedState(imported);
|
||||
return {
|
||||
gate_id: normalizeGateId(gateId),
|
||||
epoch: Number(imported.snapshot.epoch || 0),
|
||||
ciphertext: encodeGateCiphertext(rawCiphertext),
|
||||
nonce: generateGateNonce(),
|
||||
};
|
||||
}
|
||||
|
||||
export async function decryptInlineGateMessages(
|
||||
messages: Array<{ gate_id: string; epoch?: number; ciphertext: string }>,
|
||||
): Promise<LocalGateDecryptResult[]> {
|
||||
const results: LocalGateDecryptResult[] = [];
|
||||
for (const message of messages) {
|
||||
const gateId = normalizeGateId(message.gate_id);
|
||||
try {
|
||||
const imported = await ensureImportedGateState(gateId);
|
||||
const requestedEpoch = Number(message.epoch || 0);
|
||||
if (requestedEpoch > 0 && requestedEpoch > Number(imported.snapshot.epoch || 0)) {
|
||||
results.push({
|
||||
ok: false,
|
||||
detail: `browser_gate_state_resync_required:${gateId}`,
|
||||
gate_id: gateId,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
const ciphertext = decodeGateCiphertext(message.ciphertext);
|
||||
let opened: Uint8Array | null = null;
|
||||
for (const groupHandle of imported.groupHandles) {
|
||||
try {
|
||||
opened = wasm_gate_decrypt(BigInt(groupHandle), ciphertext);
|
||||
break;
|
||||
} catch {
|
||||
/* keep trying remapped members */
|
||||
}
|
||||
}
|
||||
if (!opened) {
|
||||
results.push({
|
||||
ok: false,
|
||||
detail: 'gate_mls_decrypt_failed',
|
||||
gate_id: gateId,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
const decoded = decodeGatePlaintext(opened, requestedEpoch || Number(imported.snapshot.epoch || 0));
|
||||
await persistImportedState(imported);
|
||||
results.push({
|
||||
ok: true,
|
||||
gate_id: gateId,
|
||||
epoch: decoded.epoch,
|
||||
plaintext: decoded.plaintext,
|
||||
reply_to: decoded.reply_to,
|
||||
identity_scope: 'browser_privacy_core',
|
||||
});
|
||||
} catch (error) {
|
||||
results.push({
|
||||
ok: false,
|
||||
detail: error instanceof Error ? error.message : 'browser_gate_crypto_error',
|
||||
gate_id: gateId,
|
||||
});
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
export async function forgetInlineGateState(gateId?: string): Promise<void> {
|
||||
const normalized = normalizeGateId(gateId || '');
|
||||
if (!normalized) {
|
||||
for (const imported of gateStateCache.values()) {
|
||||
releaseImportedState(imported);
|
||||
}
|
||||
gateStateCache.clear();
|
||||
try {
|
||||
wasm_reset_all_state();
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
await clearWorkerGateStates();
|
||||
return;
|
||||
}
|
||||
const existing = gateStateCache.get(normalized);
|
||||
if (existing) {
|
||||
releaseImportedState(existing);
|
||||
gateStateCache.delete(normalized);
|
||||
}
|
||||
await deleteWorkerGateState(normalized);
|
||||
}
|
||||
Reference in New Issue
Block a user