Working one-time message encryption and decryption

This commit is contained in:
2025-12-03 16:40:26 +03:00
Unverified
parent d4b1e261d7
commit fcb3dff2c9
18 changed files with 1392 additions and 336 deletions
+76
View File
@@ -0,0 +1,76 @@
import { randomBytes } from "./kdf";
/**
* Padding sizes that look like normal HTTP/WebSocket traffic
* These sizes are common in real web traffic to avoid fingerprinting
*/
const PADDING_BUCKETS = [64, 128, 256, 512, 1024, 2048, 4096];
/**
* Adds padding to a message to make it resistant to size-based fingerprinting
* Pads to the nearest bucket size to make all messages look similar
* @param data - The data to pad
* @returns Padded data with padding length prefix
*/
export function addPadding(data: string): string {
const dataBytes = new TextEncoder().encode(data);
const dataSize = dataBytes.length;
// Find the smallest bucket that fits the data
let targetSize = PADDING_BUCKETS[PADDING_BUCKETS.length - 1];
for (const bucket of PADDING_BUCKETS) {
if (bucket >= dataSize + 4) { // +4 for padding length header
targetSize = bucket;
break;
}
}
// Calculate padding needed (subtract data size and 4-byte length header)
const paddingSize = targetSize - dataSize - 4;
const padding = randomBytes(Math.max(0, paddingSize));
// Create padded message: [4-byte length][data][random padding]
const lengthBytes = new Uint8Array(4);
const view = new DataView(lengthBytes.buffer);
view.setUint32(0, dataSize, true); // Little-endian
const padded = new Uint8Array(4 + dataSize + padding.length);
padded.set(lengthBytes, 0);
padded.set(dataBytes, 4);
padded.set(padding, 4 + dataSize);
// Return as base64 for easy transmission
// Use chunked approach to avoid "Maximum call stack size exceeded" for large arrays
// Convert Uint8Array to base64 in chunks
const chunkSize = 8192;
let binary = '';
for (let i = 0; i < padded.length; i += chunkSize) {
const chunk = padded.slice(i, i + chunkSize);
binary += String.fromCharCode.apply(null, Array.from(chunk));
}
return btoa(binary);
}
/**
* Removes padding from a message
* @param paddedData - The padded data (base64)
* @returns Original unpadded data
*/
export function removePadding(paddedData: string): string {
try {
const padded = Uint8Array.from(atob(paddedData), c => c.charCodeAt(0));
// Read length from first 4 bytes
const view = new DataView(padded.buffer);
const dataSize = view.getUint32(0, true); // Little-endian
// Extract original data
const data = padded.slice(4, 4 + dataSize);
return new TextDecoder().decode(data);
} catch (error) {
// If padding removal fails, assume it's an old message without padding
return paddedData;
}
}
+323 -41
View File
@@ -8,10 +8,12 @@ import {
SessionCipher,
KeyHelper,
SignalProtocolAddress,
type DeviceType
type DeviceType,
type KeyPairType
} from "@privacyresearch/libsignal-protocol-typescript";
import { SignalProtocolStorage } from "./signalStorage";
import { b64, ub64 } from "../utils";
import api from "@/core/api";
// Helper to ensure we get a proper ArrayBuffer (not SharedArrayBuffer)
function toArrayBuffer(buffer: ArrayBuffer | SharedArrayBuffer): ArrayBuffer {
@@ -39,6 +41,13 @@ export interface PreKeyBundleData {
export class SignalProtocolService {
private storage: SignalProtocolStorage;
// Prekey configuration constants
private static readonly PREKEY_COUNT = 20;
private static readonly PREKEY_REGEN_THRESHOLD = 5; // Regenerate when fewer than this many prekeys are left
private static readonly PREKEY_REGEN_COUNT = 10; // Number of prekeys to regenerate
private static readonly SIGNED_PREKEY_ID = 1;
private static readonly BATCH_SIZE = 10;
constructor(userId: string) {
this.storage = new SignalProtocolStorage(userId);
@@ -64,21 +73,111 @@ export class SignalProtocolService {
await this.storage.saveLocalRegistrationId(registrationId);
// Generate signed prekey
const signedPreKeyId = 1;
const signedPreKey = await KeyHelper.generateSignedPreKey(identityKeyPair, signedPreKeyId);
await this.storage.storeSignedPreKey(signedPreKeyId, signedPreKey.keyPair);
// Store signature separately (we'll need it for the bundle)
// For now, we'll regenerate it when needed since storage doesn't store signatures
const signedPreKey = await KeyHelper.generateSignedPreKey(identityKeyPair, SignalProtocolService.SIGNED_PREKEY_ID);
// Store both the key pair and its signature
await this.storage.storeSignedPreKey(
SignalProtocolService.SIGNED_PREKEY_ID,
signedPreKey.keyPair,
new Uint8Array(signedPreKey.signature)
);
// Generate prekeys (typically 100 prekeys)
const preKeyCount = 100;
for (let i = 1; i <= preKeyCount; i++) {
// Generate prekeys (one-time keys for establishing new sessions)
// Each new conversation consumes one prekey when the first message is sent
// Generation is non-blocking (yields to event loop), so this doesn't freeze the UI
for (let i = 1; i <= SignalProtocolService.PREKEY_COUNT; i++) {
const preKey = await KeyHelper.generatePreKey(i);
await this.storage.storePreKey(i, preKey.keyPair);
// Yield to event loop every batchSize keys to prevent UI freezing
if (i % SignalProtocolService.BATCH_SIZE === 0) {
await new Promise(resolve => setTimeout(resolve, 0));
}
}
}
/**
* Ensure signed prekey exists and is valid, regenerating if necessary
*/
private async ensureSignedPreKey(identityKeyPair: KeyPairType): Promise<{ keyPair: KeyPairType; signature: Uint8Array }> {
let signature = await this.storage.loadSignedPreKeySignature(SignalProtocolService.SIGNED_PREKEY_ID);
let signedPreKey = await this.storage.loadSignedPreKey(SignalProtocolService.SIGNED_PREKEY_ID);
if (!signedPreKey || !signature) {
// Signed prekey or signature missing - regenerate both to ensure consistency
const signedPreKeyWithSig = await KeyHelper.generateSignedPreKey(identityKeyPair, SignalProtocolService.SIGNED_PREKEY_ID);
await this.storage.storeSignedPreKey(
SignalProtocolService.SIGNED_PREKEY_ID,
signedPreKeyWithSig.keyPair,
new Uint8Array(signedPreKeyWithSig.signature)
);
signedPreKey = signedPreKeyWithSig.keyPair;
signature = new Uint8Array(signedPreKeyWithSig.signature);
}
return { keyPair: signedPreKey, signature };
}
/**
* Find an available prekey, regenerating if necessary
*/
private async findOrRegeneratePreKey(): Promise<{ keyPair: KeyPairType; keyId: number }> {
// Find the first available prekey
let preKey: KeyPairType | undefined;
let preKeyId = 0;
let availableCount = 0;
for (let i = 1; i <= SignalProtocolService.PREKEY_COUNT; i++) {
const candidate = await this.storage.loadPreKey(i);
if (candidate) {
availableCount++;
if (!preKey) {
preKey = candidate;
preKeyId = i;
}
}
}
// If we're running low on prekeys, regenerate more proactively
if (availableCount < SignalProtocolService.PREKEY_REGEN_THRESHOLD) {
console.warn(`Low on prekeys (${availableCount} remaining), regenerating...`);
// Find the next available ID to regenerate from
let nextId = SignalProtocolService.PREKEY_COUNT + 1;
for (let i = 1; i <= SignalProtocolService.PREKEY_COUNT; i++) {
const existing = await this.storage.loadPreKey(i);
if (!existing) {
nextId = i;
break;
}
}
// Regenerate prekeys starting from nextId
for (let i = 0; i < SignalProtocolService.PREKEY_REGEN_COUNT; i++) {
const keyId = nextId + i;
const existing = await this.storage.loadPreKey(keyId);
if (!existing) {
const newPreKey = await KeyHelper.generatePreKey(keyId);
await this.storage.storePreKey(keyId, newPreKey.keyPair);
if (!preKey) {
preKey = newPreKey.keyPair;
preKeyId = keyId;
}
}
}
}
// Emergency fallback if still no prekey
if (!preKey) {
console.error("No prekeys available, emergency regeneration...");
const newPreKey = await KeyHelper.generatePreKey(1);
await this.storage.storePreKey(1, newPreKey.keyPair);
preKey = newPreKey.keyPair;
preKeyId = 1;
}
return { keyPair: preKey, keyId: preKeyId };
}
/**
* Get prekey bundle for this user to share with others
*/
@@ -93,36 +192,73 @@ export class SignalProtocolService {
throw new Error("Registration ID not found");
}
const signedPreKey = await this.storage.loadSignedPreKey(1);
if (!signedPreKey) {
throw new Error("Signed prekey not found");
}
// Regenerate signed prekey to get signature (since storage doesn't store it)
// In production, you'd store the signature separately
const signedPreKeyWithSig = await KeyHelper.generateSignedPreKey(identityKeyPair, 1);
await this.storage.storeSignedPreKey(1, signedPreKeyWithSig.keyPair);
// Get a prekey to include
const preKey = await this.storage.loadPreKey(1);
if (!preKey) {
throw new Error("No prekeys available");
}
const { keyPair: signedPreKey, signature } = await this.ensureSignedPreKey(identityKeyPair);
const { keyPair: preKey, keyId: preKeyId } = await this.findOrRegeneratePreKey();
return {
registrationId: registrationId,
identityKey: b64(new Uint8Array(identityKeyPair.pubKey)),
signedPreKey: {
keyId: 1,
keyId: SignalProtocolService.SIGNED_PREKEY_ID,
publicKey: b64(new Uint8Array(signedPreKey.pubKey)),
signature: b64(new Uint8Array(signedPreKeyWithSig.signature))
signature: b64(signature)
},
preKey: {
keyId: 1,
keyId: preKeyId,
publicKey: b64(new Uint8Array(preKey.pubKey))
}
};
}
/**
* Get all available prekeys for uploading to the server
*/
async getAllPreKeys(): Promise<Array<{ keyId: number; publicKey: string }>> {
const prekeys: Array<{ keyId: number; publicKey: string }> = [];
// Check all possible prekey IDs (including regenerated ones beyond initial count)
// We check up to PREKEY_COUNT + PREKEY_REGEN_COUNT to include regenerated prekeys
const maxPreKeyId = SignalProtocolService.PREKEY_COUNT + SignalProtocolService.PREKEY_REGEN_COUNT;
for (let i = 1; i <= maxPreKeyId; i++) {
const prekey = await this.storage.loadPreKey(i);
if (prekey) {
prekeys.push({
keyId: i,
publicKey: b64(new Uint8Array(prekey.pubKey))
});
}
}
return prekeys;
}
/**
* Get the base bundle (without prekey) for uploading all prekeys
*/
async getBaseBundle(): Promise<Omit<PreKeyBundleData, "preKey">> {
const identityKeyPair = await this.storage.getIdentityKeyPair();
if (!identityKeyPair) {
throw new Error("Signal Protocol not initialized");
}
const registrationId = await this.storage.getLocalRegistrationId();
if (!registrationId) {
throw new Error("Registration ID not found");
}
const { keyPair: signedPreKey, signature } = await this.ensureSignedPreKey(identityKeyPair);
return {
registrationId: registrationId,
identityKey: b64(new Uint8Array(identityKeyPair.pubKey)),
signedPreKey: {
keyId: SignalProtocolService.SIGNED_PREKEY_ID,
publicKey: b64(new Uint8Array(signedPreKey.pubKey)),
signature: b64(signature)
}
};
}
/**
* Process a prekey bundle from another user and establish a session
@@ -163,40 +299,186 @@ export class SignalProtocolService {
const sessionCipher = new SessionCipher(this.storage, address);
const plaintextBuffer = toArrayBuffer(new TextEncoder().encode(plaintext).buffer);
const { type, body } = await sessionCipher.encrypt(plaintextBuffer);
const encryptResult = await sessionCipher.encrypt(plaintextBuffer);
const { type, body } = encryptResult;
if (!body) {
throw new Error("Encryption failed: no body in ciphertext");
}
// ciphertext.body is a base64 string, but we need to convert it properly
// According to the library, body is a serialized protobuf message as base64 string
return { type, body };
// The library returns body as ArrayBuffer or Uint8Array, we need to convert it to base64 string
// Always convert to Uint8Array first, then to base64, regardless of input type
let bodyArray: Uint8Array;
const bodyAny = body as any;
if (typeof body === "string") {
// String input - check if it's already base64
const base64Regex = /^[A-Za-z0-9+/]*={0,2}$/;
if (base64Regex.test(body)) {
// Already base64, use as-is
bodyArray = ub64(body);
} else {
// String contains binary data, convert to Uint8Array
bodyArray = new Uint8Array([...body].map(c => c.charCodeAt(0)));
}
} else if (bodyAny instanceof Uint8Array) {
bodyArray = bodyAny;
} else if (bodyAny instanceof ArrayBuffer) {
bodyArray = new Uint8Array(bodyAny);
} else {
// Try to convert unknown type
if (bodyAny.buffer && bodyAny.buffer instanceof ArrayBuffer) {
bodyArray = new Uint8Array(bodyAny.buffer, bodyAny.byteOffset || 0, bodyAny.byteLength || bodyAny.buffer.byteLength);
} else {
bodyArray = new Uint8Array(bodyAny as ArrayBuffer);
}
}
// Convert to base64
const bodyBase64 = b64(bodyArray);
// Final validation - ensure the result is valid base64
const base64Regex = /^[A-Za-z0-9+/]*={0,2}$/;
if (!base64Regex.test(bodyBase64)) {
throw new Error(`Failed to convert body to base64: result contains invalid characters. Length: ${bodyBase64.length}`);
}
// Test that it can be decoded
try {
atob(bodyBase64.substring(0, Math.min(4, bodyBase64.length)));
} catch (error) {
throw new Error(`Failed to convert body to base64: ${error instanceof Error ? error.message : String(error)}`);
}
return { type, body: bodyBase64 };
}
/**
* Decrypt a message from a sender
*/
async decryptMessage(senderId: number, ciphertext: { type: number; body: string }): Promise<string> {
if (!ciphertext.body || typeof ciphertext.body !== "string") {
throw new Error("Invalid ciphertext: body is missing or not a string");
}
const address = new SignalProtocolAddress(senderId.toString(), 1);
const sessionCipher = new SessionCipher(this.storage, address);
// Handle both PreKeyWhisperMessage (type 3) and WhisperMessage (type 1)
const { buffer, byteOffset, byteLength } = ub64(ciphertext.body);
const bodyBuffer = toArrayBuffer(buffer.slice(byteOffset, byteOffset + byteLength));
let plaintextBytes: ArrayBuffer;
// ciphertext.body is a base64 string from the Signal Protocol library
let bodyBuffer: ArrayBuffer;
try {
const { buffer, byteOffset, byteLength } = ub64(ciphertext.body);
bodyBuffer = toArrayBuffer(buffer.slice(byteOffset, byteOffset + byteLength));
} catch (error) {
throw new Error(`Failed to decode ciphertext body: ${error instanceof Error ? error.message : String(error)}`);
}
if (ciphertext.type === 3) {
// PreKeyWhisperMessage
plaintextBytes = await sessionCipher.decryptPreKeyWhisperMessage(bodyBuffer);
} else {
// WhisperMessage
plaintextBytes = await sessionCipher.decryptWhisperMessage(bodyBuffer);
let plaintextBytes: ArrayBuffer;
try {
if (ciphertext.type === 3) {
// PreKeyWhisperMessage - this will consume a prekey
// Count available prekeys before decryption
const prekeysBefore = await this.countAvailablePrekeys();
plaintextBytes = await sessionCipher.decryptPreKeyWhisperMessage(bodyBuffer);
// Check if a prekey was consumed (removed by the library)
const prekeysAfter = await this.countAvailablePrekeys();
if (prekeysBefore > prekeysAfter) {
// A prekey was consumed - refresh the bundle in the background
// This ensures new users can still message you while you're offline
this.refreshPreKeyBundle().catch(err =>
console.warn("Failed to refresh prekey bundle after consumption:", err)
);
}
} else {
// WhisperMessage - uses existing session, no prekey consumed
plaintextBytes = await sessionCipher.decryptWhisperMessage(bodyBuffer);
}
} catch (error) {
// Log detailed error information for debugging
const errorMessage = error instanceof Error ? error.message : String(error);
// Handle different types of decryption errors
if (errorMessage.includes("Bad MAC")) {
console.warn(`Bad MAC error detected for sender ${senderId} (type ${ciphertext.type}). Session may be out of sync.`);
// For both types, remove the session so next message can re-establish it
try {
await this.storage.removeSession(address.toString());
console.warn(`Removed corrupted session for sender ${senderId}. Sender needs to send a new message to re-establish session.`);
} catch (resetError) {
console.error("Failed to remove session:", resetError);
}
} else if (errorMessage.includes("Tried to decrypt on a sending chain") || errorMessage.includes("No record for device")) {
// These errors indicate the session state is corrupted or missing
// Remove the session so it can be re-established
console.warn(`Session state error for sender ${senderId}: ${errorMessage}. Removing session.`);
try {
await this.storage.removeSession(address.toString());
console.warn(`Removed corrupted session for sender ${senderId}. Sender needs to send a new message to re-establish session.`);
} catch (resetError) {
console.error("Failed to remove session:", resetError);
}
}
console.error("Signal Protocol decryption failed:", {
senderId,
type: ciphertext.type,
bodyLength: ciphertext.body.length,
bodyFirst50: ciphertext.body.substring(0, 50),
bodyLast50: ciphertext.body.substring(Math.max(0, ciphertext.body.length - 50)),
bodyIsBase64: /^[A-Za-z0-9+/]*={0,2}$/.test(ciphertext.body),
error: errorMessage
});
throw new Error(`Failed to decrypt message: ${errorMessage}`);
}
return new TextDecoder().decode(plaintextBytes);
}
/**
* Count available prekeys
*/
private async countAvailablePrekeys(): Promise<number> {
let count = 0;
const maxPreKeyId = SignalProtocolService.PREKEY_COUNT + SignalProtocolService.PREKEY_REGEN_COUNT;
for (let i = 1; i <= maxPreKeyId; i++) {
const prekey = await this.storage.loadPreKey(i);
if (prekey) {
count++;
}
}
return count;
}
/**
* Refresh prekey bundle after a prekey was consumed
* This ensures new users can still message you while you're offline
* Uploads all available prekeys to the server for rotation
*/
private async refreshPreKeyBundle(): Promise<void> {
try {
const token = api.user.auth.getAuthToken();
if (!token) {
console.warn("No auth token, cannot refresh prekey bundle");
return;
}
const baseBundle = await this.getBaseBundle();
const prekeys = await this.getAllPreKeys();
// Upload all prekeys in the background
api.crypto.prekeys.uploadAllPreKeys(baseBundle, prekeys, token).catch(err =>
console.warn("Failed to upload all prekeys:", err)
);
} catch (error) {
console.error("Failed to refresh prekey bundle:", error);
}
}
/**
* Check if a session exists for a recipient
+86 -9
View File
@@ -69,9 +69,44 @@ async function getStore(storeName: keyof SignalDB, mode: IDBTransactionMode = "r
}
// Helper to convert Uint8Array to ArrayBuffer
function toArrayBuffer(u8: Uint8Array | ArrayBuffer): ArrayBuffer {
function toArrayBuffer(u8: Uint8Array | ArrayBuffer | ArrayBufferLike): ArrayBuffer {
if (u8 instanceof ArrayBuffer) return u8;
return u8.buffer.slice(u8.byteOffset, u8.byteOffset + u8.byteLength);
// Check if SharedArrayBuffer is available (requires COOP/COEP headers)
const SharedArrayBufferConstructor = typeof SharedArrayBuffer !== "undefined" ? SharedArrayBuffer : null;
if (SharedArrayBufferConstructor && u8 instanceof SharedArrayBufferConstructor) {
// Convert SharedArrayBuffer to ArrayBuffer by copying
const view = new Uint8Array(u8);
const copy = new Uint8Array(view.length);
copy.set(view);
// copy.buffer is always ArrayBuffer for a newly created Uint8Array
return copy.buffer as ArrayBuffer;
}
// Uint8Array case - buffer might be SharedArrayBuffer, so copy it
if (u8 instanceof Uint8Array) {
const buffer = u8.buffer;
if (SharedArrayBufferConstructor && buffer instanceof SharedArrayBufferConstructor) {
const copy = new Uint8Array(u8.length);
copy.set(u8);
// copy.buffer is always ArrayBuffer for a newly created Uint8Array
return copy.buffer as ArrayBuffer;
}
const sliced = buffer.slice(u8.byteOffset, u8.byteOffset + u8.byteLength);
// Ensure we return ArrayBuffer, not SharedArrayBuffer
if (SharedArrayBufferConstructor && sliced instanceof SharedArrayBufferConstructor) {
const copy = new Uint8Array(sliced);
// copy.buffer is always ArrayBuffer for a newly created Uint8Array
return copy.buffer as unknown as ArrayBuffer;
}
// TypeScript doesn't know that slice() returns ArrayBuffer when buffer is ArrayBuffer
// But we've already checked it's not SharedArrayBuffer, so it must be ArrayBuffer
return sliced as unknown as ArrayBuffer;
}
// Fallback: treat as ArrayBuffer
return u8 as unknown as ArrayBuffer;
}
// Helper to convert ArrayBuffer to Uint8Array
@@ -90,7 +125,7 @@ export class SignalProtocolStorage implements StorageType {
// Identity Key Management
async getIdentityKeyPair(): Promise<KeyPairType | undefined> {
const store = await getStore("identityKeys");
const result = await new Promise<{ publicKey: ArrayBuffer; privateKey: ArrayBuffer } | undefined>((resolve, reject) => {
const result = await new Promise<KeyPairType | undefined>((resolve, reject) => {
const request = store.get(this.userId);
request.onsuccess = () => {
const data = request.result;
@@ -165,7 +200,7 @@ export class SignalProtocolStorage implements StorageType {
async loadPreKey(encodedAddress: string | number): Promise<KeyPairType | undefined> {
const preKeyId = typeof encodedAddress === "number" ? encodedAddress : parseInt(encodedAddress, 10);
const store = await getStore("preKeys");
const result = await new Promise<{ publicKey: ArrayBuffer; privateKey: ArrayBuffer } | undefined>((resolve, reject) => {
const result = await new Promise<KeyPairType | undefined>((resolve, reject) => {
const request = store.get([this.userId, preKeyId]);
request.onsuccess = () => {
const data = request.result;
@@ -213,7 +248,7 @@ export class SignalProtocolStorage implements StorageType {
async loadSignedPreKey(keyId: number | string): Promise<KeyPairType | undefined> {
const signedPreKeyId = typeof keyId === "number" ? keyId : parseInt(keyId, 10);
const store = await getStore("signedPreKeys");
const result = await new Promise<{ publicKey: ArrayBuffer; privateKey: ArrayBuffer; keyId: number } | undefined>((resolve, reject) => {
const result = await new Promise<KeyPairType | undefined>((resolve, reject) => {
const request = store.get(this.userId);
request.onsuccess = () => {
const data = request.result;
@@ -232,20 +267,49 @@ export class SignalProtocolStorage implements StorageType {
return result;
}
async storeSignedPreKey(keyId: number | string, keyPair: KeyPairType): Promise<void> {
async storeSignedPreKey(keyId: number | string, keyPair: KeyPairType, signature?: Uint8Array): Promise<void> {
const signedPreKeyId = typeof keyId === "number" ? keyId : parseInt(keyId, 10);
const store = await getStore("signedPreKeys", "readwrite");
await new Promise<void>((resolve, reject) => {
const request = store.put({
interface SignedPreKeyData {
userId: string;
keyId: number;
publicKey: Uint8Array;
privateKey: Uint8Array;
signature?: Uint8Array;
}
const data: SignedPreKeyData = {
userId: this.userId,
keyId: signedPreKeyId,
publicKey: toUint8Array(keyPair.pubKey),
privateKey: toUint8Array(keyPair.privKey)
});
};
if (signature) {
data.signature = toUint8Array(signature);
}
const request = store.put(data);
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
}
async loadSignedPreKeySignature(keyId: number | string): Promise<Uint8Array | undefined> {
const signedPreKeyId = typeof keyId === "number" ? keyId : parseInt(keyId, 10);
const store = await getStore("signedPreKeys");
const result = await new Promise<{ signature?: Uint8Array } | undefined>((resolve, reject) => {
const request = store.get(this.userId);
request.onsuccess = () => {
const data = request.result;
if (!data || data.keyId !== signedPreKeyId) {
resolve(undefined);
return;
}
resolve(data.signature ? { signature: toUint8Array(data.signature) } : undefined);
};
request.onerror = () => reject(request.error);
});
return result?.signature;
}
async removeSignedPreKey(keyId: number | string): Promise<void> {
const store = await getStore("signedPreKeys", "readwrite");
@@ -263,7 +327,7 @@ export class SignalProtocolStorage implements StorageType {
const deviceId = parts.length > 1 ? parts[1] : encodedAddress;
const store = await getStore("sessions");
const result = await new Promise<{ record: string } | undefined>((resolve, reject) => {
const result = await new Promise<string | undefined>((resolve, reject) => {
const request = store.get([this.userId, deviceId]);
request.onsuccess = () => {
const data = request.result;
@@ -291,5 +355,18 @@ export class SignalProtocolStorage implements StorageType {
request.onerror = () => reject(request.error);
});
}
async removeSession(encodedAddress: string): Promise<void> {
// encodedAddress format: "userId.deviceId"
const parts = encodedAddress.split(".");
const deviceId = parts.length > 1 ? parts[1] : encodedAddress;
const store = await getStore("sessions", "readwrite");
await new Promise<void>((resolve, reject) => {
const request = store.delete([this.userId, deviceId]);
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
}
}
+11 -1
View File
@@ -33,7 +33,17 @@ export function delay(ms: number): Promise<void> {
}
export function b64(a: Uint8Array): string { return btoa(String.fromCharCode(...a)); }
export function b64(a: Uint8Array): string {
// Use chunked approach to avoid "Maximum call stack size exceeded" for large arrays
// Process in chunks and use apply to avoid spreading large arrays
const chunkSize = 8192;
let binary = '';
for (let i = 0; i < a.length; i += chunkSize) {
const chunk = a.slice(i, i + chunkSize);
binary += String.fromCharCode.apply(null, Array.from(chunk));
}
return btoa(binary);
}
export function ub64(s: string): Uint8Array {
const bin = atob(s);
const arr = new Uint8Array(bin.length);