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
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:
1 parent
d92e899fc7
commit
e2d9bf1514
3 files changed
+26
-42
No files matched your search
@@ -5,6 +5,11 @@
|
||||
const ENC = new TextEncoder();
|
||||
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 ──────────────────────────────────────────────────────────
|
||||
|
||||
export async function generateKdfSalt(): Promise<string> {
|
||||
@@ -47,21 +52,15 @@ export async function decryptMasterKey(encB64: string, derived: CryptoKey): Prom
|
||||
|
||||
// ── 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<{
|
||||
publicKeyB64: string;
|
||||
privateKey: CryptoKey;
|
||||
pkcs8Bytes: Uint8Array;
|
||||
pkcs8Bytes: Uint8Array<ArrayBuffer>;
|
||||
}> {
|
||||
const kp = await crypto.subtle.generateKey({ name: 'X25519' } as any, true, ['deriveBits']);
|
||||
const publicRaw = await crypto.subtle.exportKey('raw', (kp as CryptoKeyPair).publicKey);
|
||||
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(
|
||||
'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, master: CryptoKey): Promise<string> {
|
||||
export async function encryptPrivateKey(pkcs8: Uint8Array<ArrayBuffer>, master: CryptoKey): Promise<string> {
|
||||
return aesEncryptRaw(pkcs8, master);
|
||||
}
|
||||
|
||||
/** Decrypt and import the private key, returning a non-extractable CryptoKey */
|
||||
export async function decryptPrivateKeyToCryptoKey(
|
||||
encB64: string,
|
||||
master: CryptoKey
|
||||
@@ -93,20 +90,14 @@ export async function decryptPrivateKeyToCryptoKey(
|
||||
|
||||
// ── Conversation key exchange (ephemeral X25519 ECDH sealed box) ─────────────
|
||||
//
|
||||
// Wire format for sealConvKey output: ephemeral_pub_key(32) | nonce(12) | ciphertext
|
||||
// The ephemeral public key is used by the recipient to reconstruct the shared secret.
|
||||
// Wire format: ephemeral_pub_key(32) | nonce(12) | ciphertext
|
||||
|
||||
export function generateConvKeyRaw(): Uint8Array {
|
||||
export function generateConvKeyRaw(): Uint8Array<ArrayBuffer> {
|
||||
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(
|
||||
keyRaw: Uint8Array,
|
||||
keyRaw: Uint8Array<ArrayBuffer>,
|
||||
recipientPublicKeyB64: string
|
||||
): Promise<string> {
|
||||
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, [
|
||||
'deriveBits',
|
||||
]);
|
||||
@@ -125,20 +115,17 @@ export async function sealConvKey(
|
||||
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(
|
||||
{ name: 'X25519', public: recipientPubKey } as any,
|
||||
(ephemeral as CryptoKeyPair).privateKey,
|
||||
256
|
||||
);
|
||||
|
||||
// HKDF to derive AES key — use ephemeral public key as salt for domain separation
|
||||
const aesKey = await hkdfToAes(sharedBits, ephemeralPubRaw, ['encrypt']);
|
||||
|
||||
const nonce = crypto.getRandomValues(new Uint8Array(12));
|
||||
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);
|
||||
out.set(ephemeralPubRaw, 0);
|
||||
out.set(nonce, 32);
|
||||
@@ -146,18 +133,14 @@ export async function sealConvKey(
|
||||
return bufToB64(out);
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a sealed conversation key using own X25519 private key.
|
||||
* Returns the raw 32-byte AES key.
|
||||
*/
|
||||
export async function openSealedConvKey(
|
||||
sealedB64: string,
|
||||
myPrivateKey: CryptoKey
|
||||
): Promise<Uint8Array> {
|
||||
): Promise<Uint8Array<ArrayBuffer>> {
|
||||
const data = b64ToBuf(sealedB64);
|
||||
const ephemeralPubRaw = data.slice(0, 32);
|
||||
const nonce = data.slice(32, 44);
|
||||
const ct = data.slice(44);
|
||||
const ephemeralPubRaw = u8(data, 0, 32);
|
||||
const nonce = u8(data, 32, 44);
|
||||
const ct = u8(data, 44);
|
||||
|
||||
const ephemeralPubKey = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
@@ -178,7 +161,7 @@ export async function openSealedConvKey(
|
||||
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, [
|
||||
'encrypt',
|
||||
'decrypt',
|
||||
@@ -211,9 +194,9 @@ export async function encryptFile(file: File, key: CryptoKey): Promise<Blob> {
|
||||
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 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);
|
||||
}
|
||||
|
||||
@@ -229,7 +212,7 @@ export async function decryptFileKey(encB64: string, masterKey: CryptoKey): Prom
|
||||
|
||||
async function hkdfToAes(
|
||||
sharedBits: ArrayBuffer,
|
||||
salt: Uint8Array,
|
||||
salt: Uint8Array<ArrayBuffer>,
|
||||
usages: KeyUsage[]
|
||||
): Promise<CryptoKey> {
|
||||
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 ct = await crypto.subtle.encrypt({ name: 'AES-GCM', iv: nonce }, key, data);
|
||||
const combined = new Uint8Array(12 + ct.byteLength);
|
||||
@@ -253,17 +236,17 @@ async function aesEncryptRaw(data: Uint8Array, key: CryptoKey): Promise<string>
|
||||
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 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);
|
||||
}
|
||||
|
||||
export function bufToB64(buf: ArrayBuffer | Uint8Array): string {
|
||||
const u8 = buf instanceof Uint8Array ? buf : new Uint8Array(buf);
|
||||
return btoa(String.fromCharCode(...u8));
|
||||
const u8arr = buf instanceof Uint8Array ? buf : new Uint8Array(buf);
|
||||
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));
|
||||
}
|
||||
@@ -37,7 +37,7 @@ class CloudLinkSocket {
|
||||
|
||||
on(handler: MessageHandler) {
|
||||
this.handlers.add(handler);
|
||||
return () => this.handlers.delete(handler);
|
||||
return () => { this.handlers.delete(handler); };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"types": ["vite/client"],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"jsx": "react-jsx",
|
||||
|
||||
Reference in new issue
Block a user