1cb20a22a633
feat: add EFF passphrase generator and 256-character random passwords (v5.1.1)
- time
- 2026-09-05 12:45 UTC · 1 month ago
- machine
- koh2
- agent
- gpt-6
- session
- Vault V4 password and passphrase support
- parent
- 62e1ce2b9167
↓ peach-browser-1cb20a22.tar.gzatlas at this save
files changed8 files
manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/shared/eff-short-wordlist.test.ts+12 −0src/shared/eff-short-wordlist.txt+1296 −0src/shared/generate.empty-pool.test.ts+8 −8src/shared/generate.test.ts+21 −1src/shared/generate.ts+33 −0manifest.firefox.json+1 −1
@@ -1,7 +1,7 @@11 {22 "manifest_version": 3,33 "name": "Peach Password Manager",4- "version": "5.1.0",4+ "version": "5.1.1",55 "description": "Zero-knowledge password manager",66 "browser_specific_settings": {77 "gecko": {manifest.json+1 −1
@@ -2,7 +2,7 @@22 "manifest_version": 3,33 "minimum_chrome_version": "116",44 "name": "Peach Password Manager",5- "version": "5.1.0",5+ "version": "5.1.1",66 "description": "Zero-knowledge password manager",77 "action": {88 "default_popup": "src/popup/index.html",package.json+1 −1
@@ -1,6 +1,6 @@11 {22 "name": "peach",3- "version": "5.1.0",3+ "version": "5.1.1",44 "description": "Zero-knowledge password manager",55 "license": "GPL-3.0-only",66 "type": "module",src/shared/eff-short-wordlist.test.ts+12 −0
@@ -0,0 +1,12 @@1+import { createHash } from "node:crypto";2+import { describe, expect, it } from "vitest";3+import wordList from "./eff-short-wordlist.txt?raw";4+5+describe("EFF short wordlist", () => {6+ it("retains the pinned 1,296-word resource", () => {7+ expect(wordList.trim().split("\n")).toHaveLength(1296);8+ expect(createHash("sha256").update(wordList).digest("hex")).toBe(9+ "36ecca49e4fa20ca84b176c32f2e9c82f98f446585190e75f9879a95c08247bf",10+ );11+ });12+});src/shared/eff-short-wordlist.txt+1296 −0
@@ -0,0 +1,1296 @@1+acid2+acorn3+acre4+acts5+afar6+affix7+aged8+agent9+agile10+aging11+agony12+ahead13+aide14+aids15+aim16+ajar17+alarm18+alias19+alibi20+alien21+alike22+alive23+aloe24+aloft25+aloha26+alone27+amend28+amino29+ample30+amuse31+angel32+anger33+angle34+ankle35+apple36+april37+apron38+aqua39+area40+arena41+argue42+arise43+armed44+armor45+army46+aroma47+array48+arson49+art50+ashen51+ashes52+atlas53+atom54+attic55+audio56+avert57+avoid58+awake59+award60+awoke61+axis62+bacon63+badge64+bagel65+baggy66+baked67+baker68+balmy69+banjo70+barge71+barn72+bash73+basil74+bask75+batch76+bath77+baton78+bats79+blade80+blank81+blast82+blaze83+bleak84+blend85+bless86+blimp87+blink88+bloat89+blob90+blog91+blot92+blunt93+blurt94+blush95+boast96+boat97+body98+boil99+bok100+bolt101+boned102+boney103+bonus104+bony105+book106+booth107+boots108+boss109+botch110+both111+boxer112+breed113+bribe114+brick115+bride116+brim117+bring118+brink119+brisk120+broad121+broil122+broke123+brook124+broom125+brush126+buck127+bud128+buggy129+bulge130+bulk131+bully132+bunch133+bunny134+bunt135+bush136+bust137+busy138+buzz139+cable140+cache141+cadet142+cage143+cake144+calm145+cameo146+canal147+candy148+cane149+canon150+cape151+card152+cargo153+carol154+carry155+carve156+case157+cash158+cause159+cedar160+chain161+chair162+chant163+chaos164+charm165+chase166+cheek167+cheer168+chef169+chess170+chest171+chew172+chief173+chili174+chill175+chip176+chomp177+chop178+chow179+chuck180+chump181+chunk182+churn183+chute184+cider185+cinch186+city187+civic188+civil189+clad190+claim191+clamp192+clap193+clash194+clasp195+class196+claw197+clay198+clean199+clear200+cleat201+cleft202+clerk203+click204+cling205+clink206+clip207+cloak208+clock209+clone210+cloth211+cloud212+clump213+coach214+coast215+coat216+cod217+coil218+coke219+cola220+cold221+colt222+coma223+come224+comic225+comma226+cone227+cope228+copy229+coral230+cork231+cost232+cot233+couch234+cough235+cover236+cozy237+craft238+cramp239+crane240+crank241+crate242+crave243+crawl244+crazy245+creme246+crepe247+crept248+crib249+cried250+crisp251+crook252+crop253+cross254+crowd255+crown256+crumb257+crush258+crust259+cub260+cult261+cupid262+cure263+curl264+curry265+curse266+curve267+curvy268+cushy269+cut270+cycle271+dab272+dad273+daily274+dairy275+daisy276+dance277+dandy278+darn279+dart280+dash281+data282+date283+dawn284+deaf285+deal286+dean287+debit288+debt289+debug290+decaf291+decal292+decay293+deck294+decor295+decoy296+deed297+delay298+denim299+dense300+dent301+depth302+derby303+desk304+dial305+diary306+dice307+dig308+dill309+dime310+dimly311+diner312+dingy313+disco314+dish315+disk316+ditch317+ditzy318+dizzy319+dock320+dodge321+doing322+doll323+dome324+donor325+donut326+dose327+dot328+dove329+down330+dowry331+doze332+drab333+drama334+drank335+draw336+dress337+dried338+drift339+drill340+drive341+drone342+droop343+drove344+drown345+drum346+dry347+duck348+duct349+dude350+dug351+duke352+duo353+dusk354+dust355+duty356+dwarf357+dwell358+eagle359+early360+earth361+easel362+east363+eaten364+eats365+ebay366+ebony367+ebook368+echo369+edge370+eel371+eject372+elbow373+elder374+elf375+elk376+elm377+elope378+elude379+elves380+email381+emit382+empty383+emu384+enter385+entry386+envoy387+equal388+erase389+error390+erupt391+essay392+etch393+evade394+even395+evict396+evil397+evoke398+exact399+exit400+fable401+faced402+fact403+fade404+fall405+false406+fancy407+fang408+fax409+feast410+feed411+femur412+fence413+fend414+ferry415+fetal416+fetch417+fever418+fiber419+fifth420+fifty421+film422+filth423+final424+finch425+fit426+five427+flag428+flaky429+flame430+flap431+flask432+fled433+flick434+fling435+flint436+flip437+flirt438+float439+flock440+flop441+floss442+flyer443+foam444+foe445+fog446+foil447+folic448+folk449+food450+fool451+found452+fox453+foyer454+frail455+frame456+fray457+fresh458+fried459+frill460+frisk461+from462+front463+frost464+froth465+frown466+froze467+fruit468+gag469+gains470+gala471+game472+gap473+gas474+gave475+gear476+gecko477+geek478+gem479+genre480+gift481+gig482+gills483+given484+giver485+glad486+glass487+glide488+gloss489+glove490+glow491+glue492+goal493+going494+golf495+gong496+good497+gooey498+goofy499+gore500+gown501+grab502+grain503+grant504+grape505+graph506+grasp507+grass508+grave509+gravy510+gray511+green512+greet513+grew514+grid515+grief516+grill517+grip518+grit519+groom520+grope521+growl522+grub523+grunt524+guide525+gulf526+gulp527+gummy528+guru529+gush530+gut531+guy532+habit533+half534+halo535+halt536+happy537+harm538+hash539+hasty540+hatch541+hate542+haven543+hazel544+hazy545+heap546+heat547+heave548+hedge549+hefty550+help551+herbs552+hers553+hub554+hug555+hula556+hull557+human558+humid559+hump560+hung561+hunk562+hunt563+hurry564+hurt565+hush566+hut567+ice568+icing569+icon570+icy571+igloo572+image573+ion574+iron575+islam576+issue577+item578+ivory579+ivy580+jab581+jam582+jaws583+jazz584+jeep585+jelly586+jet587+jiffy588+job589+jog590+jolly591+jolt592+jot593+joy594+judge595+juice596+juicy597+july598+jumbo599+jump600+junky601+juror602+jury603+keep604+keg605+kept606+kick607+kilt608+king609+kite610+kitty611+kiwi612+knee613+knelt614+koala615+kung616+ladle617+lady618+lair619+lake620+lance621+land622+lapel623+large624+lash625+lasso626+last627+latch628+late629+lazy630+left631+legal632+lemon633+lend634+lens635+lent636+level637+lever638+lid639+life640+lift641+lilac642+lily643+limb644+limes645+line646+lint647+lion648+lip649+list650+lived651+liver652+lunar653+lunch654+lung655+lurch656+lure657+lurk658+lying659+lyric660+mace661+maker662+malt663+mama664+mango665+manor666+many667+map668+march669+mardi670+marry671+mash672+match673+mate674+math675+moan676+mocha677+moist678+mold679+mom680+moody681+mop682+morse683+most684+motor685+motto686+mount687+mouse688+mousy689+mouth690+move691+movie692+mower693+mud694+mug695+mulch696+mule697+mull698+mumbo699+mummy700+mural701+muse702+music703+musky704+mute705+nacho706+nag707+nail708+name709+nanny710+nap711+navy712+near713+neat714+neon715+nerd716+nest717+net718+next719+niece720+ninth721+nutty722+oak723+oasis724+oat725+ocean726+oil727+old728+olive729+omen730+onion731+only732+ooze733+opal734+open735+opera736+opt737+otter738+ouch739+ounce740+outer741+oval742+oven743+owl744+ozone745+pace746+pagan747+pager748+palm749+panda750+panic751+pants752+panty753+paper754+park755+party756+pasta757+patch758+path759+patio760+payer761+pecan762+penny763+pep764+perch765+perky766+perm767+pest768+petal769+petri770+petty771+photo772+plank773+plant774+plaza775+plead776+plot777+plow778+pluck779+plug780+plus781+poach782+pod783+poem784+poet785+pogo786+point787+poise788+poker789+polar790+polio791+polka792+polo793+pond794+pony795+poppy796+pork797+poser798+pouch799+pound800+pout801+power802+prank803+press804+print805+prior806+prism807+prize808+probe809+prong810+proof811+props812+prude813+prune814+pry815+pug816+pull817+pulp818+pulse819+puma820+punch821+punk822+pupil823+puppy824+purr825+purse826+push827+putt828+quack829+quake830+query831+quiet832+quill833+quilt834+quit835+quota836+quote837+rabid838+race839+rack840+radar841+radio842+raft843+rage844+raid845+rail846+rake847+rally848+ramp849+ranch850+range851+rank852+rant853+rash854+raven855+reach856+react857+ream858+rebel859+recap860+relax861+relay862+relic863+remix864+repay865+repel866+reply867+rerun868+reset869+rhyme870+rice871+rich872+ride873+rigid874+rigor875+rinse876+riot877+ripen878+rise879+risk880+ritzy881+rival882+river883+roast884+robe885+robin886+rock887+rogue888+roman889+romp890+rope891+rover892+royal893+ruby894+rug895+ruin896+rule897+runny898+rush899+rust900+rut901+sadly902+sage903+said904+saint905+salad906+salon907+salsa908+salt909+same910+sandy911+santa912+satin913+sauna914+saved915+savor916+sax917+say918+scale919+scam920+scan921+scare922+scarf923+scary924+scoff925+scold926+scoop927+scoot928+scope929+score930+scorn931+scout932+scowl933+scrap934+scrub935+scuba936+scuff937+sect938+sedan939+self940+send941+sepia942+serve943+set944+seven945+shack946+shade947+shady948+shaft949+shaky950+sham951+shape952+share953+sharp954+shed955+sheep956+sheet957+shelf958+shell959+shine960+shiny961+ship962+shirt963+shock964+shop965+shore966+shout967+shove968+shown969+showy970+shred971+shrug972+shun973+shush974+shut975+shy976+sift977+silk978+silly979+silo980+sip981+siren982+sixth983+size984+skate985+skew986+skid987+skier988+skies989+skip990+skirt991+skit992+sky993+slab994+slack995+slain996+slam997+slang998+slash999+slate1000+slaw1001+sled1002+sleek1003+sleep1004+sleet1005+slept1006+slice1007+slick1008+slimy1009+sling1010+slip1011+slit1012+slob1013+slot1014+slug1015+slum1016+slurp1017+slush1018+small1019+smash1020+smell1021+smile1022+smirk1023+smog1024+snack1025+snap1026+snare1027+snarl1028+sneak1029+sneer1030+sniff1031+snore1032+snort1033+snout1034+snowy1035+snub1036+snuff1037+speak1038+speed1039+spend1040+spent1041+spew1042+spied1043+spill1044+spiny1045+spoil1046+spoke1047+spoof1048+spool1049+spoon1050+sport1051+spot1052+spout1053+spray1054+spree1055+spur1056+squad1057+squat1058+squid1059+stack1060+staff1061+stage1062+stain1063+stall1064+stamp1065+stand1066+stank1067+stark1068+start1069+stash1070+state1071+stays1072+steam1073+steep1074+stem1075+step1076+stew1077+stick1078+sting1079+stir1080+stock1081+stole1082+stomp1083+stony1084+stood1085+stool1086+stoop1087+stop1088+storm1089+stout1090+stove1091+straw1092+stray1093+strut1094+stuck1095+stud1096+stuff1097+stump1098+stung1099+stunt1100+suds1101+sugar1102+sulk1103+surf1104+sushi1105+swab1106+swan1107+swarm1108+sway1109+swear1110+sweat1111+sweep1112+swell1113+swept1114+swim1115+swing1116+swipe1117+swirl1118+swoop1119+swore1120+syrup1121+tacky1122+taco1123+tag1124+take1125+tall1126+talon1127+tamer1128+tank1129+taper1130+taps1131+tarot1132+tart1133+task1134+taste1135+tasty1136+taunt1137+thank1138+thaw1139+theft1140+theme1141+thigh1142+thing1143+think1144+thong1145+thorn1146+those1147+throb1148+thud1149+thumb1150+thump1151+thus1152+tiara1153+tidal1154+tidy1155+tiger1156+tile1157+tilt1158+tint1159+tiny1160+trace1161+track1162+trade1163+train1164+trait1165+trap1166+trash1167+tray1168+treat1169+tree1170+trek1171+trend1172+trial1173+tribe1174+trick1175+trio1176+trout1177+truce1178+truck1179+trump1180+trunk1181+try1182+tug1183+tulip1184+tummy1185+turf1186+tusk1187+tutor1188+tutu1189+tux1190+tweak1191+tweet1192+twice1193+twine1194+twins1195+twirl1196+twist1197+uncle1198+uncut1199+undo1200+unify1201+union1202+unit1203+untie1204+upon1205+upper1206+urban1207+used1208+user1209+usher1210+utter1211+value1212+vapor1213+vegan1214+venue1215+verse1216+vest1217+veto1218+vice1219+video1220+view1221+viral1222+virus1223+visa1224+visor1225+vixen1226+vocal1227+voice1228+void1229+volt1230+voter1231+vowel1232+wad1233+wafer1234+wager1235+wages1236+wagon1237+wake1238+walk1239+wand1240+wasp1241+watch1242+water1243+wavy1244+wheat1245+whiff1246+whole1247+whoop1248+wick1249+widen1250+widow1251+width1252+wife1253+wifi1254+wilt1255+wimp1256+wind1257+wing1258+wink1259+wipe1260+wired1261+wiry1262+wise1263+wish1264+wispy1265+wok1266+wolf1267+womb1268+wool1269+woozy1270+word1271+work1272+worry1273+wound1274+woven1275+wrath1276+wreck1277+wrist1278+xerox1279+yahoo1280+yam1281+yard1282+year1283+yeast1284+yelp1285+yield1286+yo-yo1287+yodel1288+yoga1289+yoyo1290+yummy1291+zebra1292+zero1293+zesty1294+zippy1295+zone1296+zoomsrc/shared/generate.empty-pool.test.ts+8 −8
@@ -1,4 +1,4 @@1-// T-025: generate.ts empty-pool guard and 128-char boundary1+// T-025: generate.ts empty-pool guard and maximum-length boundary22 //33 // Why these tests exist: generatePassword has two edge-case behaviors that are44 // not tested elsewhere:@@ -7,8 +7,8 @@77 // ("abcdefghijklmnopqrstuvwxyz") rather than looping forever or throwing.88 // (line 38: `if (chars.length === 0) chars = filter(...) || "abc..."`)99 //10-// 2. 128-character limit (raised in v1.8.0) — the generator must produce11-// exactly 128 characters when length=128 and must handle the boundary10+// 2. 256-character limit — the generator must produce exactly 25611+// characters and must handle the boundary1212 // without a performance cliff or off-by-one.1313 //1414 // These tests also serve as a regression guard against any future refactor of@@ -61,23 +61,23 @@6161 expect(password).toMatch(/^[a-z]+$/);6262 });6363 64- // T-025c: 128-character password generated without performance cliff.65- // v1.8.0 raised the maximum to 128 chars. The rejection-sampling loop64+ // T-025c: 256-character password generated without performance cliff.65+ // The rejection-sampling loop6666 // must complete in reasonable time (the test has a 2-second implicit timeout).6767 //6868 // Bug this catches: if the rejection-sampling limit calculation overflows6969 // or produces 0 for large char pools, every CSPRNG value is rejected and7070 // the loop runs indefinitely.71- it("T-025c: length=128 produces exactly 128 characters without hanging", () => {71+ it("T-025c: length=256 produces exactly 256 characters without hanging", () => {7272 const password = generatePassword({73- length: 128,73+ length: 256,7474 includeUppercase: true,7575 includeLowercase: true,7676 includeNumbers: true,7777 includeSymbols: true,7878 });7979 80- expect(password.length).toBe(128);80+ expect(password.length).toBe(256);8181 });8282 8383 // T-025d: length=1 (minimum sensible boundary).src/shared/generate.test.ts+21 −1
@@ -1,5 +1,5 @@11 import { describe, expect, it } from "vitest";2-import { generatePassword } from "./generate";2+import { generatePassphrase, generatePassword } from "./generate";33 44 describe("generatePassword", () => {55 it("produces a password of the requested length", () => {@@ -48,6 +48,26 @@4848 const pw = generatePassword({ length: 64 });4949 expect(pw).toHaveLength(64);5050 expect(pw).toMatch(/^[A-Za-z0-9!@#$%^&*]+$/);51+ }52+ });53+});54+55+describe("generatePassphrase", () => {56+ it("defaults to five words separated by hyphens", () => {57+ const passphrase = generatePassphrase();58+59+ expect(passphrase.split("-")).toHaveLength(5);60+ });61+62+ it.each([4, 8, 12])("generates %i words", (wordCount) => {63+ const passphrase = generatePassphrase({ wordCount, separator: " " });64+65+ expect(passphrase.split(" ")).toHaveLength(wordCount);66+ });67+68+ it("supports the separator choices exposed by the UI", () => {69+ for (const separator of ["-", " ", ".", "_"] as const) {70+ expect(generatePassphrase({ wordCount: 4, separator }).split(separator)).toHaveLength(4);5171 }5272 });5373 });src/shared/generate.ts+33 −0
@@ -1,3 +1,5 @@1+import effShortWordList from "./eff-short-wordlist.txt?raw";2+13 export interface GeneratorOptions {24 length: number;35 includeUppercase: boolean;@@ -8,6 +10,19 @@810 forbiddenChars?: string;911 }1012 13+export const MIN_PASSPHRASE_WORDS = 4;14+export const DEFAULT_PASSPHRASE_WORDS = 5;15+export const MAX_PASSPHRASE_WORDS = 12;16+17+export type PassphraseSeparator = "-" | " " | "." | "_";18+19+export interface PassphraseOptions {20+ wordCount: number;21+ separator: PassphraseSeparator;22+}23+24+const PASSPHRASE_WORDS = effShortWordList.trim().split("\n");25+1126 /**1227 * Generate a cryptographically random password.1328 */@@ -50,4 +65,22 @@5065 }5166 }5267 return password;68+}69+70+/** Generate a passphrase from EFF's 1,296-word short list. */71+export function generatePassphrase(options: Partial<PassphraseOptions> = {}): string {72+ const wordCount = Math.max(73+ MIN_PASSPHRASE_WORDS,74+ Math.min(MAX_PASSPHRASE_WORDS, Math.floor(options.wordCount ?? DEFAULT_PASSPHRASE_WORDS)),75+ );76+ const separator = options.separator ?? "-";77+ const limit = Math.floor(0x1_0000_0000 / PASSPHRASE_WORDS.length) * PASSPHRASE_WORDS.length;78+ const words: string[] = [];79+80+ while (words.length < wordCount) {81+ const value = crypto.getRandomValues(new Uint32Array(1))[0]!;82+ if (value < limit) words.push(PASSPHRASE_WORDS[value % PASSPHRASE_WORDS.length]!);83+ }84+85+ return words.join(separator);5386 }