Skip to content

Commit bad9ba4

Browse files
committed
v2.6.1: security hardening + cross-notebook click fix + Windows taskbar icon
1 parent 503dd59 commit bad9ba4

6 files changed

Lines changed: 203 additions & 63 deletions

File tree

Build.bat

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
@echo off
2+
chcp 65001 >nul
23
title NoteForge Builder
34
cd /d "%~dp0"
45

56
echo ============================================
6-
echo NoteForge Build
7+
echo NoteForge - Build
78
echo ============================================
89
echo.
910
echo RECOMMENDED: Push a git tag to build the
1011
echo installer via GitHub Actions:
1112
echo.
12-
echo git tag v2.5.2
13-
echo git push origin v2.5.2
13+
echo git tag v2.6.1
14+
echo git push origin v2.6.1
1415
echo.
1516
echo The installer will appear on the Releases
1617
echo page automatically.
@@ -29,7 +30,7 @@ if %errorlevel% neq 0 (
2930

3031
if not exist "node_modules" (
3132
echo Installing dependencies...
32-
call npm install
33+
call npm install --no-audit --no-fund --loglevel=error
3334
if %errorlevel% neq 0 (echo [ERROR] npm install failed. & pause & exit /b 1)
3435
)
3536
echo.
@@ -62,8 +63,8 @@ if %errorlevel% neq 0 (
6263
echo Then run Build.bat again.
6364
echo.
6465
echo 2. Use GitHub Actions instead (recommended):
65-
echo git tag v2.5.2
66-
echo git push origin v2.5.2
66+
echo git tag v2.6.1
67+
echo git push origin v2.6.1
6768
echo ============================================
6869
pause
6970
exit /b 1

app.js

Lines changed: 41 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1078,7 +1078,7 @@ function NoteForge() {
10781078
aNbRef.current = aNb;
10791079
const aSecRef = useRef(null);
10801080
aSecRef.current = aSec;
1081-
const nbPasswords = useRef(new Map());
1081+
const nbKeys = useRef(new Map()); // nbId -> opaque nbKeyId (main-process session key handle)
10821082
const [autoLockMin, setAutoLockMin] = useState(savedPrefs.current.autoLockMin || 15);
10831083
useEffect(() => {
10841084
prefsStore.save({
@@ -1100,7 +1100,7 @@ function NoteForge() {
11001100
await store.set(JSON.stringify(sanitized));
11011101
}
11021102
if (window.electronAPI?.lockApp) await window.electronAPI.lockApp();
1103-
nbPasswords.current.clear();
1103+
nbKeys.current.clear();
11041104
dataRef.current = null;
11051105
setData(null);
11061106
setANb(null);
@@ -1222,12 +1222,13 @@ function NoteForge() {
12221222
saveTimer.current = setTimeout(async () => {
12231223
let toSave = nd;
12241224
// Step 1: Re-encrypt sections for any locked+unlocked-in-session notebooks
1225-
// so edits are captured in the encrypted blob before we strip plaintext
1225+
// so edits are captured in the encrypted blob before we strip plaintext.
1226+
// Uses cached main-process session key — no scrypt on the hot path.
12261227
if (hasElectronCrypto()) {
12271228
const nbs = await Promise.all(nd.notebooks.map(async nb => {
1228-
if (nb.locked && nb.sections?.length > 0 && nbPasswords.current.has(nb.id)) {
1229-
const pw = nbPasswords.current.get(nb.id);
1230-
const r = await window.electronAPI.encryptNotebookSections(JSON.stringify(nb.sections), pw);
1229+
if (nb.locked && nb.sections?.length > 0 && nbKeys.current.has(nb.id)) {
1230+
const nbKeyId = nbKeys.current.get(nb.id);
1231+
const r = await window.electronAPI.reencryptNotebookSections(JSON.stringify(nb.sections), nbKeyId);
12311232
if (r.success) return {
12321233
...nb,
12331234
encSections: r.blob
@@ -1711,7 +1712,9 @@ function NoteForge() {
17111712
...d,
17121713
notebooks: d.notebooks.filter(n => n.id !== nid)
17131714
});
1714-
nbPasswords.current.delete(nid);
1715+
const keyId = nbKeys.current.get(nid);
1716+
if (keyId && window.electronAPI?.forgetNotebookKey) window.electronAPI.forgetNotebookKey(keyId);
1717+
nbKeys.current.delete(nid);
17151718
if (aNb === nid) {
17161719
setANb(null);
17171720
setASec(null);
@@ -1733,7 +1736,8 @@ function NoteForge() {
17331736
if (!r.success) return {
17341737
error: r.error
17351738
};
1736-
nbPasswords.current.set(nbId, password);
1739+
// Store only the opaque handle to the main-process session key. Password is discarded here.
1740+
if (r.nbKeyId) nbKeys.current.set(nbId, r.nbKeyId);
17371741
// Keep sections in memory (user still has access this session).
17381742
// sanitizeForDiskSync() strips them before every write — plaintext never reaches disk.
17391743
const nd = {
@@ -1767,7 +1771,7 @@ function NoteForge() {
17671771
};
17681772
try {
17691773
const sections = JSON.parse(r.sections);
1770-
nbPasswords.current.set(nbId, password);
1774+
if (r.nbKeyId) nbKeys.current.set(nbId, r.nbKeyId);
17711775
const nd = {
17721776
...d,
17731777
notebooks: d.notebooks.map(n => n.id !== nbId ? n : {
@@ -1809,7 +1813,9 @@ function NoteForge() {
18091813
})
18101814
};
18111815
persist(nd);
1812-
nbPasswords.current.delete(nbId);
1816+
const keyId = nbKeys.current.get(nbId);
1817+
if (keyId && window.electronAPI?.forgetNotebookKey) window.electronAPI.forgetNotebookKey(keyId);
1818+
nbKeys.current.delete(nbId);
18131819
setUnlockedNbs(p => {
18141820
const s = new Set(p);
18151821
s.delete(nbId);
@@ -1892,8 +1898,12 @@ function NoteForge() {
18921898
/* ═══ Export ═══════════════════════════════════════════════ */
18931899
const doExportHTML = async () => {
18941900
if (!curPage) return;
1895-
if (window.electronAPI) await window.electronAPI.exportHTML(curPage.title, curPage.content);else {
1896-
const doc = `<!DOCTYPE html><html><head><meta charset="UTF-8"><title>${escHtml(curPage.title)}</title><style>body{font-family:'DM Sans',sans-serif;max-width:800px;margin:40px auto;padding:0 20px;line-height:1.7;color:#1a1a1a}h1,h2,h3,h4{margin:.5em 0 .3em}table{border-collapse:collapse;width:100%}td,th{border:1px solid #ddd;padding:8px}pre{background:#f5f5f5;padding:14px;border-radius:8px;overflow-x:auto}code{background:#f5f5f5;padding:2px 6px;border-radius:4px}blockquote{border-left:3px solid #6359d0;padding-left:14px;opacity:.85}</style></head><body>${curPage.content}</body></html>`;
1901+
// Re-sanitize on export — storage may contain pre-DOMPurify HTML from older versions
1902+
const cleanHTML = sanitizeHTML(curPage.content || "");
1903+
const parentNb = aNb ? dataRef.current?.notebooks.find(n => n.id === aNb) : null;
1904+
const isLocked = !!parentNb?.locked;
1905+
if (window.electronAPI) await window.electronAPI.exportHTML(curPage.title, cleanHTML, isLocked);else {
1906+
const doc = `<!DOCTYPE html><html><head><meta charset="UTF-8"><title>${escHtml(curPage.title)}</title><style>body{font-family:'DM Sans',sans-serif;max-width:800px;margin:40px auto;padding:0 20px;line-height:1.7;color:#1a1a1a}h1,h2,h3,h4{margin:.5em 0 .3em}table{border-collapse:collapse;width:100%}td,th{border:1px solid #ddd;padding:8px}pre{background:#f5f5f5;padding:14px;border-radius:8px;overflow-x:auto}code{background:#f5f5f5;padding:2px 6px;border-radius:4px}blockquote{border-left:3px solid #6359d0;padding-left:14px;opacity:.85}</style></head><body>${cleanHTML}</body></html>`;
18971907
const b = new Blob([doc], {
18981908
type: "text/html"
18991909
});
@@ -1907,7 +1917,9 @@ function NoteForge() {
19071917
const doExportText = async () => {
19081918
if (!curPage || !edRef.current) return;
19091919
const text = edRef.current.innerText;
1910-
if (window.electronAPI) await window.electronAPI.exportText(curPage.title, text);else {
1920+
const parentNb = aNb ? dataRef.current?.notebooks.find(n => n.id === aNb) : null;
1921+
const isLocked = !!parentNb?.locked;
1922+
if (window.electronAPI) await window.electronAPI.exportText(curPage.title, text, isLocked);else {
19111923
const b = new Blob([text], {
19121924
type: "text/plain"
19131925
});
@@ -2405,24 +2417,33 @@ function NoteForge() {
24052417
});
24062418
return;
24072419
}
2420+
const switchingNb = aNb !== nb.id;
24082421
setANb(nb.id);
2409-
setExpNb(p => ({
2410-
...p,
2411-
[nb.id]: !p[nb.id]
2412-
}));
2413-
if (!expNb[nb.id]) {
2422+
setShowTrash(false);
2423+
if (switchingNb) {
2424+
// Switched to a different notebook — sync sec/page panes, don't leave stale state
2425+
setPgFilter("");
24142426
const sec = nb.sections?.[0];
24152427
if (sec) {
24162428
setASec(sec.id);
2417-
setPgFilter("");
24182429
const pg = sec.pages.find(p => !p.deleted);
24192430
setAPg(pg?.id || null);
24202431
} else {
24212432
setASec(null);
24222433
setAPg(null);
24232434
}
2435+
// Force expand on switch so the user immediately sees the notebook's sections
2436+
setExpNb(p => ({
2437+
...p,
2438+
[nb.id]: true
2439+
}));
2440+
} else {
2441+
// Same notebook clicked — just toggle expand, keep current section/page
2442+
setExpNb(p => ({
2443+
...p,
2444+
[nb.id]: !p[nb.id]
2445+
}));
24242446
}
2425-
setShowTrash(false);
24262447
},
24272448
onContextMenu: e => {
24282449
e.preventDefault();

app.jsx

Lines changed: 44 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,7 @@ function NoteForge(){
347347
dataRef.current=data;
348348
const aNbRef=useRef(null);aNbRef.current=aNb;
349349
const aSecRef=useRef(null);aSecRef.current=aSec;
350-
const nbPasswords=useRef(new Map());
350+
const nbKeys=useRef(new Map()); // nbId -> opaque nbKeyId (main-process session key handle)
351351
const [autoLockMin,setAutoLockMin]=useState(savedPrefs.current.autoLockMin||15);
352352

353353
useEffect(()=>{prefsStore.save({dark,navOpen,wrap,zoom,autoLockMin})},[dark,navOpen,wrap,zoom,autoLockMin]);
@@ -362,7 +362,7 @@ function NoteForge(){
362362
await store.set(JSON.stringify(sanitized));
363363
}
364364
if(window.electronAPI?.lockApp)await window.electronAPI.lockApp();
365-
nbPasswords.current.clear();
365+
nbKeys.current.clear();
366366
dataRef.current=null;setData(null);setANb(null);setASec(null);setAPg(null);
367367
setUnlockedNbs(new Set());
368368
if(encEnabled)setAppPhase("needsPassword");
@@ -453,12 +453,13 @@ function NoteForge(){
453453
saveTimer.current=setTimeout(async()=>{
454454
let toSave=nd;
455455
// Step 1: Re-encrypt sections for any locked+unlocked-in-session notebooks
456-
// so edits are captured in the encrypted blob before we strip plaintext
456+
// so edits are captured in the encrypted blob before we strip plaintext.
457+
// Uses cached main-process session key — no scrypt on the hot path.
457458
if(hasElectronCrypto()){
458459
const nbs=await Promise.all(nd.notebooks.map(async nb=>{
459-
if(nb.locked&&nb.sections?.length>0&&nbPasswords.current.has(nb.id)){
460-
const pw=nbPasswords.current.get(nb.id);
461-
const r=await window.electronAPI.encryptNotebookSections(JSON.stringify(nb.sections),pw);
460+
if(nb.locked&&nb.sections?.length>0&&nbKeys.current.has(nb.id)){
461+
const nbKeyId=nbKeys.current.get(nb.id);
462+
const r=await window.electronAPI.reencryptNotebookSections(JSON.stringify(nb.sections),nbKeyId);
462463
if(r.success)return{...nb,encSections:r.blob};
463464
}
464465
return nb;
@@ -708,7 +709,9 @@ function NoteForge(){
708709
const pc=nb?(nb.sections||[]).reduce((a,s)=>a+s.pages.length,0):0;
709710
if(!confirm(pc>0?`Delete "${nb.name}" and all ${pc} pages?`:`Delete "${nb?.name}"?`))return;
710711
persist({...d,notebooks:d.notebooks.filter(n=>n.id!==nid)});
711-
nbPasswords.current.delete(nid);
712+
const keyId=nbKeys.current.get(nid);
713+
if(keyId&&window.electronAPI?.forgetNotebookKey)window.electronAPI.forgetNotebookKey(keyId);
714+
nbKeys.current.delete(nid);
712715
if(aNb===nid){setANb(null);setASec(null);setAPg(null)}
713716
};
714717

@@ -719,7 +722,8 @@ function NoteForge(){
719722
if(!nb||!nb.sections?.length)return{error:"Nothing to lock"};
720723
const r=await window.electronAPI.encryptNotebookSections(JSON.stringify(nb.sections),password);
721724
if(!r.success)return{error:r.error};
722-
nbPasswords.current.set(nbId,password);
725+
// Store only the opaque handle to the main-process session key. Password is discarded here.
726+
if(r.nbKeyId)nbKeys.current.set(nbId,r.nbKeyId);
723727
// Keep sections in memory (user still has access this session).
724728
// sanitizeForDiskSync() strips them before every write — plaintext never reaches disk.
725729
const nd={...d,notebooks:d.notebooks.map(n=>n.id!==nbId?n:{...n,locked:true,encSections:r.blob})};
@@ -736,7 +740,7 @@ function NoteForge(){
736740
if(!r.success)return{error:r.error};
737741
try{
738742
const sections=JSON.parse(r.sections);
739-
nbPasswords.current.set(nbId,password);
743+
if(r.nbKeyId)nbKeys.current.set(nbId,r.nbKeyId);
740744
const nd={...d,notebooks:d.notebooks.map(n=>n.id!==nbId?n:{...n,sections})};
741745
dataRef.current=nd;
742746
setData(nd);
@@ -754,7 +758,10 @@ function NoteForge(){
754758
const removeNotebookLock=(nbId)=>{
755759
const d=dataRef.current;
756760
const nd={...d,notebooks:d.notebooks.map(n=>n.id!==nbId?n:{...n,locked:false,encSections:null})};
757-
persist(nd);nbPasswords.current.delete(nbId);
761+
persist(nd);
762+
const keyId=nbKeys.current.get(nbId);
763+
if(keyId&&window.electronAPI?.forgetNotebookKey)window.electronAPI.forgetNotebookKey(keyId);
764+
nbKeys.current.delete(nbId);
758765
setUnlockedNbs(p=>{const s=new Set(p);s.delete(nbId);return s});
759766
};
760767

@@ -814,15 +821,21 @@ function NoteForge(){
814821
/* ═══ Export ═══════════════════════════════════════════════ */
815822
const doExportHTML=async()=>{
816823
if(!curPage)return;
817-
if(window.electronAPI)await window.electronAPI.exportHTML(curPage.title,curPage.content);
824+
// Re-sanitize on export — storage may contain pre-DOMPurify HTML from older versions
825+
const cleanHTML=sanitizeHTML(curPage.content||"");
826+
const parentNb=aNb?dataRef.current?.notebooks.find(n=>n.id===aNb):null;
827+
const isLocked=!!(parentNb?.locked);
828+
if(window.electronAPI)await window.electronAPI.exportHTML(curPage.title,cleanHTML,isLocked);
818829
else{
819-
const doc=`<!DOCTYPE html><html><head><meta charset="UTF-8"><title>${escHtml(curPage.title)}</title><style>body{font-family:'DM Sans',sans-serif;max-width:800px;margin:40px auto;padding:0 20px;line-height:1.7;color:#1a1a1a}h1,h2,h3,h4{margin:.5em 0 .3em}table{border-collapse:collapse;width:100%}td,th{border:1px solid #ddd;padding:8px}pre{background:#f5f5f5;padding:14px;border-radius:8px;overflow-x:auto}code{background:#f5f5f5;padding:2px 6px;border-radius:4px}blockquote{border-left:3px solid #6359d0;padding-left:14px;opacity:.85}</style></head><body>${curPage.content}</body></html>`;
830+
const doc=`<!DOCTYPE html><html><head><meta charset="UTF-8"><title>${escHtml(curPage.title)}</title><style>body{font-family:'DM Sans',sans-serif;max-width:800px;margin:40px auto;padding:0 20px;line-height:1.7;color:#1a1a1a}h1,h2,h3,h4{margin:.5em 0 .3em}table{border-collapse:collapse;width:100%}td,th{border:1px solid #ddd;padding:8px}pre{background:#f5f5f5;padding:14px;border-radius:8px;overflow-x:auto}code{background:#f5f5f5;padding:2px 6px;border-radius:4px}blockquote{border-left:3px solid #6359d0;padding-left:14px;opacity:.85}</style></head><body>${cleanHTML}</body></html>`;
820831
const b=new Blob([doc],{type:"text/html"});const a=document.createElement("a");a.href=URL.createObjectURL(b);a.download=curPage.title.replace(/[^a-z0-9]/gi,"_")+".html";a.click();URL.revokeObjectURL(a.href);
821832
}
822833
};
823834
const doExportText=async()=>{
824835
if(!curPage||!edRef.current)return;const text=edRef.current.innerText;
825-
if(window.electronAPI)await window.electronAPI.exportText(curPage.title,text);
836+
const parentNb=aNb?dataRef.current?.notebooks.find(n=>n.id===aNb):null;
837+
const isLocked=!!(parentNb?.locked);
838+
if(window.electronAPI)await window.electronAPI.exportText(curPage.title,text,isLocked);
826839
else{const b=new Blob([text],{type:"text/plain"});const a=document.createElement("a");a.href=URL.createObjectURL(b);a.download=curPage.title.replace(/[^a-z0-9]/gi,"_")+".txt";a.click();URL.revokeObjectURL(a.href)}
827840
};
828841

@@ -1031,14 +1044,26 @@ function NoteForge(){
10311044
onClick={()=>{
10321045
if(editId===nb.id)return;
10331046
if(locked){setPwDialog({type:"unlock-nb",nbId:nb.id,name:nb.name});return}
1034-
setANb(nb.id);setExpNb(p=>({...p,[nb.id]:!p[nb.id]}));
1035-
if(!expNb[nb.id]){
1047+
const switchingNb=aNb!==nb.id;
1048+
setANb(nb.id);setShowTrash(false);
1049+
if(switchingNb){
1050+
// Switched to a different notebook — sync sec/page panes, don't leave stale state
1051+
setPgFilter("");
10361052
const sec=nb.sections?.[0];
1037-
if(sec){setASec(sec.id);setPgFilter("");
1038-
const pg=sec.pages.find(p=>!p.deleted);setAPg(pg?.id||null)
1039-
}else{setASec(null);setAPg(null)}
1053+
if(sec){
1054+
setASec(sec.id);
1055+
const pg=sec.pages.find(p=>!p.deleted);
1056+
setAPg(pg?.id||null);
1057+
}else{
1058+
setASec(null);setAPg(null);
1059+
}
1060+
// Force expand on switch so the user immediately sees the notebook's sections
1061+
setExpNb(p=>({...p,[nb.id]:true}));
1062+
}else{
1063+
// Same notebook clicked — just toggle expand, keep current section/page
1064+
setExpNb(p=>({...p,[nb.id]:!p[nb.id]}));
10401065
}
1041-
setShowTrash(false)}}
1066+
}}
10421067
onContextMenu={e=>{e.preventDefault();setCtx({x:e.clientX,y:e.clientY,id:nb.id})}}>
10431068
<div className={`nf-nb-chevron${expNb[nb.id]?" open":""}`}><I n="chev" s={11}/></div>
10441069
<div className="nf-nb-color" style={{background:nb.color}}/>

0 commit comments

Comments
 (0)