Skip to content

Commit 5643f60

Browse files
authored
Merge pull request #3 from typelets/fix/encryption-user-id-debug
fix: resolve file encryption issues and add note ID to status bar
2 parents c4b3f59 + 0e74222 commit 5643f60

File tree

7 files changed

+73
-14
lines changed

7 files changed

+73
-14
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,3 +260,6 @@ drizzle/
260260
*.njsproj
261261
*.sln
262262
*.sw?
263+
264+
# Claude Code
265+
.claude

src/components/editor/Editor/StatusBar.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ interface StatusBarProps {
77
scrollPercentage: number;
88
zoomLevel: number;
99
saveStatus: 'saved' | 'saving' | 'error';
10+
noteId?: string;
1011
onZoomIn: () => void;
1112
onZoomOut: () => void;
1213
onResetZoom: () => void;
@@ -19,6 +20,7 @@ export function StatusBar({
1920
scrollPercentage,
2021
zoomLevel,
2122
saveStatus,
23+
noteId,
2224
onZoomIn,
2325
onZoomOut,
2426
onResetZoom,
@@ -52,6 +54,13 @@ export function StatusBar({
5254
</span>
5355
)}
5456

57+
{/* Note ID */}
58+
{noteId && (
59+
<span className="hover:bg-muted px-1.5 py-0.5 rounded cursor-default" title="Note ID">
60+
{noteId}
61+
</span>
62+
)}
63+
5564
{/* Zoom controls */}
5665
<div className="flex items-center gap-1">
5766
<button

src/components/editor/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -620,6 +620,7 @@ export default function Index({
620620
scrollPercentage={scrollPercentage}
621621
zoomLevel={zoomLevel}
622622
saveStatus={saveStatus}
623+
noteId={note?.id}
623624
onZoomIn={handleZoomIn}
624625
onZoomOut={handleZoomOut}
625626
onResetZoom={resetZoom}

src/components/folders/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ export default function FolderPanel({
212212
<h3 className="text-foreground text-lg font-semibold">Folders</h3>
213213
</div>
214214
<div className="text-muted-foreground flex items-center gap-2 text-sm">
215-
<span className="text-xs opacity-80">Typelets v{APP_VERSION}</span>
215+
<span className="text-xs opacity-80">Typelets v{APP_VERSION}.beta</span>
216216
</div>
217217
</div>
218218

src/components/layout/MainLayout.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ export default function MainLayout() {
128128
onDeleteNote: deleteNote,
129129
onArchiveNote: archiveNote,
130130
onToggleStar: toggleStar,
131+
userId,
131132
};
132133

133134
if (isChecking) {

src/lib/encryption/index.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -364,8 +364,7 @@ class EncryptionService {
364364
});
365365

366366
return result;
367-
} catch (error) {
368-
console.error('Decryption failed:', error);
367+
} catch {
369368
throw new Error('Failed to decrypt note.');
370369
}
371370
}

src/services/fileService.ts

Lines changed: 57 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { encryptNoteData, decryptNoteData } from '@/lib/encryption';
1+
import { encryptNoteData } from '@/lib/encryption';
22
import type { FileAttachment } from '@/types/note';
33

44
const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:3001/api';
@@ -34,7 +34,17 @@ export class FileService {
3434

3535
private async encryptFile(file: File, userId: string): Promise<{ encryptedData: string; encryptedTitle: string; iv: string; salt: string }> {
3636
const fileBuffer = await file.arrayBuffer();
37-
const base64Content = btoa(String.fromCharCode(...new Uint8Array(fileBuffer)));
37+
38+
// Convert ArrayBuffer to base64 safely for large files
39+
const bytes = new Uint8Array(fileBuffer);
40+
let base64Content = '';
41+
const chunkSize = 0x8000; // 32KB chunks to avoid function argument limits
42+
43+
for (let i = 0; i < bytes.length; i += chunkSize) {
44+
const chunk = bytes.subarray(i, Math.min(i + chunkSize, bytes.length));
45+
base64Content += String.fromCharCode.apply(null, Array.from(chunk));
46+
}
47+
base64Content = btoa(base64Content);
3848

3949
const encrypted = await encryptNoteData(userId, file.name, base64Content);
4050
return {
@@ -45,15 +55,50 @@ export class FileService {
4555
};
4656
}
4757

48-
private async decryptFile(encryptedData: string, encryptedTitle: string, iv: string, salt: string, userId: string): Promise<ArrayBuffer> {
49-
const decrypted = await decryptNoteData(userId, encryptedTitle, encryptedData, iv, salt);
58+
private async decryptFile(encryptedData: string, iv: string, salt: string, userId: string): Promise<ArrayBuffer> {
59+
60+
const { encryptionService } = await import('@/lib/encryption');
61+
62+
// Convert base64 strings to required formats
63+
const ivBytes = this.base64ToUint8Array(iv);
64+
const saltBytes = this.base64ToUint8Array(salt);
65+
const encryptedBytes = this.base64ToArrayBuffer(encryptedData);
66+
67+
// Derive the same key used for encryption
68+
const key = await encryptionService.deriveKey(userId, saltBytes);
69+
70+
// Decrypt the file content
71+
const decryptedBuffer = await crypto.subtle.decrypt(
72+
{ name: 'AES-GCM', iv: ivBytes },
73+
key,
74+
encryptedBytes
75+
);
76+
77+
// The decrypted content is base64-encoded, so we need to decode it
78+
const decryptedText = new TextDecoder().decode(decryptedBuffer);
5079

51-
const binaryString = atob(decrypted.content);
52-
const bytes = new Uint8Array(binaryString.length);
53-
for (let i = 0; i < binaryString.length; i++) {
54-
bytes[i] = binaryString.charCodeAt(i);
80+
// Decode the base64 to get the actual file content
81+
const actualFileContent = atob(decryptedText);
82+
const finalBuffer = new Uint8Array(actualFileContent.length);
83+
for (let i = 0; i < actualFileContent.length; i++) {
84+
finalBuffer[i] = actualFileContent.charCodeAt(i);
5585
}
56-
return bytes.buffer;
86+
87+
return finalBuffer.buffer;
88+
}
89+
90+
private base64ToUint8Array(base64: string): Uint8Array {
91+
const binary = atob(base64);
92+
const bytes = new Uint8Array(binary.length);
93+
for (let i = 0; i < binary.length; i++) {
94+
bytes[i] = binary.charCodeAt(i);
95+
}
96+
return bytes;
97+
}
98+
99+
private base64ToArrayBuffer(base64: string): ArrayBuffer {
100+
const bytes = this.base64ToUint8Array(base64);
101+
return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
57102
}
58103

59104
async uploadFiles(
@@ -78,6 +123,7 @@ export class FileService {
78123
userId: string,
79124
onProgress?: (progress: UploadProgress) => void
80125
): Promise<FileAttachment> {
126+
81127
if (file.size > 10 * 1024 * 1024) {
82128
throw new Error('File size exceeds 10MB limit');
83129
}
@@ -150,13 +196,13 @@ export class FileService {
150196
const encryptedFile = await response.json();
151197
const decryptedBuffer = await this.decryptFile(
152198
encryptedFile.encryptedData,
153-
encryptedFile.encryptedTitle,
154199
encryptedFile.iv,
155200
encryptedFile.salt,
156201
userId
157202
);
158203

159-
return new Blob([decryptedBuffer], { type: attachment.mimeType });
204+
const blob = new Blob([decryptedBuffer], { type: attachment.mimeType });
205+
return blob;
160206
}
161207

162208
async removeFile(attachmentId: string): Promise<void> {

0 commit comments

Comments
 (0)