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 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));
|
||||||
}
|
}
|
||||||
@@ -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); };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in new issue
Block a user