Skip to content

Commit 4938589

Browse files
committed
optimize crypto algo for speed and show progress
1 parent 955c216 commit 4938589

12 files changed

Lines changed: 1536 additions & 127 deletions

File tree

desktop/frontend/src/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Events } from "@wailsio/runtime";
22
import { useCallback, useEffect, useRef, useState } from "react";
33
import { api } from "./api";
4+
import { VaultProgressOverlay } from "./components";
45
import { Lock } from "./components/icons/Lock";
56
import { AdminPanel } from "./screens/AdminPanel/AdminPanel";
67
import {
@@ -153,6 +154,7 @@ function App() {
153154
return (
154155
<>
155156
{body}
157+
<VaultProgressOverlay />
156158
{secondTouchPlugin && (
157159
<SecondTouchOverlay pluginName={secondTouchPlugin} />
158160
)}
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import { Events } from "@wailsio/runtime";
2+
import { useEffect, useRef, useState } from "react";
3+
import { Lock } from "./icons/Lock";
4+
5+
interface ProgressPayload {
6+
filesDone: number;
7+
filesTotal: number;
8+
bytesDone: number;
9+
bytesTotal: number;
10+
}
11+
12+
type Mode = "lock" | "unlock";
13+
14+
interface ActiveProgress extends ProgressPayload {
15+
mode: Mode;
16+
}
17+
18+
/**
19+
* VaultProgressOverlay listens for "lock:progress" / "unlock:progress"
20+
* events during multi-vault encryption/decryption, plus the explicit
21+
* "lock:complete" / "unlock:complete" signal emitted from the backend's
22+
* deferred Done(). The complete event is what dismisses the overlay —
23+
* relying on filesDone==filesTotal alone races with view transitions
24+
* (LockScreen has its own 500ms success delay before swapping to admin)
25+
* and can produce a flicker where the user sees bar → auth screen →
26+
* bar → admin instead of bar → admin.
27+
*
28+
* Once a complete event arrives we ignore further progress events for
29+
* the same operation; the next operation will start a fresh cycle when
30+
* its first progress event lands.
31+
*/
32+
export function VaultProgressOverlay() {
33+
const [progress, setProgress] = useState<ActiveProgress | null>(null);
34+
// dismissing is read synchronously from event handlers so we use
35+
// a ref. A late progress event (delivered after :complete fired)
36+
// must not re-show the overlay during its 250ms hide animation.
37+
const dismissingRef = useRef(false);
38+
const [dismissTick, setDismissTick] = useState(0);
39+
40+
useEffect(() => {
41+
const handleProgress =
42+
(mode: Mode) =>
43+
({ data }: { data: ProgressPayload }) => {
44+
// A filesDone==0 event is the EmitStart of a new
45+
// operation. Clear any leftover dismissing state from
46+
// the previous run so back-to-back lock-then-unlock
47+
// (or vice versa) renders correctly.
48+
if (data.filesDone === 0) {
49+
dismissingRef.current = false;
50+
}
51+
if (dismissingRef.current) return;
52+
setProgress({ ...data, mode });
53+
};
54+
55+
const handleComplete = () => {
56+
dismissingRef.current = true;
57+
setDismissTick((n) => n + 1);
58+
};
59+
60+
const offLockProgress = Events.On("lock:progress", handleProgress("lock"));
61+
const offUnlockProgress = Events.On(
62+
"unlock:progress",
63+
handleProgress("unlock"),
64+
);
65+
const offLockComplete = Events.On("lock:complete", handleComplete);
66+
const offUnlockComplete = Events.On("unlock:complete", handleComplete);
67+
68+
return () => {
69+
offLockProgress();
70+
offUnlockProgress();
71+
offLockComplete();
72+
offUnlockComplete();
73+
};
74+
}, []);
75+
76+
useEffect(() => {
77+
if (dismissTick === 0) return;
78+
// Brief visual hold so the user sees 100% before the bar
79+
// disappears. We do NOT clear dismissingRef on the timer:
80+
// stale progress events from the just-completed run could
81+
// still arrive after this timer fires (Wails IPC ordering
82+
// vs JS scheduling). Dismissing stays true until the next
83+
// fresh-start event (filesDone==0) clears it.
84+
const t = window.setTimeout(() => setProgress(null), 250);
85+
return () => window.clearTimeout(t);
86+
}, [dismissTick]);
87+
88+
if (!progress || progress.filesTotal === 0) return null;
89+
90+
const pct = Math.min(
91+
100,
92+
Math.round((progress.bytesDone / Math.max(1, progress.bytesTotal)) * 100),
93+
);
94+
const verb = progress.mode === "lock" ? "Encrypting" : "Decrypting";
95+
96+
return (
97+
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-md p-6">
98+
<div className="glass rounded-2xl max-w-sm w-full p-6">
99+
<div className="flex justify-center mb-5">
100+
<div className="relative">
101+
<div className="absolute inset-0 rounded-full bg-accent/20 animate-ping motion-reduce:animate-none" />
102+
<div className="relative w-14 h-14 rounded-full bg-accent/15 text-accent flex items-center justify-center [&_svg]:size-6">
103+
<Lock />
104+
</div>
105+
</div>
106+
</div>
107+
<h2 className="text-base font-semibold text-text text-center mb-1">
108+
{verb} vaults
109+
</h2>
110+
<p className="text-xs text-text-secondary text-center mb-4">
111+
{progress.filesDone.toLocaleString()} /{" "}
112+
{progress.filesTotal.toLocaleString()} files ·{" "}
113+
{formatBytes(progress.bytesDone)} / {formatBytes(progress.bytesTotal)}
114+
</p>
115+
<div className="h-2 w-full rounded-full bg-black/10 dark:bg-white/10 overflow-hidden">
116+
<div
117+
className="h-full bg-accent transition-[width] duration-150 ease-out"
118+
style={{ width: `${pct}%` }}
119+
/>
120+
</div>
121+
<p className="mt-3 text-center text-xs text-text-secondary tabular-nums">
122+
{pct}%
123+
</p>
124+
</div>
125+
</div>
126+
);
127+
}
128+
129+
function formatBytes(b: number): string {
130+
if (b < 1024) return `${b} B`;
131+
const units = ["KB", "MB", "GB", "TB"];
132+
let v = b / 1024;
133+
let i = 0;
134+
while (v >= 1024 && i < units.length - 1) {
135+
v /= 1024;
136+
i++;
137+
}
138+
return `${v.toFixed(v >= 10 ? 0 : 1)} ${units[i]}`;
139+
}

desktop/frontend/src/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ export { Select } from "./Select";
1010
export { StatusText } from "./StatusText";
1111
export { Tabs } from "./Tabs";
1212
export { Toggle } from "./Toggle";
13+
export { VaultProgressOverlay } from "./VaultProgressOverlay";

desktop/internal/app/app_auth.go

Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -223,12 +223,44 @@ func (a *App) Unlock(pin string) error {
223223
return gateErr
224224
}
225225

226+
// Pre-walk eager vaults to compute total files/bytes for progress.
227+
// Skipped vaults (lazy/lazy_strict) aren't in the totals.
228+
var (
229+
totalFiles int64
230+
totalBytes int64
231+
)
232+
for _, v := range sc.Vaults {
233+
if sc.VaultDecryptMode(v.Path) != monban.DecryptEager {
234+
continue
235+
}
236+
if v.IsFile() {
237+
if !monban.IsFileLocked(v.Path) {
238+
continue
239+
}
240+
f, b, _ := monban.ManifestStats(encKey, monban.FileVaultDirOf(v.Path))
241+
totalFiles += f
242+
totalBytes += b
243+
} else {
244+
if !monban.IsLocked(v.Path) {
245+
continue
246+
}
247+
f, b, _ := monban.ManifestStats(encKey, v.Path)
248+
totalFiles += f
249+
totalBytes += b
250+
}
251+
}
252+
progress := newProgressEmitter(a.window, "unlock", totalFiles, totalBytes)
253+
if totalFiles > 0 {
254+
progress.EmitStart()
255+
}
256+
defer progress.Done()
257+
226258
// Unlock all eager vaults
227259
for _, v := range sc.Vaults {
228260
if sc.VaultDecryptMode(v.Path) != monban.DecryptEager {
229261
continue
230262
}
231-
if err := monban.UnlockVaultEntry(encKey, v); err != nil {
263+
if err := monban.UnlockVaultEntry(encKey, v, progress.Func()); err != nil {
232264
return err
233265
}
234266
}
@@ -275,6 +307,38 @@ func (a *App) Lock() error {
275307
return err
276308
}
277309

310+
// Pre-walk to compute progress totals. Skip vaults that are
311+
// already locked (no work) and stop walking on first error
312+
// (best-effort — a missing/permission-denied vault still gets
313+
// counted as zero, so the bar may overshoot rather than freeze).
314+
var (
315+
totalFiles int64
316+
totalBytes int64
317+
)
318+
for _, v := range a.secureCfg.Vaults {
319+
if v.IsFile() {
320+
if monban.IsFileLocked(v.Path) {
321+
continue
322+
}
323+
if info, statErr := os.Stat(v.Path); statErr == nil {
324+
totalFiles++
325+
totalBytes += info.Size()
326+
}
327+
} else {
328+
if monban.IsLocked(v.Path) {
329+
continue
330+
}
331+
f, b, _ := monban.FolderStats(v.Path)
332+
totalFiles += f
333+
totalBytes += b
334+
}
335+
}
336+
progress := newProgressEmitter(a.window, "lock", totalFiles, totalBytes)
337+
if totalFiles > 0 {
338+
progress.EmitStart()
339+
}
340+
defer progress.Done()
341+
278342
var lockErr error
279343
for _, v := range a.secureCfg.Vaults {
280344
mode := a.secureCfg.VaultDecryptMode(v.Path)
@@ -284,14 +348,14 @@ func (a *App) Lock() error {
284348
lockErr = fmt.Errorf("deriving lazy strict key: %w", err)
285349
break
286350
}
287-
if err := monban.LockVaultEntry(lazyKey, v); err != nil {
351+
if err := monban.LockVaultEntry(lazyKey, v, progress.Func()); err != nil {
288352
monban.ZeroBytes(lazyKey)
289353
lockErr = err
290354
break
291355
}
292356
monban.ZeroBytes(lazyKey)
293357
} else {
294-
if err := monban.LockVaultEntry(a.encKey, v); err != nil {
358+
if err := monban.LockVaultEntry(a.encKey, v, progress.Func()); err != nil {
295359
lockErr = err
296360
break
297361
}

desktop/internal/app/app_vaults.go

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ func (a *App) RemoveFolder(folderPath string, pin string) error {
8282
}
8383

8484
// Ensure files are decrypted
85-
if err := monban.UnlockVaultEntry(a.encKey, sc.Vaults[idx]); err != nil {
85+
if err := monban.UnlockVaultEntry(a.encKey, sc.Vaults[idx], nil); err != nil {
8686
return err
8787
}
8888

@@ -133,7 +133,9 @@ func (a *App) DecryptLazyVault(path string, pin string) error {
133133
decMode := a.secureCfg.VaultDecryptMode(absPath)
134134

135135
if decMode == monban.DecryptEager || decMode == monban.DecryptLazy {
136-
if err := monban.UnlockVaultEntry(a.encKey, v); err != nil {
136+
progress := a.maybeProgressForUnlock(a.encKey, v)
137+
defer progress.Done()
138+
if err := monban.UnlockVaultEntry(a.encKey, v, progress.Func()); err != nil {
137139
return err
138140
}
139141
return nil
@@ -157,7 +159,9 @@ func (a *App) DecryptLazyVault(path string, pin string) error {
157159
}
158160
defer monban.ZeroBytes(lazyStrictKey)
159161

160-
if err := monban.UnlockVaultEntry(lazyStrictKey, v); err != nil {
162+
progress := a.maybeProgressForUnlock(lazyStrictKey, v)
163+
defer progress.Done()
164+
if err := monban.UnlockVaultEntry(lazyStrictKey, v, progress.Func()); err != nil {
161165
return err
162166
}
163167

@@ -190,6 +194,9 @@ func (a *App) LockVault(path string) error {
190194
v := a.secureCfg.Vaults[idx]
191195
mode := a.secureCfg.VaultDecryptMode(absPath)
192196

197+
progress := a.maybeProgressForLock(v)
198+
defer progress.Done()
199+
193200
if mode == monban.DecryptLazyStrict {
194201
hmacSalt, err := a.secureCfg.DecodeHmacSalt()
195202
if err != nil {
@@ -199,13 +206,13 @@ func (a *App) LockVault(path string) error {
199206
if err != nil {
200207
return fmt.Errorf("deriving lazy strict key: %w", err)
201208
}
202-
if err := monban.LockVaultEntry(lazyKey, v); err != nil {
209+
if err := monban.LockVaultEntry(lazyKey, v, progress.Func()); err != nil {
203210
monban.ZeroBytes(lazyKey)
204211
return err
205212
}
206213
monban.ZeroBytes(lazyKey)
207214
} else {
208-
if err := monban.LockVaultEntry(a.encKey, v); err != nil {
215+
if err := monban.LockVaultEntry(a.encKey, v, progress.Func()); err != nil {
209216
return err
210217
}
211218
}
@@ -261,14 +268,14 @@ func (a *App) UpdateVaultMode(path string, mode string, pin string) error {
261268
// eager <-> lazy: no re-encryption needed, just update flag
262269

263270
case oldMode != monban.DecryptLazyStrict && newMode == monban.DecryptLazyStrict:
264-
if err := monban.UnlockVaultEntry(a.encKey, v); err != nil {
271+
if err := monban.UnlockVaultEntry(a.encKey, v, nil); err != nil {
265272
return fmt.Errorf("decrypting vault for mode change: %w", err)
266273
}
267274
lazyStrictKey, err := monban.DeriveLazyStrictKey(masterSecret, hmacSalt, absPath)
268275
if err != nil {
269276
return fmt.Errorf("deriving lazy strict key: %w", err)
270277
}
271-
if err := monban.LockVaultEntry(lazyStrictKey, v); err != nil {
278+
if err := monban.LockVaultEntry(lazyStrictKey, v, nil); err != nil {
272279
monban.ZeroBytes(lazyStrictKey)
273280
return fmt.Errorf("re-encrypting vault with lazy strict key: %w", err)
274281
}
@@ -279,14 +286,14 @@ func (a *App) UpdateVaultMode(path string, mode string, pin string) error {
279286
if err != nil {
280287
return fmt.Errorf("deriving lazy strict key: %w", err)
281288
}
282-
if err := monban.UnlockVaultEntry(lazyStrictKey, v); err != nil {
289+
if err := monban.UnlockVaultEntry(lazyStrictKey, v, nil); err != nil {
283290
monban.ZeroBytes(lazyStrictKey)
284291
return fmt.Errorf("decrypting vault from lazy strict: %w", err)
285292
}
286293
monban.ZeroBytes(lazyStrictKey)
287294

288295
if newMode == monban.DecryptLazy {
289-
if err := monban.LockVaultEntry(a.encKey, v); err != nil {
296+
if err := monban.LockVaultEntry(a.encKey, v, nil); err != nil {
290297
return fmt.Errorf("re-encrypting vault with enc key: %w", err)
291298
}
292299
}

0 commit comments

Comments
 (0)