From 3bcb343fe31cfd10ec21e34c2887372ac5e44a2d Mon Sep 17 00:00:00 2001 From: LC mac Date: Fri, 13 Feb 2026 23:24:26 +0800 Subject: [PATCH] docs: update version to v1.4.7 and organize documentation - Update package.json version to v1.4.7 - Update README.md header to v1.4.7 - Update GEMINI.md version references to v1.4.7 - Update RECOVERY_PLAYBOOK.md version to v1.4.7 - Update SECURITY_AUDIT_REPORT.md version to v1.4.7 - Move documentation files to doc/ directory for better organization - Add new documentation files: LOCAL_TESTING_GUIDE.md, SERVE.md, TAILS_OFFLINE_PLAYBOOK.md - Add Makefile and serve.ts for improved development workflow --- Makefile | 100 +++ README.md | 48 +- Screenshot 2026-02-08 at 21.14.14.png | Bin 25218 -> 0 bytes GEMINI.md => doc/GEMINI.md | 4 +- .../IMPLEMENTATION_SUMMARY.md | 0 doc/LOCAL_TESTING_GUIDE.md | 244 +++++++ MEMORY_STRATEGY.md => doc/MEMORY_STRATEGY.md | 0 .../RECOVERY_PLAYBOOK.md | 4 +- .../SECURITY_AUDIT_REPORT.md | 2 +- .../SECURITY_PATCHES.md | 0 doc/SERVE.md | 44 ++ doc/TAILS_OFFLINE_PLAYBOOK.md | 618 ++++++++++++++++++ package.json | 4 +- serve.ts | 57 ++ vite.config.ts | 2 +- 15 files changed, 1114 insertions(+), 13 deletions(-) create mode 100644 Makefile delete mode 100644 Screenshot 2026-02-08 at 21.14.14.png rename GEMINI.md => doc/GEMINI.md (99%) rename IMPLEMENTATION_SUMMARY.md => doc/IMPLEMENTATION_SUMMARY.md (100%) create mode 100644 doc/LOCAL_TESTING_GUIDE.md rename MEMORY_STRATEGY.md => doc/MEMORY_STRATEGY.md (100%) rename RECOVERY_PLAYBOOK.md => doc/RECOVERY_PLAYBOOK.md (99%) rename SECURITY_AUDIT_REPORT.md => doc/SECURITY_AUDIT_REPORT.md (99%) rename SECURITY_PATCHES.md => doc/SECURITY_PATCHES.md (100%) create mode 100644 doc/SERVE.md create mode 100644 doc/TAILS_OFFLINE_PLAYBOOK.md create mode 100644 serve.ts diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..0be554c --- /dev/null +++ b/Makefile @@ -0,0 +1,100 @@ +.PHONY: help install build build-offline serve-local audit clean verify-offline + +help: + @echo "seedpgp-web Makefile - Bun-based build system" + @echo "" + @echo "Usage:" + @echo " make install - Install dependencies with Bun" + @echo " make build - Build production bundle (for Cloudflare)" + @echo " make build-offline - Build with relative paths (for offline/Tails use)" + @echo " make serve-local - Serve dist/ locally for testing (http://localhost:8000)" + @echo " make audit - Run security audit" + @echo " make verify-offline - Verify offline compatibility" + @echo " make clean - Clean build artifacts" + @echo "" + +# Install dependencies +install: + @echo "πŸ“¦ Installing dependencies with Bun..." + bun install + +# Build for Cloudflare (absolute paths) +build: + @echo "πŸ”¨ Building for Cloudflare Pages (absolute paths)..." + VITE_BASE_PATH="/" bun run vite build + @echo "βœ… Build complete: dist/" + +# Build for offline/Tails (relative paths) +build-offline: + @echo "πŸ”¨ Building for offline/Tails (relative paths)..." + VITE_BASE_PATH="./" bun run vite build + @echo "βœ… Build complete: dist/ (with relative asset paths)" + +# Development server (for testing locally) +serve-local: + @echo "πŸš€ Starting local server at http://localhost:8000" + @echo " Press Ctrl+C to stop" + # Use Python builtin http.server for a simple, dependency-free static server + cd dist && python3 -m http.server 8000 + +serve-bun: + @echo "πŸš€ Starting Bun static server at http://127.0.0.1:8000" + @echo " Press Ctrl+C to stop" + bun ./serve.ts + +# Security audit - check for network calls and suspicious patterns +audit: + @echo "πŸ” Running security audit..." + @echo "" + @echo "Checking for fetch/XHR calls..." + @grep -r "fetch\|XMLHttpRequest\|axios\|http\|https" src/ --include="*.ts" --include="*.tsx" --include="*.js" || echo "βœ… No network calls found" + @echo "" + @echo "Checking dist/ for external resources..." + @grep -r "cloudflare\|googleapis\|cdn\|http:" dist/ || echo "βœ… No external URLs in build" + @echo "" + @echo "Checking for localStorage/sessionStorage usage..." + @grep -r "localStorage\|sessionStorage" src/ --include="*.ts" --include="*.tsx" || echo "βœ… No persistent storage calls" + @echo "" + @echo "βœ… Security audit complete" + +# Verify offline compatibility +verify-offline: + @echo "πŸ§ͺ Verifying offline compatibility..." + @echo "" + @echo "Checking dist/ file structure..." + @find dist -type f | wc -l | xargs echo "Total files:" + @echo "" + @echo "Verifying index.html exists and is readable..." + @[ -f dist/index.html ] && echo "βœ… index.html found" || echo "❌ index.html NOT found" + @echo "" + @echo "Checking for asset references in index.html..." + @head -20 dist/index.html | grep -q "assets" && echo "βœ… Assets referenced" || echo "⚠️ No assets referenced" + @echo "" + @echo "Checking for relative path usage..." + @grep -q 'src="./' dist/index.html && echo "βœ… Relative paths detected" || echo "⚠️ Check asset paths" + @echo "" + @echo "βœ… Offline compatibility check complete" + +# Clean build artifacts +clean: + @echo "πŸ—‘οΈ Cleaning build artifacts..." + rm -rf dist/ + rm -rf .dist/ + @echo "βœ… Clean complete" + +# Full pipeline: clean, build for offline, verify +full-build-offline: clean build-offline verify-offline audit + @echo "" + @echo "βœ… Full offline build pipeline complete!" + @echo " Ready to copy to USB for Tails" + @echo "" + @echo "Next steps:" + @echo " 1. Format USB: diskutil secureErase freespace 0 /dev/diskX" + @echo " 2. Copy: cp -R dist/* /Volumes/SEEDPGP/" + @echo " 3. Eject: diskutil eject /Volumes/SEEDPGP" + @echo " 4. Boot Tails and insert Application USB" + +# Quick development setup +dev: + @echo "πŸš€ Starting Bun dev server..." + bun run dev diff --git a/README.md b/README.md index 19da9dc..b22f222 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,49 @@ -# SeedPGP v1.4.5 +# SeedPGP v1.4.7 **Secure BIP39 mnemonic backup using PGP encryption and QR codes** A client-side web app for encrypting cryptocurrency seed phrases with OpenPGP and encoding them as QR-friendly Base45 frames with CRC16 integrity checking. +**Quick note for Bitcoin users (beginner-friendly):** + +- This tool helps you securely back up your Bitcoin seed phrase (BIP39) by encrypting it with OpenPGP and giving you a compact QR-friendly export. You don't need to understand the internals to use it β€” follow the Quick Start below and test recovery immediately. +- If you are new to Bitcoin: write your seed phrase on paper, keep copies in separate secure locations, and consider using Tails for larger amounts. + **Live App:** --- +## 🚦 Quick Start β€” Bitcoin Beginners + +If you're new to Bitcoin, this short guide gets you from zero to a tested backup in a few minutes. + +1. Clone the repo and install dependencies: + +```bash +git clone https://github.com/kccleoc/seedpgp-web.git +cd seedpgp-web +bun install +``` + +1. Build the offline bundle and serve it locally (recommended): + +```bash +make full-build-offline # builds and verifies dist/ +make serve-local # start local HTTP server on http://localhost:8000 +# or: bun run serve # uses Bun server +``` + +1. Open your browser at `http://localhost:8000`, generate a seed, write it on paper, then encrypt/export using the app. + +2. IMPORTANT: Test recovery immediately β€” import the backup into the app and confirm the seed matches. + +Notes: + +- Always store the written seed (paper) securely; treat it like cash. +- For larger amounts, follow the Tails air-gapped instructions in the `doc/TAILS_OFFLINE_PLAYBOOK.md` file. + +--- + ## πŸ’‘ Safe Usage Guide: Choose Your Path **Before you start**: How much are you backing up? This determines your setup. @@ -263,7 +299,7 @@ You now have: ## πŸ›‘οΈ Threat Model & Limitations -See [MEMORY_STRATEGY.md](MEMORY_STRATEGY.md) for comprehensive explanation of what SeedPGP protects against and what it can't. +See [MEMORY_STRATEGY.md](doc/MEMORY_STRATEGY.md) for comprehensive explanation of what SeedPGP protects against and what it can't. **TL;DR - Real risks are:** @@ -341,9 +377,9 @@ bun test:integration ## πŸ“– Technical Documentation -- [MEMORY_STRATEGY.md](MEMORY_STRATEGY.md) - Why JS can't zero memory and how SeedPGP defends -- [RECOVERY_PLAYBOOK.md](RECOVERY_PLAYBOOK.md) - Offline recovery instructions -- [SECURITY_AUDIT_REPORT.md](SECURITY_AUDIT_REPORT.md) - Full audit findings +- [MEMORY_STRATEGY.md](doc/MEMORY_STRATEGY.md) - Why JS can't zero memory and how SeedPGP defends +- [RECOVERY_PLAYBOOK.md](doc/RECOVERY_PLAYBOOK.md) - Offline recovery instructions +- [SECURITY_AUDIT_REPORT.md](doc/SECURITY_AUDIT_REPORT.md) - Full audit findings --- @@ -377,7 +413,7 @@ Guard it with your life. - **Issues:** [GitHub Issues](https://github.com/kccleoc/seedpgp-web/issues) - **Security:** Private disclosure via GitHub security advisory -- **Recovery Help:** See [RECOVERY_PLAYBOOK.md](RECOVERY_PLAYBOOK.md) +- **Recovery Help:** See [RECOVERY_PLAYBOOK.md](doc/RECOVERY_PLAYBOOK.md) **Author:** kccleoc **Security Audited:** v1.4.4 (no exploits found) diff --git a/Screenshot 2026-02-08 at 21.14.14.png b/Screenshot 2026-02-08 at 21.14.14.png deleted file mode 100644 index ded40a3f941154a1e2572ee3a4ef8a712d922e56..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25218 zcmZU51zglk^EeICh&0Dh(k;?=h_u9sC?Vb59Y=@KqLg&Vp>#-hcT4xt9Y@3Q_j!JA zJih-(zvDP4sNq&97=Kmy{ZRjYeuKw{pH{C#a>%^n%Qd0SlRt%&uyg*Tuk$UTideDTOWhU?TwEhV1UJU#DQ@0xDm|_h%?U#=q zGvc7L%pkF7D8s4zC9uNFd*FO@S0?)oe9IRBc4hoVAIRwg@fGNwytX#Wo{Rwe+ASC^ zHK?teDw*3#_qb^ug>CRQ-DsTG>HB5b1_c&5_+kH;XE%wt2_vH&598?B+jF13g zVzj>!fnRZ4C)Y_+|CRi2Su07LdUoJHYW`i+3TB{(8E3ALR{k%5GlwA7f3W@w%?vBX z`rCa{Ut{vWY5f=Z3)Rr>f4AU2xwjz!X%WyM$l(7rviJKa<^Q$zKXqRkW)jD#mqU&} zYX47%P({h8(f=32J}H_r$HgNsKi+>a{LP8cUmC=M)Jz_*S=u@tYdJWp%DZj_#5=Lm z>Kr>+`8iF-s`1jIo`G+B3I~n4rvqY16Qyq(%m82h(V-WqjC|$k5ufn8G$=SNnes_! ztj1$|3Jwam_MDN2oX1lyAWdlNV6NTgL8DPo-6=U0qI{Cn8q4j4=G#OdZ$#*^z zswqOIUk2Dp-{H&^&=5gJIsQQ*fY=d(C0||bIR@M~MnnHOdB<_==}NRnKYOa##+}xI zEVPs1*(r&pSv-$rp<7!=?JLr24M8r>0c%+$>O^_H(Pu!baT!b5p8Blln=A`jvtrX~ z4$iZX1&!03D1n^ltXSXCx9}{^By31wy+Hc-AKb%4BOUpUlgnGr_a3}!NiX-+Y1Yux zuc~4t@BAQB(+Yc9~an@5Kv?*CiKaTHEJ}%7=`FN5BP2+?~%t@3!4%6&X|z zfjdzKWA=xdf&RUY^-;j;teCyil}bJ(;-_yp|6g_ zy>D@cRyb)A&8YG)RpO-%BR&&XghdVwy>2Vh4JPLA4|`>FD&~-Xu52Id9ispABB%6; z>`m55a_G}F<^{Y)qHjMiWH*l-pN3htO%D>j8vx_05h^95Uf@O)lfQw*FF8TqNXliw z_|`Ob3q80^eZpkepG+sruJpNu`8~DZ{3W_aj}fXWr8y*DXZ*)3M+^K2e;d+oB*N&P z`^n4Ck~rK&%?+nF(mHr-#@5`MItBmB=Nx93byj>yC26Cp+?x+?bNSweo0bz`2}wg2 zKAw<0=B2xdxS)c=bwAK(XXfxON#f>7;L7622U{hoVHL7*Oyuc*t1v?3skFz(^c4nB zTJZD@IDh0UZ0)MnEg3Kj77Zc%WHD&Hb`XPmwLK^b=pVTG?4FF|>OJrTv=BSDq$PSy zGwgNw9;&~3Io~eTf1d?jdIVFr;Wy}&N(GGaTRMB?YcON{Lmqs%XrvwAae@WJf*NT? z1XZ@>whPjP?c;U#|x?zJ^D#85nFZdoGg)T?&>I z)wW+Fu3yLwyrG*+PrcO1pAXAKOp?Hcyz}4iQd#q^=dI-ty>M-GnKz?#{=`XHq!@ms z3gvFbgT`C0)~b!z7Ql(NYB=ibmMW5xuR>Xu!5#H#CdQF| zVZj4E-fMIXJ|vZhsF&}pi&VhO7*ZNJr(Kzj7tX@tBpei(w| z=GC{WJ_u6!t$hw(o}S0sdS8wM%*oKuz7k6J1&EYXhd_|zI{g!g z60Yr5$#vy7XlU0biWO}m$g$6z{vKSl#;Y+3Rp~Pb?tjX6lsfR^#@jNYLxt9vGqOjN zpIzC+c9dH`$m)I8NwBP{Z`0isjOsOw%fammb$TU6opF^ErI)9;g8GO$bKQ(84TZTa zR>GK~v!K$@kw=4HumCd5Y=P-pra3F-LF4t*Y)$$R5{XtwHHt}HyA-?6R)>0cF9$l0 z2}=u1De`$$e^>RA7*ln1gFVzaiIo2by$~&45A$Whux5WlzG?W)RSq~CC)Ma!6&7_# z<29Ym&zBv7M%h0^Pga|Bv&jjm%dj=Q*lVj27pGE+K^L=nLU*SiZga@wv%(-wvh(!~ zP>f`~gThVbk#?A?sUD8YqnpQQ|4_Yb417*$xHYGSTXpXqp~rerR@u9bXX9+;km}*~ zpbPdDgz$fS=!=7$e`t-ceNoTV&ji{_M$EJiqfMGJ3Hg`oR3#Gtl>)KGEsdQExE+&}3sIp6B9ZeL2cahV=+VLQH%q zu_t=z)h9;}!#6Ra<^?@`hz5<4s_)AWfhrGE=X`WlzV7V-4s+ct*CFRXm#U$A-iKfL z5?66Fy}=W(*?I?Gvk$=Usv@|Hv3HZ^NVvr+zAcKRvN3gA8OWn~hoJiWzQuGXLN6NYTdzuz{mlGW3W+FDuEt3HstuRriUcsi9P&}9Xaxce}KoH5Ye+I{CW;x8~Y z@dJnIqz$*Xj|ZyHxTifYT7{d)Yy~6QJT9~^ z#)0>mqmt$>4=XKg6~JR>Y6MO5l_*UbXnwP0Z>!kkmT+s)ZFpAoD5Zb8aTy6plE%)v ze7-fK*^%zZ^tSU^_9?zxi?ma_w* z=WVtG)!cWQ`)y`ECe*z`SzLLmAIYoJGgVx@?0=&oVnm~cj~Q^U)bh|U<FfrU9)MpKEqQ$ zl#I`hX0++DVmkl*u~4=HiJ`(XM7nRz!Bw5dTvTcMN{rdZZ3)Y-i;Gz=eQfcvgGClm zrPBkG=>@B8oos0hs;G>%5+*HDEO5h z(mgul2D?otr@o+fOc)?Gr@qsg`<@IaJdd^^-g{W}pGApUYVLw*U4oR-T^yHmMQ|i5 zc9xOVZlE)+0_rJ`_LJ%}H_cWnk#Vu9qEI#5(3Jv2hEL4%n;Y8;`G~mu`!!5F;*~z2(fPG{h-Ddd!^{&C;lUE*kepDIT50wKsUGqLuDRQ|DID%J-swBt~ z=^wMm;Y>G)`klD42Zs3h?rjB8ka5WHb6!sxtLIqu7`xk)j?a6G_v3ZNh&vFXaD>r~ z<9Tmr2V`6P{D}t}^?dbwn&cg65lZpdyG*Nx3+O!Jq@_%c!oU(Fc}JoVO{aR3kVdiA ze1G6jxijs@sVAxb3`^`~t+|SQFvre@N!z1 z-Rt3#{LG=csymZ!|M;z;Z+q|15j#HB)JormK5#KtqF-+Nefc;%5S_3$%%X!}Wj6CF z&ka=5js;VxY5uGlg6e~nfpDlUkq5d2Cw1Go%)PZSChNe$TKGUHZ+ZDWQqTN>L_W{G z5xA|{D8Gv3g20Pp`SP*VPO^m8b;)h#=~UpmkYS1|Ka@cau}7|Py))&fbaY^cwZT=Gk%)P$iJ~+Gg9;?H@9(GgHp5F2qbqixCi4&YOiA- zQj2d-DQqVU?jXz3l6$2O-Srwpz1@uXs=~EOz8SWtn5N@b%h9YG_=}J=#g>UvJfNpg z15b17l>fO>_0EXoE~IS>i3GNnEhWwcoy^VD;l=cv9ctP7Jn+u8GDMb4?**iR1~S_(@2)g#A663?Ffv|lNbn&^QynlO z7dgcbtM>)G=byubcbLL(N~(*b{ipm7l&TN+45}$wf?!vkbz)R=d6$VrHnZxUK#ZH# z@@(~XGSU*~W~D@F&5(_^`b|Hb?% zrJ4s!mNrme6oO=rXwH4puoRXA6N^{%+BQmUPk`w)RH|y*Gq6662>s{AT~0d7^kcM~ zQ2DO+VcoR3-yxpR{ocbaWVq(;!hzu6d^wl1d=b-$S*miiGPC;p;Z%G6j@7e|3epT) za!zfL{N&yfM-H6Ha;3Day5wq_)|9KhYdc!ii$Oeg?oGY+$gJ*;0W=fXz*!~jbspfQ zbBMiW_}5X(i`UlTWa~#|E@MlRz-417YlfjN|7;goPzlep4J9;k5aD2Oq^fvSS9-#& z(4k;ynS`bZ_O@?mx>A<)_QdhtMpwR=#8y>}9G&?&pp>faeNkX{kyjxjZ5#gCRT3GZ zg0An{T75t66VAa|PxKD|8+B9N;)2|29R4SKNVN2(Bi6bxmf)kUx@S{-O|wvs=|pQb?PUQOHnPUeUo7Yh^~{E8DiW`ufZI z#e#F8lg^tXS&A9^Mo627l9;C9Q${o=Opuu?Dtg2I&nf>-BW=04!Oh)r{OmpU(W{@7 z%1#Ux#Jpeq7jF{NB0lHxM2$0t-vmhbe-leA*E5vbzdr0~HL?xfpue#BFwt;aqCy%o zi#S>`^quawm0QC3X8MHvk0X#7(!fVQ-$=nH(Fx>ODo(cN{Vk*9K+*oRtOKWQ*E=|B z%}gu)0|Bf!bteb$@wowT4H`BFe8kqv%KNhA44=a-j)ZN;Uo3LsU1ju#|c{+-_?x4nbcX-2DqHZ0T&I zvKbz$G>v9=3GmSMhG4iYLRv{=Qv7uZamOS!1l=#^iXcc8Z^iT=$^!A7@9LwKX;v3N zH@#Ss0qS<1G=b-i%+)Wm(+DZj1xgGLT9x*r0pfaLvFwkN$_@3U{&{F-_!fg@=~XC= zFCXO@sReZclH$7+N^Y_d%@QCBRq}(~%afO`zioRPe?S{S4nmkzfPZ5Q)kqf{pAg1H zpn=tiT+bXJ&9YfZo5>JAtXLF*e)O+2+9h)67Jm#9>D$3&%?wKVM03A_Sy!rIc>5%l zZ=<{lRI@}FeBqiV=a%eX@%6MdSlfKO?LifjYW47WnfDqY_O}W>5fDiDp6&cmArWZC zZ_;tGX*LIFMfOx&q4QA{SL%g>Hry z5Z}X93%?F9i3;_Isssa`LJM@nhHyE_JXuYrNeI-l#*&bYG5_qKIDtTX&Iazg(w4C}R7amutPAfHl3nXpK*hV~Qd=0)gXxZtR zvcH*;kJ5n`h^NzpQ5oEZ85p!6*H^PKK%ht_h- z))1duzEL*73(*>awjDM!4VE;lsxA%2 zC!OMC6D^@|m2!yxEpOwz`ii zf|*_zK#hCZ+|vtm32l?oxkZJrU_nPBwjDs{+-Ku`k~s3s3n_AvZsv5px^{*upS?V2 zldUKX-Y+9+{Oy9TZt)LoW%TS}q5xLy)y^Vk%T^=Yo;)oQdXT3c(Du7C)bocr8h5$0 zK@TB^lcFt_#w*YIcf9(ChoD)HftauTDqveSH@}6 zNI}jsQq)G(jI&DceJp+J@@T8lc>_;aU;5G3Ntv^OE zGX`}edHO3(#4o^Aayl?%?Mgl4CYTIGa<_`JM;}Y_BMg;a#rr{(-2=x`qGB?7oB3I> zzUj;AVAk2bPllk3pP$LOREMltWzi0fiJH%OwQ!i4m&K+_5S&qiN1S()&zZaEg0M^=(6Ku58c>2l#m!k=#4XK9T5*ZZX@>*bxUiUx15}qW)#)e zt6Tc;R<8v+y~61%4wG1s>h!va_4;7m5+!4-hD$EzKkc>v8fnk>toAA%qd_4rE{=38 zzlCx`YB^y7$Msm~qIj4k5XfSq=VN(C%Gt?#v)XS4Pa6YM_St!qvK?1`0;&THRN;~n` zHzY8;{Mq9IB?J?x&LZnsj#y91`!~YCY*|J`Df;cN$VEvC*^)3t`LwGt`_qLLX7Ysf zIkId|M?V^u*JN&lo~?y`AAV>4-0PS@6u!>K02FgBiete;46Ien8Xfba?0XS7&y`Dw ziOMK~wIyncdfEQ4A~+ZkxhT;V z3`|sg1%$cOQ$^EKQS$?~J?O?ri>Hw@gg9cyMnXSBUr~3)tzb36Y5}Z+7H{fZI~5w# zVkK^0pij}DV460NC}SQkVYw->@_xcHvqgt0S5zoJQOnp{d#=DQtsN(=hObBp@ubLM zJV#q*HeG(zb{mv~B_((4t6De1m8uijsAkba@nE671Rj56ZY>pPif_)U`Ubn-EH1D} z97h%4X3L?+c?8t-V2g}N5!Q)Z!0`-F4o~f1mF;QAs zejns@5Mo)0VDyQ=)IJ*5yik&>qZkj$Lw)44pw2$T5k^QE&nA_nM$Jy0ye@rwLEJIp zs;D1z5lQ(mZP>kLt0Taj>MEyMiu@oiY|NapmYGm2U!AGBDS|M}CJB(V-Wrz?!fwdT z6e-6rFh+ly_T-hG)Bfue#DE*!Q{`N+oA{&brM@wvAnP&Z5j!)09#G)Z({Kh`wKy5F z8+;c-w%BeEHOI^VLisjAnq1kt8gsWuF%g2QHQ&an2eyt0_;5cYquPLdr7O8P^`_Tnhx!-C{YDa-cpP6es%^{{K?)H2g?CA+`qI9I6bd>lOLW}BcRp3ugr z;~^&VwFa0;rOrEcEEc~dwn(wMCAwnFGDfug^?c|l$Zpv@^ycwBA1I#qLG?5jrEZ~xSzt?B0~xU3 z#x?m7gCjynI{(j;{NFRT<2#$pK9;cn{-S5dx9uak<0JM_4!4d3{^VEgMhs|o%!-Ay z3L&JcVJ5~a9oCOG&QWd4sULSH!E&4VUg52FkcN^9G>UA@ER{8v5p2V6_%}IljWw&8 z1Fju;wD!G;ex11%8b1xMDk2J&*mm)97VLc?sx5cSH{jac1E|O<95>a7eZQ?xZh!fCoH#7^69~rm z=$b^Rx{*0~Sr!r>YCF#VIl&^o zD12hIFbY^f*Kb!rU!z@eRyAGz=Fer5AO=f}dYk?zB|cMy?F#|GJ2lmtECT)RU&IKK zq`i(Ewj?zct4fnK7Q3ov%5oYX1e_JZgakUa<@&GzvSa{!D+a+mxHhpI+et;i*g9Pf zU#oV!mA1x#(yK zs9%8wbtP#mM$eB1|2#gX#QXd3Tv+f+|Se3^6MQajbf>E=9v|2%5%7^-N~ky;p_Aj z^s`UMh`Hj;a>Z6;eXReWlTgD^SPi`U)4;rEqq=@t&5H$ zb4n08E%wYQp-tMrfQWE@;+^>7)$?k4gGQ-j_aFPw(R>_bWf&um-0T&ew7~BBt~}ne zsvN4~qJHr=sl{_vvN{PAcCl}VZ+@h4*gXdd6gnl8aMUP6<~X2}n%DiV8wdr28eJ8M z{I%yA2jscIuX3)+S2`u?@BdhA=;A2b!g3b6G}}cMQiU$VsuxPFZ#Lr3ock7apcjeF zKP=MkILFKZpf0BWdNTqaMZCDVwIqG(_5BYY$8QIFD`)I)$G0YuS^@o^C*7zFeAy(H znFo-B0X`A`=JSmD&Kn}oAlBA!@gxW5e=+|xxmZ}y0I9+{(-9j}XQRPW<$m`AXfI91 za@0@A?R`S^DD-TbEHZxSr@wLdW7iteIuP|@q)Ewcx434%ak@J+b$>P~wrYPM> z-L3443f+rOAAB|$)8HiAAdC1u?LQ)>*^%=4|8w9IVcZN$3<2;o_V*%W=ZoH6i9fMO zdlcJXrJe{jOy|@%KuyE_v$X-l3OKII&{DS|U9*Ogx;bwnd5G8k*xZp;RkZq8!@A%@ zhokx-bhQ4+zqhDT9WMJ%vHf+|L5+A@w_#Y!K+9&~v8<2xoQiPFOOW%sd0O@X^$bce z;{npkUVb)mLTc=KqW|_XU=wu+ooN`|7dwSv-)dxkL-TiQ@rP5;v%XG!d`kO=d*Ux) zbcYAbkCf#uX3zl#=-%4F-)aTRi_p6o&Z`$r(7v%d9evxvO4!cI(8{V!4WtJCa>}}u zzQiUeh>7gA@wNONwvivMv#22j*9`OH*=PFQc9r+vw>?(WqJy*E(|P@3#XyU-=JE|8 z`u{*|{6_Dxu8r0PiJV%dKFnKuLsJr>!@{y|r6>QNk@b-TN2i_z;{g7)^MW<~=B*3c z2bF(v%W#~LxY|_(WzOMNUB7JfJH$%{Cl({bk(f~C1^n-D8Tc1lzGBN|V2^JXoG*eo z$;wTi6&yf*$^|9=AvYFYG$W;HnJj2pgjNOB4uTb&vNB*25gObbc!>TNivP0ug&7?^ z{kin5oj=lo z>Q;oT{C;Ocf83WhIOs(;L6MK`ap&K95>@|D!wth2*lVTlUQ~=N)cI zFBJqPKLV7C@Dkuy^bx+;(^1Y)z_r`CwMVl+vvQIXVWT#kD@oFh{?1-`jW-tbx!~w8e|U>2ESO4@v?zZ6f0(2KG&qsZXKlG3l~wv5}OlZJ*in zQ;xRrN@K%mduoXQOb`8*q9NA`-eG^b8BS0ULio*VtPM;ocmliFzXwXG2FAsS zaPe=U@dfOrA)i1R#dpLE&2m2DiSW^D9b$E5J+DL^bIe5x9Tc{F%lt+-L{By9%VWv- zzEAWZD&nA6{#3kRv3_>RIabNwfj<5nHs)>lh(4KS>-(i%-aQiM#s_Wtx(w@RO-K)3 zXN>q7A&Of3QPSkOjML}ytJ>^vp@kEalNEX%bQ6J1O4CqU@@EF!fu5CV47>8}xnz3p zkY#=^t`xkzeTaL(^wwCw9+De)%pM9Ud*+AMw{{Li5*sgNc0FPS;G7)Nsw-{2qnP?p z45d3xlT7lB{k8Hw8?{C9brN%S-{h;7Bfy9d>b70}0WZ?kmc5jdr!fTmHfx-R;=6!@ zXEA;iO>sx(R~#h}Yn~FFlYqeJ@WxNw5CkiAaW(1rJHPRSbK3-1bK8jb?nHz=m*@`) zdum~mWruo*Yo>Ce zek1J#X`#Ad}F8Fkc5egNz={K=c7Eg}DX)BXly=Xcyh_ zlw;)WeMlo5>v1=}C^b2$a(@I;oi?AEh;oaL5|i$`>4%JKXVClyceaT=g1yNiHT2QQgP=~H4i#__#em<`AvIAwGvQI&q*Ykc-)9eVO4o(*5RONT+y ziW6Acp?Mf<0UCN@iO0zV`+Sx6nY5BNa6{oS2~m`UVkGEUsKm7Bp5j`8Iklk4?oJF% z^b4W5D5*7Yp+$Z1d_^DD?QJD_QIA^+c!2!SBko{#*>0r7fIz|Pm-DUBdgzG)86_H# zp-9y??suY`*Um!1VQfDsx^geR#X;`d0wd?%j7SK5^|hv(#uOXM08f~!q#M}?7gr+N z7MI+E1xE+c@i<8AWWMshi}X zm$V#_JERnP*lWdxcpbtDznZB#XpZ6OMP1I`_EH1!Mgu6B`|~0{6aI3!YoPt&6G3H+ z4-ySqYFvNWfJEvzw%A|#6toH+rnWwDde3i+{$+%+?R%+G0R(=cGn^!NqXPnVnkC%IBE zKP|vO@U3?2@B1iu*(BkyG_F!W@7mhVEv$$d60{0hTokmGfFhK1xs{5D|Y|7QEL#Zsr*PBHBH*Lv%dI(Pov zX$SwlJ(xSBd_L2w)7~3FXM@q7WX+p_X}gNO${FY1#g^P5UM? zwb__P$boM!m5W?6kban;8A?{=E|Aqr+Icw34Xeww&wv{$cneN!TxN;6&k^ zD;8)i%oXo+I+*|Sf{5sjkE+1)I#o`e6iX&Imqv2;E<4;YX~e*n8J7t~ahZD32sDvD z7g)L3W*Ju54)h(pj+MkGFVPS*Que{|>vc2{z<)$dgkwKrMqSEVv&RdouUoA`UOn;c z(x5#x6IF!7sS)+R5Phg?)7x&fm!iXWL7rX2oBHOasd(K+DJsIB{alqxFO0uVBgEtp zf0l4vdEa&8k?s~+JX=53jK{kARecx};*3#S`2A61Mt6Vb!3fA&$D?CxZ@0{aNW5*y zyZ`!PGP2NC6pV}qk#UwNO??(Stsm#hU)`}2TgbI zBitoE_d7q2%+yah-w)>3nIgTBY+1#0=y3~xBkDg7S5uVT$imv^L;Rxsw%1e%HsBo} zu4E;XFZTK>ib&6tNuRM8+fd(92i_Y3Ng7_u4a=wX#S--ZQ`aeI-vfI_D;4)={8;nx z#`z%&cWbYSuh|oQaq_-N4<3Y2IRI&jj6vJJ2~3wAN9d)k6tc(2-k9uVizT1{A!ZAZ zNlDa|&=dMmisDcWOJ9C_bUPqsU)ar~Nv-F0q^2=2_LB8hM(<NLCoAAsV;x_I>bd#h=9*MUAXThgwyt)P={7a7q zW+hJ>98ZH|&qxvcgepA*A=>(5gg+!?8h&Y=w}<1r?U7x74i@|!SbFYBF1?3pe=nA< zNEYWOGTV=X7~-l~9LZ%@h(yr+Tdagnc9<=6*SAzM?T$pZ+%D4Pk17gPMdmjRZ~OH| zB&{X=c8zR}c52{6VuSjzr{DQrh`=D?##O8vkL#GViHl7s??2A(7PUItIMEUYyOwBu=8ul-G4prm`ZfT$} zl9wIfdN2G-bbT|orEf(=@39ehO9V$wfE}3*H&2nkpB*FV`w8QBvCXUyA zuk$Gm=EChOqs6Ohr34)|ZEstK>&B5M)qA$`M?4ODk4H!LQ_PpWt}Z6&_7QboQcpaO_T0-Vty|*sC$sV80`KjL@_)po~UaZKAi4Q z$mgCpR~A<6rK5Y2<#gbrt5fPCi^>eocUz_NTx8!`$q-q%$mE}&vYWS^Hj)6Nvkm|V zDDOKCgY_4%As|a7O^KbP5Wz~AYK@I5csgdupW(1n%E$41!o9=fQtMf1qHFs{&jI$W z*|kvlnW?38_dVw#7IM_&&68CfrWBU^S}Z~ircq>%(vr23Civs+il?~clrG`{^Om~1 z^#UB_HL6ZDo*tM`H4rP&bu+dFlpeWFl1y38Ejhmz`(+ z>Se@TcC`dovYU9?*rL_+gf`)P!B3aA_s4@$gKHU+9@XOUZ+*Ja_fI z6I8#|?V9{>I$>`LrF6g1k?W1lMM7KUU4P)c#fO}eNpUEjNKNzqZ5Xo_iI10KZ_|6v=-Fzb3Y-!j`V9;VHpRjpGIiSHq+hgRd4u<^7 z57LqH)bgL-uzelGPo7spvz8CFkhP##);@}&qU??Oin;AWm_RPD?z`iUv!>zXY zFs_Zy^m{eetEQ2iAm)dbyV=X3T^L^jC<6MVS5hscc=Au;-M zHMxv^aVddt`>ZmOx5@!te;KO22Pd*KOcNpLQuqI{aW6;hOJnGo>3aNW3Iq*1FOLt# zcsB`84h`4gC4h>=P#Ep&J0px69_e)6)zgLUc{AL2Kf8H_o>;WF(QIE+D&bnF(s8vP z1h<~izsRUG*n-|C$vf0ASpY3dL;0Hk@a57-hnm26q$%xCQi%;Ir{EN7QPvTae7Y$s z928aBb_{(Kc9PvX)id>=(UKkqQ3a&Ho`_tf*r zpJ^Apoux5v>Gta_wjKcf6sB{jZ`wmB?N>UM-QBEx9qvjts5n-*?f$Iv`$8c0Y{Q6W zadDVQS>kk}r!g&lgMky&V}H)f1-9xZD?U-q9XUK3?TIpmou2q&>#}L{{NhBs+0LfM zwwug$ZtplQ8xM*c98b-<2msVPFz&TN`?0p)rlD^=46$a|39tD^!Si*{+yiW;pb^4p+t4-?rXWrR0&rWXljCeTti5%{avJQ? zQq%N!xD(Yg-iIZF%OKp@Au#4Kc!BN=ChSKAF*o8{*|D7)iTSY`qN38P0IoV?KJWTG z)#X|LYAe*ug4R2VyX*6k6!qIIaSF%*P`{?#UjMpw+5j(jM|i}q2@nk1T6b`7=-Lf4 zb&1oU0*_`k`ZA7e8V~g9@9i{R8B2~+kh0fi$1uaUStl&(Y3f@x@}_4XWdLh*4XT9` zcw$OcWb#}WIR)kB7M)W(=x)uq!All#PKP+R7XJ#>tKm2g#gb^(pe#1XlA0xA4N9J1 z&t-GgdkLlC6}E;hgjKdkOYLe`sj zne#K~U~GXO1Xfg;lHng>mMa~#6dQKB$O4F_aR~`}hRk|+oTP8(Ea+Y=*>1!n*@BPI zi|TbRC-2>{4hv`1^JgEzi$91v9$i1j3QMB%4qB`}eKZYE3y%G%^GUznUi6D)?TP<$ zNA?nVZOIshkg5$%ho`4DOJ%G=MXH{Gd3$x?2Ow4hOBaj53Khro0&z`1%k?C*`Vryv znU+*7F`*dXkizjOhcl~_tW zdyhuXgH}=ZLV#aO8L2DIo(AaCCc*J>bHiHl#6D|nw{WTcCfwwg`sLbc7n->J>gvT+ zzr`q>>E}<2>%Q=S?2tnrVz z&|s#xo|x|7-vOOC+454FuG%fkLq%n)t zwaFk1iZkLI>uc`-xab~fPoxL;spO2j*-V)=i12E?Ap(Y~EUPMFUDGcUPP3=HZ)orJ6v6ICZf^M4 zXS7N#igt5ae@cl!l4yRg8%%%6+&VSyNZ!)0n2jb5&@%9YjbozuGwy7=9u?nC%!|!VJvOBfJf)UO=IdQRZrF8qQ-q zu0_?+HBQ@?G%3;ZzEZsS`0XLFUtwg$+v3`{pB8Ea;6HXRtD&3UOEkfZAnas`O5iN- z@8q@@248;QZv8Rk!ng8~q-ToTHABG9rhB)rCY--*(~*1gvegUah@)8|qexK%n`S@Y z`?JsXs+s&2Uk~_dPx?hxPg^6*F7P}J!sa(Jx)XD5S4NvdepQ>63E44d)NbBF9SP>l z_iBmgO~jYd>+ENt)i|C`Ki52l>wgrmF1i-w4R&$7mpo35co#rcYcMfm#n?2wA(B5? zeD{b;U0%-tXtJWEO0^V=rQuO_6Hq3p!CGr9tDRadJ66*l7{(b+vx=h(e}&ysRG8tY zaLIY7KHACO=E_*Vl4TI|D?^=}_Auu0?Q=4nwES#d%bEEGiYr~0yIJ=^KRhTN`N>># z_>)F~iE_}V!Y^CxV`3JoYN`;ZPgV!JC(7x}sae(+SsW3A0QL&}~X2VSQ1O#Zw>&omR4bZXtAYxHrXs=gk~1dR;qdRCoaBBC2Sy!Eit zDEntsf|(?XL;F^Bo{3$XelZJVt*%%hsofjxD;C=)wTigNW~GgH+EHX*hatzp_9A_| zmkpLe+cxOlQ`%*%DlIH{U(|T`_1sv-o$WaGW}iqWzZ4ocbI&8(9;C4HaJhYRI8oLd zG)0mx5%QC-Ju=ShRFK(~^3$U>xFsQw!Su+O@?BI%+mjfE;H;dN6vgX6b%p z-4T3ZVJsW9O3;&Gb=ih`$Br+(7ZRgOW-K|AafxatuCXvxJ)9l^;G;j%I;#TUp(Dyw zcw4XbYbeSY<8S)+zV@)*3~)mhoZ-`MvCYoD4C|YeXHY!4*?9x%freDarL52Jl^OH& z+RRvZtC@&P;4P$F0I^kb$GL*65BS$V%!YEciSd{Siny@@WJGFH(JyqTwL8)h_iziY`Gbo2_{*0Qorn* zcLwWoC*5F7fWXYSk05*2mw02W9}k%iSS(g@*CiNNo_AVGNSY44FxcSBq@o}Wds|g}2jy)0XZ5EF`Eb~TzlYbST_BU~7MgznK30;`n!$KQ+SBo2fp@5s*}moHeD-a9_nFJj zCNj|++Lz&3cT0<&YY}`FxKYBwF9&P7JQ>bST3$kWOlZC1MLWvn_k*r6bFsQ(C7*|m zb*uZ9h#U%vWAff1@0%pc?2zN0iPGA#Eq#%%d*#vYDN)>YjFJIDG&++NYJxz>;&rFb zv)kR|3dz>wwo<0s_KF3FzJWuy0I9b3Ivp0JJ*%NBroO2M=%|5Y`U#<~E^jBK)Bw)w z;Lzo^j0JT+7UT@Et0inGzlrZA!PWJ%k4^D~emFn#OhNLAX!H!}7+#h@>D~pqw>O4f zNMuw^@Nt*y?cd`z*OsK#!1i{bIa2+^)6piJ{z4`Tj{7`mLgIZWSmCQ6dZ0@Tx@%5M zXKY(|CEvVXp#LAwmAQV7twL$;)HK~ zGu>q$t*!rDpa+L%=`o!X_2?OLF5(mw@jzVU=_#4Dl|4ynbhVs+oD-2z7iB`QI+{{T zRDld143G#J@~r7N5;yKdB2b^T5Iu7b#OzTRb@csNYY^Ke1p8K)QGILRyQC>u&!`eD|5_z2#sBsh(fsXjbAK$=#v`ew znGT4w6Bd3^kA~!4@Ho&p9#t-Cz|G{hk06|i?@2OxlF|(DhrIcz+r>P{6AfAEd7Ypm zMAz+(o4ds4YZ)RD0PBPqb9OQDN<=1v4Y^+%vEGobusY4XLB|@@XjCf0^UnOJ9;feX zotrN5U}#&Jf-X(#(|nzQx{fdU7C!lTw?6RY;&+FR{MC)MSw6$9lc%m2_LV7E(iZ4Q z9$CqZ&gRVzqz3{L8_53P8ZDxJiZu+L3kS}qWq^IHbdms%pPtcznxBC{09~ukQ&@Wz zbUk#-_ua;+bZ+U$Q{O;0T0HA*mtevKhtiiVcQ-G|`MA(-SLoRLQ?Q0?2*sc7ToWTY z&mBQ+m6&0tgp@&$)&gI&>G@3M4s9{tr9gL5;8yb;?jjH7#{oq0_4g(iyrQib{tzaTZDiKRck-SyVc1UF_gXgOj%nwIw!Vn!5hN zH&!nMWDKCnFP_`Nz3F-o&hsN(;j|9RzH4;i_(ynv=%mlr;n=J9d6(Xfdeoia$;WWd z6=Y!Z=+wDh(4|*KAZZn*&QH%5D5bm*E`b&ZwL8sW@MWgkH&AS`njdiG|8;ZTQB7^x zA4ft7f+PqSdZ>aF0R^RpCa54?MWusMq)H7XR4LL$K@$R`VG@15kvDIC^VV->&HAlh&cAovoPF-z`|Nwq_p|Hi6c8uj&`EP-05Z7wc!#2h zn~GD+_#L84^Z6bk8)pYsP1BMEk;+2Pby(wOR^>@6KhJKCYbx}F&B<%j2RJ&8VW$Z# zNt+gY$g=dI9GvD892a$z9iV-a+Vle^6$Lqsei)I`w3?i$y6iUgsFis!_e;V}kH%+N zTeR-3KF#c{^H-uUt<{bpCA6gnFGKwg9|z!yAYO}c8b!eJeGCM9b@O#L8wFh-fs)AWwZ@}rI-UsZBC+}x4X7(Tizer zk{`G+sT#KZMs`K|&|@=8kx(N&=o9StJWP!ArEWA)+e^nfvtkgDT;f3%3?k7^&oTH5&e##w{mM(8m=#2^{uY3wX+QL z(f|%bT79#%NOaLTt&Bh-4`Ztaj;|VL67f-%w81S|SL?dUf~I`}1T3cOX$sG-D%=W& z?KNd}uR&eB4xp+po6S8bUr3ZpET#yv4e@vFSP@Q;;o$U>--hp`zxG^Ae$hRy>U;Ew zl#G?%#6~N?Pc1NkAj*}2U~BW@ZSdNAyP{~7p)RN2xwo0;J94ojbE?pK_1HGSUAMs7 z1}f*>6)BlB+p!)880sJBUb#4@Xx1krcM$tj&PBMneA0v~vaycfuas_iQ&>`tYnkXX z+=uijPU{!~A^fYyH>9L6viZ(K{HL2w1S>o`@8*wIQKVSrW>{FLhpAgy`)R?(&QU0T zI|>0BI(D<$)#g#we=Y%7grn49+M!3|TXbM5y z%Q)GYXXYA=p19n(SYv%>K?m#ex?;v7zjR7mW@=I;bn(Pxv}XPnziBaKlE!^bWUKdW z47OBiICkNVZG`VuZu$00*|{QKzdEy9sEpY4Zm?O3T8av=VSt5n77ZtY^^~l?d-EQcP52vK)tA&?hGd{ zE`)1f$F-MzY#)>|?|JoYEpiuH*g=}6X6j_dhpV378*Qb5Y7bo4>9zG8l|*6{cAk1p zM0CqVLom?l@YUYK;g))H=~ZPpA?5`QgNliX?t;)z=5?a6CS4H+R=Jsd>q)w&zk)vKL}3V2s9_ zx=NHNj_+a>S2(a0Hrv3eFMk1WZ3_{-^MY3RCINj{r()cNLNd`8f1Wcj6PxhPam8c) zLUw8APGHDY=M3gmPzi0Za|HcE^KT8 zE2`x;uQFfD#k3zeyldDFiyAu=V`OY?m4DvEuJ+!dH?WZ=1U^U;kH|OhIE51|^ z+V|Yq7!F@%RWi#=kAygGpO(U{74Id(I(_HMRI1!xPo~ERza0ykdL3_7=A+tYeXYG{ zg3aGZ_2d1>@#vbAg!5qfU7gXB+2Eb5(XF)NsklXyzj3NQ2Ib`v+d%2IEM{j|u=kca z7mX`;#%GI!f5j9TRI)sK-L`n;GmVFRz^;_sk;$0CR~FHyijbUcwXl0S92StTDc)T6-KHe1{j$b z@G{aWZq2ROxqwTq(J>;7nQaC1X?59YMVVfqwM-a$JEiSD5DIo*&r4R3im=nOnj<4yw~(*m9SCLC6F9&{?er^6oD*h`(NEcpg0Rhkv0u4JdT zYWA_mXdY8oCMHDMYUA@cJ6&k-@~m5E8ZP3b82df_bl|Q=igPLRkUdN;zHV~Yx#dWO z^-xH>f6JiILby+0AlS=-^OQ+}b$trDd*LKE=i@jf1U*+w)NhdFb5_dH;Ff# z^eVv#dH)XX!(%?wX+A{AHl`P3aaY1Z^E!9<9^x5KW6Nhfq8hd+LvOk*(RbIqQf@D# zPIC{f^S(IHSW>T)?uyKDt9*<-43fjY!H+5?&~}R1~zxIG9b*--sAWV!LKYqZ8QKZN(^!Qi&|n|!%vL}a*s9%EYs`(vus-t z72=#x`PLNrsd2guWq#Tn)}ihwr!zc{3=7%Wa(b_H`weh7qvvEW!1gqDyUk&T~c1xyk|i zEcM8Ro~(W)0nl{X&OyA_1;dUTghw$MHkuS&x56V)3vD9r6xkac;`~(=xM*h8QW;)l z6n8!5+PqDAm0Zk6RE(7lrwxinr$?{GG~CLl`OFg0v=r*t+F)e06do(62siVU10RP{ z!x`WR!Jbb%c7|MM@+IfG;vfds1aT9r=obPO#ULhMI|B0|>e?H+yzYViRP3#x4`!qn z!cqbYbNF+_;0L4PUi32up<~y~2y9hG)9pU(;v1d@B2;`^ZLDf99mGDM9|=9-y~>3d zqrtsb*3&q$A5)I$gocZo~`@UwD(KZt$H0Fd{f6e{kP!-{X*U1f@)q= zgJVHfi5 zL?-P;pFCa-fuATyLiBsW!QrsYGDX&zmt29YRjlt_h4~6Kr^G!^>pwyvrsNb3+@+J#Np*LG{fAs~^L-+}rAc8U_ggc$zB)i$~5 zRNJ+IvyV~0+9fIXGofn+V@CS#6`>Ayh+*Nk>Mi3uv%+3+(RGB+ICNa5>O?NY$_=dx zO|unF+bcKbjyFl-a?NKkFYBhx%BN4*La5~JR2_T`U%q8Pi;obqs|c#P7O&BOjz}cwYG3KgFv>6I3^aBGXzo@uN&ug7QCVBPI+UUp5jPXvK z*Bj2#UYORt%RUpTxSsB7qHG&|3Cmtu-z2sXyQzJr)QbMGMpP;Iiz2ytiFC-=92_ zqg^ZBrQI@sWzU1YBh3)r?wq)$O`q@B#;KI!xsBK6#uQ1W9s5lkzZl-6(z{O9CnpMR zNO?f1MehKvNGvVT&b19@wCy)4&XYu#CF?5a>dmFV?MrN?tyPb&;q^Nm%<4B?t!VS-SYfGyy$gzB+{L=QCx64J? zXX5RQmw1{zhec`xQ&dAw2lILov&Th?d)4n#ZoN`kVongEs%mN4jzHER-XThB?(bgm zx=7%+oQny&6le(DV5(HwC`At%G2%azyh^H0#PVsc&dJe3r%sLp6|Ls1uHH!iF*f1J zPq&G8#ZMl+GK{9Ij8D^8YAobBT~j1@ui*2UjDa)L_sfYylhp*GNw=lTc1-S<(nHm# zjdS7t6CF=2p{F8uco{rDAKtDK$?8gc+$VOWZ+rf}Gtx6=MEGE{+IMB6+D;F^QDzA5 zO46NjQDi0LC4r+qJUYP>YFw-9_*3FbZo?A#Cca5!B$~9)5eLk_Y!tpxju;Hse?fxk z@OxR0N!Q8EgKe9)$0TN$!Od!N-~;gh^7uOuAoX4;X({3A#A2lplg_DK0sDXGHW&vD zt$5tgCmr{Uv;wBj;PU#x=x+Os=|4dWRIl{`xF-LzQt)$yun$}ZZI~&4=|A(}IWR)P zoH=B3l-OGD%J4z%{t`*sU7`=9;`v~T148)?xFN|yxPI~=Fs-Y}ek3XE)z({D)1hd@ zQ>N;^^Jm0A&21ENw5W>za$7KE3J1y7n_>MbdfzV)&g^}imW0dKMY)sY?0*-tKcsD! z2Z4+5IfN=0HP4-aA~0ifDA0k6Jmyq>U=|^>p{UtVue3qDE2$ncefDl%{{_t-MfTEU zk$nVNWIs&ICyC-2sZsky@mIf#;#Eo7`jRY#oInnazwi(h2*n>&!72OrIzcU7=lO?V z^4_9zN+6TSLh?dO?53XH3^sU+Opo{q96p0cyJYorsO>MCxQGat?k!uP+-FHoV_(iSLGI zlf1_xQ84^Zry@m!^TwzCjLU!dNEHa@3guUX1Xa?F#JPT*F3Al&dI3}=2#3Z+{m>}1 zjA%r-`_E6Je+Oo)p1^s>h5q6)R>9^s3Lk2yIqx_72-$2(^6F_R%4QdMUuSfZ4U3L z{oWKy_ND{m)$|w%L(2&M`^)*=E00m+MzyDzl%Pl-4=$SVOa8MJBauR3J)qSizGKe= zAk}1R^Ehaae^~1*RpA5snb=MV1V*cRn`v5LG{|F1yNC9JDJdLj--4L!$zM9O#=b*8 zWCVu+fZEs1Q`hNxwjqDZLy`RHf6lwg84Q0)12y5kgvwm~i}4};Db5Sej5Jx9k^a-@ z&OQMGC;eB`ESl%see8Ud$46+g z(oPLqOEr#L z$6d)uR$G?QtYjwna*#UjN>O{WHVz#6wI=#idlY#rB|ZR^pvd9)5PGzC7rb>dz9#6B zsLdNKQoOOz*f8y@a|XZHIT0lJip@@nUT}##OY2dpU-l9y$L~lW#bPxfCP0q;Pf9XU z{$K3_I6nW2Wyo)=U-5sD_4}8J=I(za(adcKXU;Z}syVy!|GR4L$a<6NL;|1Ldvl|O zy5Aoe2he6;1JDO%K!%OQ=i#6^6yy}hN>;!^wIu2X)JQgzlMcQAav7leZS*7{;9Con zSV8E81|KeEMa3C{!br6(i{2$!#=yViN*3PhElsX+EIYn+8mq;@4L)ZZYbOpy zLpi1#>dm|fpl7V;{UA9QqQ1Z6RnM+xik8ra68odR86&q}DsNAx+=znGb>upd=;WcL zz)?9?bFs_AOYrq4%im5uS;sF6CB~M@1={5PLt=YKdvsfAQj7)O&3suTEAZ>F3Cb7f zr0}2}=EeD!IWAQ&tP(E7R;t1wbTx0lbuDRb)|UDwDGfl>StRLA$`5>v)|(jSMVX;lJ&vM{oohBrmGOJ zg9eq4vzaTD*g1huEi6QuTlWOdVUQ^OCu0$;PYSSOb%PH2Lx}~0u68_xY$Ti+H8c0t{FSuLW~4MLmRqX9#4}R+VmElaE4V$UwyT^mM;0uq zdJ2NSyi~BE&#Lki9j+vC+~35GoRPf$lVIWfEr6{+R5j|-*pJsXMJ7hWs=P#j&6B&^ zA4x>?0&GJH#_Hr2%2fXcTePYCx{OJ{c)PF2=Jjr4_D5CTqFtFL`%riC`(ZGQYGih! zFc=ms_{Uc3q3=jN*rhoXO7^``ktS!QOX~X`N0$uzCBJ;1E88Jbd-lAXyx%F)pJiXW srm9}s18~gurtCk>BLDsWm%qTl%*1Zo)-L9SQjmV<&gf~Dsau8p4Y-*p4gdfE diff --git a/GEMINI.md b/doc/GEMINI.md similarity index 99% rename from GEMINI.md rename to doc/GEMINI.md index 357c5bb..5b11c15 100644 --- a/GEMINI.md +++ b/doc/GEMINI.md @@ -2,7 +2,7 @@ ## Project Overview -**SeedPGP v1.4.5**: Client-side BIP39 mnemonic encryption webapp +**SeedPGP v1.4.7**: Client-side BIP39 mnemonic encryption webapp **Stack**: Bun + Vite + React + TypeScript + OpenPGP.js + Tailwind CSS **Deploy**: Cloudflare Pages (private repo: `seedpgp-web`) **Live URL**: @@ -300,7 +300,7 @@ await window.runSessionCryptoTest() --- -## Current Version: v1.4.5 +## Current Version: v1.4.7 **Recent Changes (v1.4.5):** - Fixed QR Scanner bugs related to camera initialization and race conditions. diff --git a/IMPLEMENTATION_SUMMARY.md b/doc/IMPLEMENTATION_SUMMARY.md similarity index 100% rename from IMPLEMENTATION_SUMMARY.md rename to doc/IMPLEMENTATION_SUMMARY.md diff --git a/doc/LOCAL_TESTING_GUIDE.md b/doc/LOCAL_TESTING_GUIDE.md new file mode 100644 index 0000000..b12aeb5 --- /dev/null +++ b/doc/LOCAL_TESTING_GUIDE.md @@ -0,0 +1,244 @@ +# Local Testing & Offline Build Guide + +## What Changed + +βœ… **Updated vite.config.ts** + +- Changed `base: '/'` β†’ `base: process.env.VITE_BASE_PATH || './'` +- Assets now load with relative paths: `./assets/` instead of `/assets/` +- This fixes Safari's file:// protocol restrictions for offline use + +βœ… **Created Makefile** (Bun-based build system) + +- `make build-offline` - Build with relative paths for Tails/USB +- `make serve-local` - Test locally on +- `make audit` - Security scan for network calls +- `make full-build-offline` - Complete pipeline (build + verify + audit) + +βœ… **Updated TAILS_OFFLINE_PLAYBOOK.md** + +- All references changed from `npm` β†’ `bun` +- Added Makefile integration +- Added local testing instructions +- Added Appendix sections for quick reference + +--- + +## Why file:// Protocol Failed in Safari + +``` +[Error] Not allowed to load local resource: file:///assets/index-DRV-ClkL.js +``` + +**Root cause:** Assets referenced as `/assets/` (absolute paths) don't work with `file://` protocol in browsers for security reasons. + +**Solution:** Use relative paths `./assets/` which: + +- Work with both `file://` on Tails +- Work with `http://` on macOS for testing +- Are included in the vite.config.ts change above + +--- + +## Testing Locally on macOS (Before Tails) + +### Step 1: Build with offline configuration + +```bash +cd seedpgp-web +make install # Install dependencies if not done +make build-offline # Build with relative paths +``` + +### Step 2: Serve locally + +```bash +make serve-local +# Output: πŸš€ Starting local server at http://localhost:8000 +``` + +### Step 3: Test in Safari + +- Open Safari +- Go to: `http://localhost:8000` +- Verify: + - All assets load (no errors in console) + - UI displays correctly + - Functionality works + +### Step 4: Clean up + +```bash +# Stop server: Ctrl+C +# Clean build: make clean +``` + +--- + +## Building for Cloudflare vs Offline + +### For Tails/Offline (use this for your air-gapped workflow) + +```bash +make build-offline +# Builds with: base: './' +# Assets use relative paths +``` + +### For Cloudflare Pages (production deployment) + +```bash +make build +# Builds with: base: '/' +# Assets use absolute paths (correct for web servers) +``` + +--- + +## Verification Commands + +```bash +# Check assets are using relative paths +head -20 dist/index.html | grep "src=\|href=" + +# Should show: src="./assets/..." href="./assets/..." + +# Run full security pipeline +make full-build-offline + +# Just audit for network calls +make audit +``` + +--- + +## USB Transfer Workflow + +Once local testing passes: + +```bash +# 1. Build with offline paths +make build-offline + +# 2. Format USB (replace diskX with your USB) +diskutil secureErase freespace 0 /dev/diskX + +# 3. Create partition +diskutil partitionDisk /dev/diskX 1 MBR FAT32 SEEDPGP 0b + +# 4. Copy all files +cp -R dist/* /Volumes/SEEDPGP/ + +# 5. Eject +diskutil eject /Volumes/SEEDPGP + +# 6. Boot Tails, insert USB, open file:///media/amnesia/SEEDPGP/index.html in Firefox +``` + +--- + +## File Structure After Build + +``` +dist/ +β”œβ”€β”€ index.html (references ./assets/...) +β”œβ”€β”€ assets/ +β”‚ β”œβ”€β”€ index-xxx.js (minified app code) +β”‚ β”œβ”€β”€ index-xxx.css (styles) +β”‚ └── secp256k1-xxx.wasm (crypto library) +└── vite.svg +``` + +All assets have relative paths in index.html βœ… + +--- + +## Why This Works for Offline + +| Scenario | Base Path | Works? | +|----------|-----------|--------| +| `file:///media/amnesia/SEEDPGP/index.html` on Tails | `./` | βœ… Yes | +| `http://localhost:8000` on macOS | `./` | βœ… Yes | +| `https://example.com` on Cloudflare | `./` | βœ… Yes (still works) | +| `file://` with absolute paths `/assets/` | `/` | ❌ No (security blocked) | + +The relative path solution works everywhere! βœ… + +--- + +## Next Steps + +1. **Test locally first** + + ```bash + make build-offline && make serve-local + ``` + +2. **Verify no network calls** + + ```bash + make audit + ``` + +3. **Prepare USB for Tails** + - Follow the USB Transfer Workflow section above + +4. **Boot Tails and test** + - Follow Phase 5-7 in TAILS_OFFLINE_PLAYBOOK.md + +5. **Generate seed phrase** + - All offline, no network exposure βœ… + +--- + +## Troubleshooting + +**"Cannot find module 'bun'"** + +```bash +brew install bun +``` + +**"make: command not found"** + +```bash +# macOS should have make pre-installed +# Verify: which make +``` + +**"Port 8000 already in use"** + +```bash +# The serve script will automatically find another port +# Or kill existing process: lsof -ti:8000 | xargs kill -9 +``` + +**Assets still not loading in Safari** + +```bash +# Clear Safari cache +# Safari β†’ Settings β†’ Privacy β†’ Remove All Website Data +# Then test again +``` + +--- + +## Key Differences from Original Setup + +| Aspect | Before | After | +|--------|--------|-------| +| Package Manager | npm | Bun | +| Base Path | `/` (absolute) | `./` (relative) | +| Build Command | `npm run build` | `make build-offline` | +| Local Testing | Couldn't test locally | `make serve-local` | +| File Protocol Support | ❌ Broken in Safari | βœ… Works everywhere | + +--- + +## Files Modified/Created + +- ✏️ `vite.config.ts` - Changed base path to relative +- ✨ `Makefile` - New build automation (8 commands) +- πŸ“ `TAILS_OFFLINE_PLAYBOOK.md` - Updated for Bun + local testing + +All three files are ready to use now! diff --git a/MEMORY_STRATEGY.md b/doc/MEMORY_STRATEGY.md similarity index 100% rename from MEMORY_STRATEGY.md rename to doc/MEMORY_STRATEGY.md diff --git a/RECOVERY_PLAYBOOK.md b/doc/RECOVERY_PLAYBOOK.md similarity index 99% rename from RECOVERY_PLAYBOOK.md rename to doc/RECOVERY_PLAYBOOK.md index beac245..54309df 100644 --- a/RECOVERY_PLAYBOOK.md +++ b/doc/RECOVERY_PLAYBOOK.md @@ -1,6 +1,6 @@ ## SeedPGP Recovery Playbook - Offline Recovery Guide -**Generated:** Feb 3, 2026 | **SeedPGP v1.4.4** | **Frame Format:** `SEEDPGP1:0:CRC16:BASE45_PAYLOAD` +**Generated:** Feb 3, 2026 | **SeedPGP v1.4.7** | **Frame Format:** `SEEDPGP1:0:CRC16:BASE45_PAYLOAD` *** @@ -415,7 +415,7 @@ print(f"BIP39 Passphrase used: {'YES' if data['pp'] == 1 else 'NO'}") **Print this playbook on archival paper or metal. Store separately from encrypted backups and private keys.** πŸ”’ **Last Updated:** February 3, 2026 -**SeedPGP Version:** 1.4.4 +**SeedPGP Version:** 1.4.7 **Frame Example CRC:** 58B5 βœ“ **Test Recovery:** [ ] Completed [ ] Not Tested diff --git a/SECURITY_AUDIT_REPORT.md b/doc/SECURITY_AUDIT_REPORT.md similarity index 99% rename from SECURITY_AUDIT_REPORT.md rename to doc/SECURITY_AUDIT_REPORT.md index fe954d3..fd6b5a8 100644 --- a/SECURITY_AUDIT_REPORT.md +++ b/doc/SECURITY_AUDIT_REPORT.md @@ -1,7 +1,7 @@ # SeedPGP Web Application - Comprehensive Forensic Security Audit Report **Audit Date:** February 12, 2026 -**Application:** seedpgp-web v1.4.6 +**Application:** seedpgp-web v1.4.7 **Scope:** Full encryption, key management, and seed handling application **Severity Levels:** CRITICAL | HIGH | MEDIUM | LOW diff --git a/SECURITY_PATCHES.md b/doc/SECURITY_PATCHES.md similarity index 100% rename from SECURITY_PATCHES.md rename to doc/SECURITY_PATCHES.md diff --git a/doc/SERVE.md b/doc/SERVE.md new file mode 100644 index 0000000..0fa8620 --- /dev/null +++ b/doc/SERVE.md @@ -0,0 +1,44 @@ +# Serving the built `dist/` folder + +This project provides two lightweight ways to serve the static `dist/` folder locally (both are offline): + +1) Bun static server (uses `serve.ts`) + +```bash +# From project root +bun run serve +# or +bun ./serve.ts +# Open: http://127.0.0.1:8000 +``` + +1) Python HTTP server (zero-deps, portable) + +```bash +# From the `dist` folder +cd dist +python3 -m http.server 8000 +# Open: http://localhost:8000 +``` + +Convenience via `package.json` scripts: + +```bash +# Run Bun server +bun run serve + +# Run Python server (from project root) +bun run serve:py +``` + +Makefile shortcuts: + +```bash +make serve-bun # runs Bun server +make serve-local # runs Python http.server +``` + +Notes: + +- The Bun server sets correct `Content-Type` for `.wasm` and other assets. +- Always use HTTP (localhost) rather than opening `file://` to avoid CORS/file restrictions. diff --git a/doc/TAILS_OFFLINE_PLAYBOOK.md b/doc/TAILS_OFFLINE_PLAYBOOK.md new file mode 100644 index 0000000..aa3059f --- /dev/null +++ b/doc/TAILS_OFFLINE_PLAYBOOK.md @@ -0,0 +1,618 @@ +# Tails Offline Air-Gapped Workflow Playbook + +## Overview + +This playbook provides step-by-step instructions for using seedpgp-web in a secure, air-gapped environment on Tails, eliminating network exposure entirely. + +--- + +## Phase 1: Prerequisites & Preparation + +### 1.1 Requirements + +- **Machine A (Build Machine)**: macOS with Bun, TypeScript, and Git installed +- **Tails USB**: 8GB+ USB drive with Tails installed (from tails.boum.org) +- **Application USB**: Separate 2GB+ USB drive for seedpgp-web +- **Network**: Initial internet access on Machine A only + +### 1.2 Verify Prerequisites on Machine A (macOS with Bun) + +```bash +# Verify Bun is installed +bun --version # Should be v1.0+ + +# Verify TypeScript tools +which tsc + +# Verify git +git --version + +# Clone repository +cd ~/workspace +git clone seedpgp-web +cd seedpgp-web +``` + +### 1.3 Security Checklist Before Starting + +- [ ] Machine A (macOS) is trusted and malware-free (or at minimum risk) +- [ ] Bun is installed and up-to-date +- [ ] Tails USB is downloaded from official tails.boum.org +- [ ] You have physical access to verify USB connections +- [ ] You understand this is offline-only after transfer to Application USB + +--- + +## Phase 2: Build Application Locally (Machine A) + +### 2.1 Clone and Verify Code + +```bash +cd ~/workspace +git clone https://github.com/seedpgp/seedpgp-web.git +cd seedpgp-web +git log --oneline -5 # Document the commit hash for reference +``` + +### 2.2 Install Dependencies with Bun + +```bash +# Use Bun for faster installation +bun install +``` + +### 2.3 Code Audit (CRITICAL) + +Before building, audit the source for security issues: + +- [ ] Review `src/lib/seedpgp.ts` - main crypto logic +- [ ] Review `src/lib/seedblend.ts` - seed blending algorithm +- [ ] Check `src/lib/bip39.ts` - BIP39 implementation +- [ ] Verify no external API calls in code +- [ ] Run `grep -r "fetch\|axios\|http\|api" src/` to find network calls +- [ ] Confirm all dependencies in `bunfig.toml` and `package.json` are necessary + +```bash +# Perform initial audit with Bun +bun run audit # If audit script exists +grep -r "fetch\|axios\|XMLHttpRequest" src/ +grep -r "localStorage\|sessionStorage" src/ # Check what data persists +``` + +### 2.4 Build Production Bundle Using Makefile + +```bash +# Using Makefile (recommended) +make build-offline + +# Or directly with Bun +bun run build +``` + +This generates: + +- `dist/index.html` - Main HTML file +- `dist/assets/` - Bundled JavaScript, CSS (using relative paths) +- All static assets + +### 2.5 Verify Build Output & Test Locally + +```bash +# List all generated files +find dist -type f + +# Verify no external resource links +grep -r "cloudflare\|googleapis\|cdn\|http:" dist/ || echo "βœ“ No external URLs found" + +# Test locally with Bun's simple HTTP server +bun ./dist/index.html + +# Or serve on port 8000 +bun --serve --port 8000 ./dist # deprecated: Bun does not provide a built-in static server +# Use the Makefile: `make serve-local` (runs a Python http.server) or run directly: +# cd dist && python3 -m http.server 8000 +# Then open http://localhost:8000 in Safari +``` + +**Why not file://?**: Safari and Firefox restrict loading local assets via `file://` protocol for security. Using a local HTTP server bypasses this while keeping everything offline. + +--- + +## Phase 3: Prepare Application USB (Machine A - macOS with Bun) + +### 3.1 Format USB Drive + +```bash +# List USB drives +diskutil list + +# Replace diskX with your Application USB (e.g., disk2) +diskutil secureErase freespace 0 /dev/diskX + +# Create new partition +diskutil partitionDisk /dev/diskX 1 MBR FAT32 SEEDPGP 0b +``` + +### 3.2 Copy Built Files to USB + +```bash +# Mount should happen automatically, verify: +ls /Volumes/SEEDPGP + +# Copy entire dist folder built with make build-offline +cp -R dist/* /Volumes/SEEDPGP/ + +# Verify copy completed +ls /Volumes/SEEDPGP/ +ls /Volumes/SEEDPGP/assets/ + +# (Optional) Generate integrity hash for verification on Tails +sha256sum dist/* > /Volumes/SEEDPGP/INTEGRITY.sha256 +``` + +### 3.3 Verify USB Contents + +```bash +# Ensure index.html exists and is readable +cat /Volumes/SEEDPGP/index.html | head -20 + +# Check file count matches +echo "Source files:" && find dist -type f | wc -l +echo "USB files:" && find /Volumes/SEEDPGP -type f | wc -l + +# Check assets are properly included +find /Volumes/SEEDPGP -type d -name "assets" && echo "βœ… Assets folder present" + +# Verify no external URLs in assets +grep -r "http:" /Volumes/SEEDPGP/ && echo "⚠️ Warning: HTTP URLs found" || echo "βœ… No external URLs" +``` + +### 3.4 Eject USB Safely + +```bash +diskutil eject /Volumes/SEEDPGP +``` + +--- + +## Phase 4: Boot Tails & Prepare Environment + +### 4.1 Boot Tails from Tails USB + +- Power off machine +- Insert Tails USB +- Power on and boot from USB (Cmd+Option during boot on Mac) +- Select "Start Tails" +- **DO NOT connect to network** (decline "Connect to Tor" if prompted) + +### 4.2 Insert Application USB + +- Once Tails is running, insert Application USB +- Tails should auto-mount it to `/media/amnesia//` + +### 4.3 Verify Files Accessible & Start HTTP Server + +```bash +# Open terminal in Tails +ls /media/amnesia/ +# Should see your Application USB mount + +# Navigate to application +cd /media/amnesia/SEEDPGP/ + +# Verify files are present +ls -la +cat index.html | head -5 + +# Start local HTTP server (runs completely offline) +python3 -m http.server 8080 & +# Output: Serving HTTP on 0.0.0.0 port 8080 + +# Verify server is running +curl http://localhost:8080/index.html | head -5 +``` + +**Note:** Python3 is pre-installed on Tails. The http.server runs completely offlineβ€”no internet access required, just localhost. + +--- + +## Phase 5: Run Application on Tails + +### 5.1 Open Application in Browser via Local HTTP Server + +**Why HTTP instead of file://?** + +- HTTP is more reliable than `file://` protocol +- Eliminates browser security restrictions +- Identical to local testing on macOS +- Still completely offline (no network exposure) + +**Steps:** + +1. **In Terminal (where you started the server from Phase 4.3):** + - Verify server is still running: `ps aux | grep http.server` + - Should show: `python3 -m http.server 8080` + - If stopped, restart: `cd /media/amnesia/SEEDPGP && python3 -m http.server 8080 &` + +2. **Open Firefox:** + - Click Firefox icon on desktop + - In address bar, type: `http://localhost:8080` + - Press Enter + +3. **Verify application loaded:** + - Page should load completely + - All UI elements visible + - No errors in browser console (F12 β†’ Console tab) + +### 5.2 Verify Offline Functionality + +- [ ] Page loads completely +- [ ] All UI elements are visible +- [ ] No error messages in browser console (F12) +- [ ] Images/assets display correctly +- [ ] No network requests are visible in Network tab (F12) + +### 5.3 Test Application Features + +**Basic Functionality:** + +``` +- [ ] Can generate new seed phrase +- [ ] Can input existing seed phrase +- [ ] Can encrypt seed phrase +- [ ] Can generate PGP key +- [ ] QR codes generate correctly +``` + +**Entropy Sources (all should work offline):** + +- [ ] Dice entropy input works +- [ ] User mouse/keyboard entropy captures +- [ ] Random.org is NOT accessible (verify UI indicates offline mode) +- [ ] Audio entropy can be recorded + +### 5.4 Generate Your Seed Phrase + +1. Navigate to main application +2. Choose entropy source (Dice, Audio, or Interaction) +3. Follow prompts to generate entropy +4. Review generated 12/24-word seed phrase +5. **Write down on paper** (do NOT screenshot, use only pen & paper) +6. Verify BIP39 validation passes + +--- + +## Phase 6: Secure Storage & Export + +### 6.1 Export Encrypted Backup (Optional) + +If you want to save encrypted backup to USB: + +1. Use application's export feature +2. Encrypt with strong passphrase +3. Save to Application USB +4. **Do NOT save to host machine** + +### 6.2 Generate PGP Key (Optional) + +1. Use seedpgp-web to generate PGP key +2. Export private key (encrypted) +3. Save encrypted to USB if desired +4. **Passphrase should be memorable but not written** + +### 6.3 Verify No Leaks + +In Firefox Developer Tools (F12): + +- **Network tab**: Should show only `localhost:8080` requests (all local) +- **Application/Storage**: Check nothing persistent was written +- **Console**: No fetch/XHR errors to external sites + +**To verify server is local-only:** + +```bash +# In terminal, check network connections +sudo netstat -tulpn | grep 8080 +# Should show: tcp 0 0 127.0.0.1:8080 (LISTEN only on localhost) +# NOT on 0.0.0.0 or external interface +``` + +--- + +## Phase 7: Shutdown & Cleanup + +### 7.1 Stop HTTP Server & Secure Shutdown + +```bash +# Stop the http.server gracefully +killall python3 +# Or find the PID and kill it +ps aux | grep http.server +kill -9 # Replace with actual process ID + +# Verify it stopped +ps aux | grep http.server # Should show nothing + +# Then power off Tails completely +sudo poweroff + +# You can also: +# - Select "Power Off" from Tails menu +# - Or simply close/restart the laptop +``` + +**Important:** Killing the server ensures no background processes remain before shutdown. + +### 7.2 Physical USB Handling + +- [ ] Eject Application USB physically +- [ ] Eject Tails USB physically +- [ ] Store both in secure location +- **Tails memory is volatile** - all session data gone after power-off + +### 7.3 Host Machine Cleanup (Machine A) + +```bash +# Remove build artifacts if desired (optional) +rm -rf dist/ + +# Clear sensitive files from shell history +history -c + +# Optionally wipe Machine A's work directory +rm -rf ~/workspace/seedpgp-web/ +``` + +--- + +## Phase 8: Verification & Best Practices + +### 8.1 Before Production Use - Full Test Run + +**Test on macOS with Bun first:** + +```bash +cd seedpgp-web +bun install +make build-offline # Build with relative paths +make serve-local # Serve on http://localhost:8000 +# Open Safari: http://localhost:8000 +# Verify: all assets load, no console errors, no network requests +``` + +1. Complete Phases 1-7 with test run on Tails +2. Verify seed phrase generation works reliably +3. Test entropy sources work offline +4. Confirm PGP key generation (if using) +5. Verify export/backup functionality + +### 8.2 Security Best Practices + +- [ ] **Air-gap is primary defense**: No network = no exfiltration +- [ ] **Tails is ephemeral**: Always boot fresh, always clean shutdown +- [ ] **Paper backups**: Write seed phrase with pen/paper only +- [ ] **Multiple USBs**: Keep Tails and Application USB separate +- [ ] **Verify hash**: Optional - generate hash of `dist/` folder to verify integrity on future builds + +### 8.3 Future Seed Generation + +Repeat these steps for each new seed phrase: + +1. **Boot Tails** from Tails USB (network disconnected) +2. **Insert Application USB** when Tails is running +3. **Start HTTP server:** + + ```bash + cd /media/amnesia/SEEDPGP + python3 -m http.server 8080 & + ``` + +4. **Open Firefox** β†’ `http://localhost:8080` +5. **Generate seed phrase** (choose entropy source) +6. **Write on paper** (pen & paper only, no screenshots) +7. **Stop server and shutdown:** + + ```bash + killall python3 + sudo poweroff + ``` + +--- + +## Troubleshooting + +### Issue: Application USB Not Mounting on Tails + +**Solution**: + +```bash +# Check if recognized +sudo lsblk + +# Manual mount +sudo mkdir -p /media/usb +sudo mount /dev/sdX1 /media/usb +ls /media/usb +``` + +### Issue: Black Screen / Firefox Won't Start + +**Solution**: + +- Let Tails fully boot (may take 2-3 minutes) +- Try manually starting Firefox from Applications menu +- Check memory requirements (Tails recommends 2GB+ RAM) + +### Issue: Assets Not Loading (Broken Images/Styling) + +**Solution**: + +```bash +# Verify file structure on USB +ls -la /media/amnesia/SEEDPGP/assets/ + +# Check permissions +chmod -R 755 /media/amnesia/SEEDPGP/ +``` + +### Issue: Browser Console Shows Errors + +**Solution**: + +- Check if `index.html` references external URLs +- Verify `vite.config.ts` doesn't have external dependencies +- Review network tab - should show only `localhost:8080` requests + +### Issue: Can't Access + +**Solution**: + +```bash +# Verify http.server is running +ps aux | grep http.server + +# If not running, restart it +cd /media/amnesia/SEEDPGP +python3 -m http.server 8080 & + +# If port 8080 is in use, try another port +python3 -m http.server 8081 & +# Then access http://localhost:8081 +``` + +### Issue: "Connection refused" in Firefox + +**Solution**: + +```bash +# Check if port is listening +sudo netstat -tulpn | grep 8080 + +# If not, the server stopped. Restart it: +cd /media/amnesia/SEEDPGP +python3 -m http.server 8080 & + +# Wait a few seconds and refresh Firefox (Cmd+R or Ctrl+R) +``` + +--- + +## Security Checklist Summary + +Before each use: + +- [ ] Using Tails booted from USB (never host OS) +- [ ] Application USB inserted (separate from Tails USB) +- [ ] Network disconnected or Tor disabled +- [ ] HTTP server started: `python3 -m http.server 8080` from USB +- [ ] Accessing (not file://) +- [ ] Firefox console shows no external requests +- [ ] All entropy sources working offline +- [ ] Seed phrase written on paper only +- [ ] HTTP server stopped before shutdown +- [ ] USB ejected after use +- [ ] Tails powered off completely + +--- + +## Appendix A: Makefile Commands Quick Reference + +All build commands are available via Makefile on Machine A: + +```bash +make help # Show all available commands +make install # Install Bun dependencies +make build-offline # Build with relative paths (for Tails/offline) +make serve-local # Test locally on http://localhost:8000 +make audit # Security audit for network calls +make verify-offline # Verify offline compatibility +make full-build-offline # Complete pipeline: clean β†’ build β†’ verify β†’ audit +make clean # Remove dist/ folder +``` + +**Example workflow:** + +```bash +cd seedpgp-web +make install +make audit # Security check +make full-build-offline # Build and verify +# Copy to USB when ready +``` + +--- + +## Appendix B: Local Testing on macOS Before Tails + +**Why test locally first?** + +- Catch build issues early +- Verify all assets load correctly +- Confirm no network requests +- Validation before USB transfer + +**Steps:** + +```bash +cd seedpgp-web +make build-offline # Build with relative paths +make serve-local # Start local server +# Open Safari: http://localhost:8000 +# Test functionality, then Ctrl+C to stop +``` + +When served locally on , assets load correctly. On Tails via , the same relative paths work seamlessly. + +--- + +## Appendix C: Why HTTP Server Instead of file:// Protocol? + +### The Problem with file:// Protocol + +Opening `file:///path/to/index.html` directly has limitations: + +- **Browser security restrictions** - Some features may be blocked or behave unexpectedly +- **Asset loading issues** - Sporadic failures with relative/absolute paths +- **localStorage limitations** - Storage APIs may not work reliably +- **CORS restrictions** - Even local files face CORS-like restrictions on some browsers +- **Debugging difficulty** - Hard to distinguish app issues from browser security issues + +### Why http.server Solves This + +Python's `http.server` module: + +1. **Mimics production environment** - Behaves like a real web server +2. **Completely offline** - Server runs only on localhost (127.0.0.1:8080) +3. **No internet required** - No connection to external servers +4. **Browser compatible** - Works reliably across Firefox, Safari, Chrome +5. **Identical to macOS testing** - Same mechanism for both platforms +6. **Simple & portable** - Python3 comes pre-installed on Tails + +**Verify the server is local-only:** + +```bash +sudo netstat -tulpn | grep 8080 +# Output should show: 127.0.0.1:8080 LISTEN (localhost only) +# NOT 0.0.0.0:8080 (would indicate public access) +``` + +This ensures your seedpgp-web app runs in a standard HTTP environment without any network exposure. βœ… + +--- + +## Additional Resources + +- **Tails Documentation**: +- **seedpgp-web Security Audit**: See SECURITY_AUDIT_REPORT.md +- **BIP39 Standard**: +- **Air-gap Best Practices**: +- **Bun Documentation**: +- **Python HTTP Server**: + +--- + +## Version History + +- **v2.1** - February 13, 2026 - Updated to use Python http.server instead of file:// for reliability +- **v2.0** - February 13, 2026 - Updated for Bun, Makefile, and offline compatibility +- **v1.0** - February 13, 2026 - Initial playbook creation diff --git a/package.json b/package.json index a2bd05f..0ec9422 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,9 @@ "preview": "vite preview", "typecheck": "tsc --noEmit", "test": "bun test", - "test:integration": "bun test src/integration.test.ts" + "test:integration": "bun test src/integration.test.ts", + "serve": "bun ./serve.ts", + "serve:py": "cd dist && python3 -m http.server 8000" }, "dependencies": { "@types/bip32": "^2.0.4", diff --git a/serve.ts b/serve.ts new file mode 100644 index 0000000..2551696 --- /dev/null +++ b/serve.ts @@ -0,0 +1,57 @@ +// Lightweight static file server using Bun +// Run with: bun ./serve.ts + +import { extname } from 'path' + +const DIST = new URL('./dist/', import.meta.url).pathname + +function contentType(path: string) { + const ext = extname(path).toLowerCase() + switch (ext) { + case '.html': return 'text/html; charset=utf-8' + case '.js': return 'application/javascript; charset=utf-8' + case '.css': return 'text/css; charset=utf-8' + case '.wasm': return 'application/wasm' + case '.svg': return 'image/svg+xml' + case '.json': return 'application/json' + case '.png': return 'image/png' + case '.jpg': case '.jpeg': return 'image/jpeg' + case '.txt': return 'text/plain; charset=utf-8' + default: return 'application/octet-stream' + } +} + +Bun.serve({ + hostname: '127.0.0.1', + port: 8000, + fetch(request) { + try { + const url = new URL(request.url) + let pathname = decodeURIComponent(url.pathname) + if (pathname === '/' || pathname === '') pathname = '/index.html' + + // prevent path traversal + const safePath = new URL('.' + pathname, 'file:' + DIST).pathname + + // Ensure file is inside dist + if (!safePath.startsWith(DIST)) { + return new Response('Not Found', { status: 404 }) + } + + try { + const file = Bun.file(safePath) + const headers = new Headers() + headers.set('Content-Type', contentType(safePath)) + // Localhost only; still set a permissive origin for local dev + headers.set('Access-Control-Allow-Origin', 'http://localhost') + return new Response(file.stream(), { status: 200, headers }) + } catch (e) { + return new Response('Not Found', { status: 404 }) + } + } catch (err) { + return new Response('Internal Server Error', { status: 500 }) + } + } +}) + +console.log('Bun static server running at http://127.0.0.1:8000 serving ./dist') diff --git a/vite.config.ts b/vite.config.ts index 266a627..95cdeb7 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -44,7 +44,7 @@ export default defineConfig({ } } }, - base: '/', // Always use root, since we're Cloudflare Pages only + base: process.env.VITE_BASE_PATH || './', // Use relative paths for offline compatibility publicDir: 'public', // ← Explicitly set (should be default) build: { outDir: 'dist',