fix: resolve TypeScript 5.5 Uint8Array generic errors and missing vite/client types
Build & Release / Docker / frontend (push) Has been cancelled
Build & Release / Desktop / Linux & Windows (push) Has been cancelled
Build & Release / Deploy to server (push) Has been cancelled
Build & Release / Create release (push) Has been cancelled
Build & Release / Docker / backend (push) Has been cancelled

TS 5.5 made typed arrays generic — Uint8Array now defaults to
Uint8Array<ArrayBufferLike>, which no longer satisfies Web Crypto's
BufferSource (requires ArrayBufferView<ArrayBuffer>).

- Add u8() helper that wraps .slice() results in new Uint8Array(),
  producing Uint8Array<ArrayBuffer> throughout crypto/index.ts
- Annotate all Uint8Array parameters/returns with <ArrayBuffer>
- Add 'vite/client' to tsconfig types so import.meta.env is recognized
- ws.ts: change socket.on() cleanup return to void (fixes useEffect type)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 4.6 committed 2026-08-28 10:56:34 -04:00
1 parent d92e899fc7
commit e2d9bf1514
3 files changed
+26 -42

No files matched your search

+24 -41
View File
@@ -5,6 +5,11 @@
const ENC = new TextEncoder(); const ENC = new TextEncoder();
const DEC = new TextDecoder(); const DEC = new TextDecoder();
// Wrap .slice() result so TypeScript sees Uint8Array<ArrayBuffer>, not Uint8Array<ArrayBufferLike>
function u8(arr: Uint8Array, start?: number, end?: number): Uint8Array<ArrayBuffer> {
return new Uint8Array(start !== undefined ? arr.slice(start, end) : arr);
}
// ── Key derivation ────────────────────────────────────────────────────────── // ── Key derivation ──────────────────────────────────────────────────────────
export async function generateKdfSalt(): Promise<string> { export async function generateKdfSalt(): Promise<string> {
@@ -47,21 +52,15 @@ export async function decryptMasterKey(encB64: string, derived: CryptoKey): Prom
// ── X25519 ECDH keypair ────────────────────────────────────────────────────── // ── X25519 ECDH keypair ──────────────────────────────────────────────────────
/**
* Generate an X25519 keypair.
* Returns the public key as base64 and the private key as a non-extractable CryptoKey.
* Also returns pkcs8Bytes so the caller can encrypt and store the private key once.
*/
export async function generateX25519Keypair(): Promise<{ export async function generateX25519Keypair(): Promise<{
publicKeyB64: string; publicKeyB64: string;
privateKey: CryptoKey; privateKey: CryptoKey;
pkcs8Bytes: Uint8Array; pkcs8Bytes: Uint8Array<ArrayBuffer>;
}> { }> {
const kp = await crypto.subtle.generateKey({ name: 'X25519' } as any, true, ['deriveBits']); const kp = await crypto.subtle.generateKey({ name: 'X25519' } as any, true, ['deriveBits']);
const publicRaw = await crypto.subtle.exportKey('raw', (kp as CryptoKeyPair).publicKey); const publicRaw = await crypto.subtle.exportKey('raw', (kp as CryptoKeyPair).publicKey);
const pkcs8 = await crypto.subtle.exportKey('pkcs8', (kp as CryptoKeyPair).privateKey); const pkcs8 = await crypto.subtle.exportKey('pkcs8', (kp as CryptoKeyPair).privateKey);
// Re-import as non-extractable for runtime use
const privateKey = await crypto.subtle.importKey( const privateKey = await crypto.subtle.importKey(
'pkcs8', 'pkcs8',
pkcs8, pkcs8,
@@ -77,12 +76,10 @@ export async function generateX25519Keypair(): Promise<{
}; };
} }
/** Encrypt the pkcs8-encoded private key with the master key for server storage */ export async function encryptPrivateKey(pkcs8: Uint8Array<ArrayBuffer>, master: CryptoKey): Promise<string> {
export async function encryptPrivateKey(pkcs8: Uint8Array, master: CryptoKey): Promise<string> {
return aesEncryptRaw(pkcs8, master); return aesEncryptRaw(pkcs8, master);
} }
/** Decrypt and import the private key, returning a non-extractable CryptoKey */
export async function decryptPrivateKeyToCryptoKey( export async function decryptPrivateKeyToCryptoKey(
encB64: string, encB64: string,
master: CryptoKey master: CryptoKey
@@ -93,20 +90,14 @@ export async function decryptPrivateKeyToCryptoKey(
// ── Conversation key exchange (ephemeral X25519 ECDH sealed box) ───────────── // ── Conversation key exchange (ephemeral X25519 ECDH sealed box) ─────────────
// //
// Wire format for sealConvKey output: ephemeral_pub_key(32) | nonce(12) | ciphertext // Wire format: ephemeral_pub_key(32) | nonce(12) | ciphertext
// The ephemeral public key is used by the recipient to reconstruct the shared secret.
export function generateConvKeyRaw(): Uint8Array { export function generateConvKeyRaw(): Uint8Array<ArrayBuffer> {
return crypto.getRandomValues(new Uint8Array(32)); return crypto.getRandomValues(new Uint8Array(32));
} }
/**
* Seal a conversation key for a recipient using their X25519 public key.
* Uses an ephemeral ECDH keypair so the sender stays anonymous.
* Only the holder of the matching private key can open it.
*/
export async function sealConvKey( export async function sealConvKey(
keyRaw: Uint8Array, keyRaw: Uint8Array<ArrayBuffer>,
recipientPublicKeyB64: string recipientPublicKeyB64: string
): Promise<string> { ): Promise<string> {
const recipientPubKey = await crypto.subtle.importKey( const recipientPubKey = await crypto.subtle.importKey(
@@ -117,7 +108,6 @@ export async function sealConvKey(
[] []
); );
// Ephemeral keypair for this seal operation
const ephemeral = await crypto.subtle.generateKey({ name: 'X25519' } as any, true, [ const ephemeral = await crypto.subtle.generateKey({ name: 'X25519' } as any, true, [
'deriveBits', 'deriveBits',
]); ]);
@@ -125,20 +115,17 @@ export async function sealConvKey(
await crypto.subtle.exportKey('raw', (ephemeral as CryptoKeyPair).publicKey) await crypto.subtle.exportKey('raw', (ephemeral as CryptoKeyPair).publicKey)
); );
// ECDH: derive shared secret between ephemeral private key and recipient public key
const sharedBits = await crypto.subtle.deriveBits( const sharedBits = await crypto.subtle.deriveBits(
{ name: 'X25519', public: recipientPubKey } as any, { name: 'X25519', public: recipientPubKey } as any,
(ephemeral as CryptoKeyPair).privateKey, (ephemeral as CryptoKeyPair).privateKey,
256 256
); );
// HKDF to derive AES key — use ephemeral public key as salt for domain separation
const aesKey = await hkdfToAes(sharedBits, ephemeralPubRaw, ['encrypt']); const aesKey = await hkdfToAes(sharedBits, ephemeralPubRaw, ['encrypt']);
const nonce = crypto.getRandomValues(new Uint8Array(12)); const nonce = crypto.getRandomValues(new Uint8Array(12));
const ct = await crypto.subtle.encrypt({ name: 'AES-GCM', iv: nonce }, aesKey, keyRaw); const ct = await crypto.subtle.encrypt({ name: 'AES-GCM', iv: nonce }, aesKey, keyRaw);
// Output: ephemeralPubKey(32) | nonce(12) | ciphertext
const out = new Uint8Array(32 + 12 + ct.byteLength); const out = new Uint8Array(32 + 12 + ct.byteLength);
out.set(ephemeralPubRaw, 0); out.set(ephemeralPubRaw, 0);
out.set(nonce, 32); out.set(nonce, 32);
@@ -146,18 +133,14 @@ export async function sealConvKey(
return bufToB64(out); return bufToB64(out);
} }
/**
* Open a sealed conversation key using own X25519 private key.
* Returns the raw 32-byte AES key.
*/
export async function openSealedConvKey( export async function openSealedConvKey(
sealedB64: string, sealedB64: string,
myPrivateKey: CryptoKey myPrivateKey: CryptoKey
): Promise<Uint8Array> { ): Promise<Uint8Array<ArrayBuffer>> {
const data = b64ToBuf(sealedB64); const data = b64ToBuf(sealedB64);
const ephemeralPubRaw = data.slice(0, 32); const ephemeralPubRaw = u8(data, 0, 32);
const nonce = data.slice(32, 44); const nonce = u8(data, 32, 44);
const ct = data.slice(44); const ct = u8(data, 44);
const ephemeralPubKey = await crypto.subtle.importKey( const ephemeralPubKey = await crypto.subtle.importKey(
'raw', 'raw',
@@ -178,7 +161,7 @@ export async function openSealedConvKey(
return new Uint8Array(pt); return new Uint8Array(pt);
} }
export async function importConvKey(raw: Uint8Array): Promise<CryptoKey> { export async function importConvKey(raw: Uint8Array<ArrayBuffer>): Promise<CryptoKey> {
return crypto.subtle.importKey('raw', raw, { name: 'AES-GCM', length: 256 }, true, [ return crypto.subtle.importKey('raw', raw, { name: 'AES-GCM', length: 256 }, true, [
'encrypt', 'encrypt',
'decrypt', 'decrypt',
@@ -211,9 +194,9 @@ export async function encryptFile(file: File, key: CryptoKey): Promise<Blob> {
return new Blob([combined], { type: 'application/octet-stream' }); return new Blob([combined], { type: 'application/octet-stream' });
} }
export async function decryptFile(blob: Blob, key: CryptoKey): Promise<Uint8Array> { export async function decryptFile(blob: Blob, key: CryptoKey): Promise<Uint8Array<ArrayBuffer>> {
const data = new Uint8Array(await blob.arrayBuffer()); const data = new Uint8Array(await blob.arrayBuffer());
const pt = await crypto.subtle.decrypt({ name: 'AES-GCM', iv: data.slice(0, 12) }, key, data.slice(12)); const pt = await crypto.subtle.decrypt({ name: 'AES-GCM', iv: u8(data, 0, 12) }, key, u8(data, 12));
return new Uint8Array(pt); return new Uint8Array(pt);
} }
@@ -229,7 +212,7 @@ export async function decryptFileKey(encB64: string, masterKey: CryptoKey): Prom
async function hkdfToAes( async function hkdfToAes(
sharedBits: ArrayBuffer, sharedBits: ArrayBuffer,
salt: Uint8Array, salt: Uint8Array<ArrayBuffer>,
usages: KeyUsage[] usages: KeyUsage[]
): Promise<CryptoKey> { ): Promise<CryptoKey> {
const keyMaterial = await crypto.subtle.importKey('raw', sharedBits, 'HKDF', false, [ const keyMaterial = await crypto.subtle.importKey('raw', sharedBits, 'HKDF', false, [
@@ -244,7 +227,7 @@ async function hkdfToAes(
); );
} }
async function aesEncryptRaw(data: Uint8Array, key: CryptoKey): Promise<string> { async function aesEncryptRaw(data: Uint8Array<ArrayBuffer>, key: CryptoKey): Promise<string> {
const nonce = crypto.getRandomValues(new Uint8Array(12)); const nonce = crypto.getRandomValues(new Uint8Array(12));
const ct = await crypto.subtle.encrypt({ name: 'AES-GCM', iv: nonce }, key, data); const ct = await crypto.subtle.encrypt({ name: 'AES-GCM', iv: nonce }, key, data);
const combined = new Uint8Array(12 + ct.byteLength); const combined = new Uint8Array(12 + ct.byteLength);
@@ -253,17 +236,17 @@ async function aesEncryptRaw(data: Uint8Array, key: CryptoKey): Promise<string>
return bufToB64(combined); return bufToB64(combined);
} }
async function aesDecryptRaw(b64: string, key: CryptoKey): Promise<Uint8Array> { async function aesDecryptRaw(b64: string, key: CryptoKey): Promise<Uint8Array<ArrayBuffer>> {
const data = b64ToBuf(b64); const data = b64ToBuf(b64);
const pt = await crypto.subtle.decrypt({ name: 'AES-GCM', iv: data.slice(0, 12) }, key, data.slice(12)); const pt = await crypto.subtle.decrypt({ name: 'AES-GCM', iv: u8(data, 0, 12) }, key, u8(data, 12));
return new Uint8Array(pt); return new Uint8Array(pt);
} }
export function bufToB64(buf: ArrayBuffer | Uint8Array): string { export function bufToB64(buf: ArrayBuffer | Uint8Array): string {
const u8 = buf instanceof Uint8Array ? buf : new Uint8Array(buf); const u8arr = buf instanceof Uint8Array ? buf : new Uint8Array(buf);
return btoa(String.fromCharCode(...u8)); return btoa(String.fromCharCode(...u8arr));
} }
export function b64ToBuf(b64: string): Uint8Array { export function b64ToBuf(b64: string): Uint8Array<ArrayBuffer> {
return Uint8Array.from(atob(b64), (c) => c.charCodeAt(0)); return Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
} }
+1 -1
View File
@@ -37,7 +37,7 @@ class CloudLinkSocket {
on(handler: MessageHandler) { on(handler: MessageHandler) {
this.handlers.add(handler); this.handlers.add(handler);
return () => this.handlers.delete(handler); return () => { this.handlers.delete(handler); };
} }
} }
+1
View File
@@ -2,6 +2,7 @@
"compilerOptions": { "compilerOptions": {
"target": "ES2020", "target": "ES2020",
"lib": ["ES2020", "DOM", "DOM.Iterable"], "lib": ["ES2020", "DOM", "DOM.Iterable"],
"types": ["vite/client"],
"module": "ESNext", "module": "ESNext",
"moduleResolution": "bundler", "moduleResolution": "bundler",
"jsx": "react-jsx", "jsx": "react-jsx",