yapg-py 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
yapg_py-0.4.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Antonio Elena
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
yapg_py-0.4.0/PKG-INFO ADDED
@@ -0,0 +1,481 @@
1
+ Metadata-Version: 2.4
2
+ Name: yapg-py
3
+ Version: 0.4.0
4
+ Summary: Yet another password generator: API keys, passwords, Diceware passphrases (en/es/de), PINs, recovery codes, TOTP, SSH/WireGuard/JWT/htpasswd material. CLI, REST API and an offline web page.
5
+ Author: Antonio Elena
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/aelena/yapg
8
+ Project-URL: Repository, https://github.com/aelena/yapg
9
+ Project-URL: Issues, https://github.com/aelena/yapg/issues
10
+ Project-URL: Changelog, https://github.com/aelena/yapg/blob/main/CHANGELOG.md
11
+ Project-URL: Web page, https://aelena.com/yapg/
12
+ Keywords: password,api-key,passphrase,diceware,recovery-codes,totp,ssh,wireguard,secrets,cli
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Environment :: Console
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Intended Audience :: System Administrators
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Security
20
+ Classifier: Topic :: Utilities
21
+ Requires-Python: >=3.12
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Requires-Dist: typer>=0.12
25
+ Provides-Extra: api
26
+ Requires-Dist: sanic>=23.6; extra == "api"
27
+ Provides-Extra: infra
28
+ Requires-Dist: cryptography>=42; extra == "infra"
29
+ Requires-Dist: bcrypt>=4.0; extra == "infra"
30
+ Provides-Extra: qr
31
+ Requires-Dist: qrcode>=7.4; extra == "qr"
32
+ Provides-Extra: all
33
+ Requires-Dist: sanic>=23.6; extra == "all"
34
+ Requires-Dist: cryptography>=42; extra == "all"
35
+ Requires-Dist: bcrypt>=4.0; extra == "all"
36
+ Requires-Dist: qrcode>=7.4; extra == "all"
37
+ Provides-Extra: dev
38
+ Requires-Dist: pytest>=7.0; extra == "dev"
39
+ Requires-Dist: ruff>=0.4; extra == "dev"
40
+ Requires-Dist: sanic>=23.6; extra == "dev"
41
+ Requires-Dist: sanic-testing>=23.6; extra == "dev"
42
+ Requires-Dist: cryptography>=42; extra == "dev"
43
+ Requires-Dist: bcrypt>=4.0; extra == "dev"
44
+ Requires-Dist: qrcode>=7.4; extra == "dev"
45
+ Dynamic: license-file
46
+
47
+ <p align="center">
48
+ <a href="https://github.com/aelena/yapg/actions/workflows/ci.yml"><img src="https://github.com/aelena/yapg/actions/workflows/ci.yml/badge.svg" alt="CI" /></a>
49
+ <img src="https://img.shields.io/badge/python-%3E%3D3.12-3776AB?logo=python&logoColor=fff" alt="Python >= 3.12" />
50
+ <img src="https://img.shields.io/badge/cli-typer-009688" alt="CLI: Typer" />
51
+ <img src="https://img.shields.io/badge/api-Sanic-ff69b4?logo=python&logoColor=fff" alt="API: Sanic" />
52
+ <img src="https://img.shields.io/badge/web-vanilla%20JS-F7DF1E?logo=javascript&logoColor=000" alt="Web: Vanilla JS" />
53
+ <img src="https://img.shields.io/badge/crypto-secrets%20%2F%20Web%20Crypto-blueviolet" alt="Crypto: secrets / Web Crypto" />
54
+ <img src="https://img.shields.io/badge/license-MIT-green" alt="License: MIT" />
55
+ </p>
56
+
57
+ # yapg
58
+
59
+ **Yet another password generator**, except it is also the key generator, the passphrase generator, the TOTP bootstrapper and the "give me an SSH key / WireGuard pair / JWT secret / htpasswd line" tool you otherwise reach for five different programs to get.
60
+
61
+ One set of cryptographically secure generators, three ways in:
62
+
63
+ | | What | Needs |
64
+ |---|---|---|
65
+ | `keygen` (alias `yapg`) | A CLI you install once and keep | Python 3.12+, `typer` |
66
+ | `keygen serve` | A stateless localhost REST API | the `[api]` extra |
67
+ | [aelena.com/yapg](https://aelena.com/yapg/) or `web/` | A single static page, works offline, nothing leaves the browser | nothing |
68
+
69
+ Every random byte comes from the operating system's CSPRNG: Python's [`secrets`](https://docs.python.org/3/library/secrets.html) on the command line, [`crypto.getRandomValues()`](https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues) in the browser. Nothing is logged, stored or sent anywhere, with one opt-in exception: `keygen check --hibp` sends five hex characters of a SHA-1 to Have I Been Pwned.
70
+
71
+ ---
72
+
73
+ ## Install
74
+
75
+ ```bash
76
+ # everything, as a global tool (recommended)
77
+ uv tool install "yapg-py[all]"
78
+
79
+ # or plain pip, into any environment
80
+ pip install "yapg-py[all]"
81
+
82
+ # straight from git, or from a checkout
83
+ uv tool install "yapg-py[all] @ git+https://github.com/aelena/yapg"
84
+ uv tool install --editable ".[all]"
85
+ ```
86
+
87
+ The distribution is `yapg-py` (plain `yapg` on PyPI belongs to an unrelated project); the import package is `keygen`.
88
+
89
+ Extras, if you want less: `[api]` adds Sanic, `[infra]` adds `cryptography` and `bcrypt` for `ssh`, `wireguard` and `htpasswd`, `[qr]` adds the ASCII QR code for `totp`. The core commands have no dependency beyond Typer.
90
+
91
+ Both `keygen` and `yapg` end up on your PATH and are the same program.
92
+
93
+ ---
94
+
95
+ ## Commands
96
+
97
+ ```
98
+ keygen key Opaque API keys / tokens
99
+ keygen password Random passwords with at least one character from every enabled class
100
+ keygen passphrase Diceware passphrases, English / Spanish / German
101
+ keygen pronounceable Nonsense words you can read out loud
102
+ keygen pin Numeric PINs
103
+ keygen recovery GitHub-style one-time recovery codes
104
+ keygen preset Named presets (stripe, github, db, wifi, vault, ...), yours included
105
+ keygen check Score a password or passphrase you already have
106
+ keygen totp new A TOTP secret with its otpauth:// URI and QR code
107
+ keygen totp code The current code for a TOTP secret
108
+ keygen ssh An ed25519 SSH key pair
109
+ keygen wireguard A WireGuard key pair (+ preshared key)
110
+ keygen jwt-secret An HMAC signing secret
111
+ keygen htpasswd A bcrypt basic-auth line
112
+ keygen selftest Chi-squared uniformity checks over every generator
113
+ keygen serve The REST API
114
+ keygen web The static page on localhost
115
+ keygen version
116
+ ```
117
+
118
+ Every generator takes `-n/--count` (1 to 500), `-o/--out FILE`, `-c/--copy` (clipboard) and `--json`. Values go to stdout one per line so they pipe cleanly; entropy, "copied", "wrote N to file" go to stderr.
119
+
120
+ ### Keys
121
+
122
+ ```bash
123
+ $ keygen key
124
+ Nf3S1x4aqS-6rYbp3yz2_hMVl6o9vPZpdTkZWFR9
125
+
126
+ $ keygen key -f hex -l 64
127
+ 1f6b0c3e9a7d4b2e8c5f0a1d3e6b9c2f4a7d0e3b6c9f2a5d8e1b4c7f0a3d6e9b
128
+
129
+ $ keygen key -f alnum -l 24 -p sk_live_ -n 3 # bodies shortened here
130
+ sk_live_Qm8vT2xLp9Zr4Wn...
131
+ sk_live_7Fg2KjN5tRw8XqL...
132
+ sk_live_Bz4Yn9Wk2Tq7Lx1...
133
+
134
+ $ keygen key -f uuid --json
135
+ {
136
+ "keys": ["3f2a9c1e-7b4d-4e8a-9c2f-1a6b3d5e7f90"],
137
+ "count": 1,
138
+ "format": "uuid"
139
+ }
140
+ ```
141
+
142
+ Formats: `hex`, `base64url` (default), `alnum`, `uuid` (v4). Length counts characters after the prefix. Prefixes are limited to 64 printable, non-whitespace characters.
143
+
144
+ ### Passwords
145
+
146
+ ```bash
147
+ $ keygen password
148
+ k7$Vq2!mZp9@Lw4#Rt8x
149
+
150
+ $ keygen password -l 32 --no-symbols -x -e # no symbols, no I l 1 O 0, show entropy
151
+ Hx7kQm3NpW9vRt2YbJ4cZf8gKd6sLn5a
152
+ entropy: 186.4 bits
153
+
154
+ $ keygen password -n 50 -o passwords.txt
155
+ wrote 50 to passwords.txt
156
+
157
+ $ keygen password --json -l 16
158
+ {
159
+ "passwords": ["p4K$mN9!xQ2@vL7w"],
160
+ "count": 1,
161
+ "length": 16,
162
+ "upper": true, "lower": true, "digits": true, "symbols": true,
163
+ "exclude_ambiguous": false,
164
+ "entropy_bits": 104.9
165
+ }
166
+ ```
167
+
168
+ Toggles: `--no-upper`, `--no-lower`, `--no-digits`, `--no-symbols`, `-x/--exclude-ambiguous`. Every enabled class is guaranteed at least once; the rest of the characters are uniform over the combined alphabet and the whole thing is Fisher-Yates shuffled.
169
+
170
+ ### Passphrases
171
+
172
+ ```bash
173
+ $ keygen passphrase
174
+ surpass-shredding-clanking-saline-direction-unsavory
175
+
176
+ $ keygen passphrase -w 5 -s " " -C -d # five words, spaces, Title Case, one digit
177
+ Banana Salt Rewrap Retail3 Update
178
+
179
+ $ keygen passphrase -L es -w 4
180
+ almohada-tortuga-nevera-bosque
181
+
182
+ $ keygen passphrase -L de -w 4 -e
183
+ klinke-wolke-fahrrad-segeln
184
+ entropy: 51.7 bits
185
+ ```
186
+
187
+ Three bundled 7776-word Diceware lists, 12.9 bits per word: the EFF large list (`en`), dadoware-bonito-es (`es`), dys2p wordlists-de (`de`). Sources and licences are in [src/keygen/data/WORDLISTS.md](src/keygen/data/WORDLISTS.md). `--capitalize` and `--digit` satisfy composition rules; they add negligible entropy.
188
+
189
+ ### Pronounceable
190
+
191
+ ```bash
192
+ $ keygen pronounceable
193
+ trobaklipuston
194
+
195
+ $ keygen pronounceable -s 3 -C -n 3 -e
196
+ Kefrilon
197
+ Bozustai
198
+ Grunopla
199
+ entropy: 34.7 bits
200
+ ```
201
+
202
+ Consonant-vowel-coda syllables, about 11.3 bits each. For the password you have to say over the phone.
203
+
204
+ ### PINs and recovery codes
205
+
206
+ ```bash
207
+ $ keygen pin
208
+ 481926
209
+
210
+ $ keygen pin -l 8 -n 3
211
+ 20471139
212
+ 88305162
213
+ 57712904
214
+
215
+ $ keygen recovery
216
+ # Recovery codes. Keep these somewhere safe; each one works once.
217
+
218
+ t6zut-6egw7
219
+ nudhv-x9b42
220
+ ... (16 by default)
221
+
222
+ $ keygen recovery -n 4 -l 12 -g 3 -e
223
+ h67z-bjmn-nc9c
224
+ 7p2k-xw4r-tf8d
225
+ mq3s-vz9b-yk5n
226
+ c8dw-rn2h-gj6p
227
+ entropy: 59.4 bits
228
+ ```
229
+
230
+ Recovery codes use the alphabet `abcdefghjkmnpqrstuvwxyz23456789`: no `0 O 1 I l`, so they survive being read off paper.
231
+
232
+ ### Presets
233
+
234
+ ```bash
235
+ $ keygen preset
236
+ stripe key format='alnum', length=24, prefix='sk_live_' Stripe-style secret key
237
+ stripe-test key format='alnum', length=24, prefix='sk_test_' Stripe-style test key
238
+ github key format='alnum', length=36, prefix='ghp_' GitHub-style personal access token
239
+ api-hex key format='hex', length=64 256-bit hex token
240
+ api-urlsafe key format='base64url', length=43 256-bit base64url token
241
+ session key format='base64url', length=32 Session cookie value
242
+ db password length=32, symbols=False Database password: 32 alphanumerics, safe in connection strings
243
+ wifi passphrase words=4, separator='-', capitalize=True WPA passphrase a guest can type
244
+ vault passphrase words=7, separator=' ' Master passphrase, about 90 bits
245
+ phone pronounceable syllables=4, capitalize=True Temporary password you can read out loud
246
+ pin pin length=6 Six-digit PIN
247
+ 2fa-backup recovery length=10, groups=2 Recovery code, xxxxx-xxxxx
248
+
249
+ $ keygen preset db
250
+ Hx7kQm3NpW9vRt2YbJ4cZf8gKd6sLn5a
251
+
252
+ $ keygen preset github -n 2
253
+ ghp_Qm8vT2xLp9Zr4WnK1cHbYd3e...
254
+ ghp_Bz4Yn9Wk2Tq7Lx1Mr8Jv5Pd3...
255
+ ```
256
+
257
+ Your own presets live in `~/.config/keygen/presets.toml` (or wherever `$KEYGEN_PRESETS` points) and override built-ins by name:
258
+
259
+ ```toml
260
+ [stripe]
261
+ command = "key"
262
+ format = "alnum"
263
+ length = 24
264
+ prefix = "sk_live_"
265
+
266
+ [vault]
267
+ command = "passphrase"
268
+ words = 7
269
+ lang = "es"
270
+ separator = " "
271
+ ```
272
+
273
+ `command` is any of `key`, `password`, `passphrase`, `pronounceable`, `pin`, `recovery`; the other keys are that command's options.
274
+
275
+ ### Check
276
+
277
+ ```bash
278
+ $ keygen check 'correct-horse-battery-staple'
279
+ length 28
280
+ classes lower, symbols
281
+ entropy 51.7 bits (fair)
282
+ looks like 4 words from the en list: 51.7 bits
283
+
284
+ $ keygen check 'Password1234!!!'
285
+ length 15
286
+ classes upper, lower, digits, symbols
287
+ entropy 53.3 bits (fair)
288
+ warning a character repeated three or more times in a row
289
+ warning keyboard/sequence run '1234'
290
+ warning contains a very common password word
291
+
292
+ $ echo 'qwerty12' | keygen check; echo "exit $?"
293
+ length 8
294
+ classes lower, digits
295
+ entropy 26.4 bits (weak)
296
+ warning shorter than 12 characters
297
+ warning keyboard/sequence run 'qwer'
298
+ exit 1
299
+
300
+ $ keygen check --hibp 'hunter2'
301
+ ...
302
+ hibp seen 17,043 times in breaches
303
+ ```
304
+
305
+ The estimate is `length × log2(pool)`, capped at `words × 12.9` when every token is in one of the bundled wordlists, minus 15 bits per pattern warning. It is a heuristic, not zxcvbn: its job is to keep obvious junk from scoring "fair". Exit code 1 means weak or breached, so it works in scripts. `--hibp` uses the [k-anonymity range API](https://haveibeenpwned.com/API/v3#SearchingPwnedPasswordsByRange): only the first five hex characters of the SHA-1 leave your machine.
306
+
307
+ ### TOTP
308
+
309
+ ```bash
310
+ $ keygen totp new --issuer "My App" --account ana@example.com
311
+ secret JBSWY3DPEHPK3PXPJBSWY3DPEHPK3PXP
312
+ uri otpauth://totp/My%20App%3Aana%40example.com?secret=JBSW...&digits=6&period=30&algorithm=SHA1&issuer=My+App
313
+ current 492039 (17s left)
314
+ █▀▀▀▀▀█ ▄▀ ▀▄█ █▀▀▀▀▀█
315
+ █ ███ █ ▀█▄▀▄ █ ███ █ (scan with any authenticator app)
316
+ ...
317
+
318
+ $ keygen totp code JBSWY3DPEHPK3PXPJBSWY3DPEHPK3PXP
319
+ 492039
320
+ 17s left
321
+
322
+ $ keygen totp new --no-qr --json --digits 8 --algorithm SHA256
323
+ { "secret": "...", "uri": "otpauth://totp/...", "current_code": "48213907", ... }
324
+ ```
325
+
326
+ RFC 6238, verified against the RFC's own test vectors. The QR code needs the `[qr]` extra; without it the URI is still printed. `totp code` is handy for testing a login flow you are building, or as the authenticator of last resort.
327
+
328
+ ### SSH, WireGuard, JWT, htpasswd
329
+
330
+ ```bash
331
+ $ keygen ssh -C ana@laptop -o ~/.ssh/id_ed25519_new
332
+ wrote ~/.ssh/id_ed25519_new and ~/.ssh/id_ed25519_new.pub
333
+ ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIK... ana@laptop
334
+
335
+ $ keygen ssh # no -o: both keys to stdout
336
+ -----BEGIN OPENSSH PRIVATE KEY-----
337
+ ...
338
+ -----END OPENSSH PRIVATE KEY-----
339
+ ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIK...
340
+
341
+ $ keygen wireguard --preshared
342
+ PrivateKey = yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk=
343
+ PublicKey = xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=
344
+ PresharedKey = FpCyhws9cxwWoV4xELtfJvjJN+zQVRPISllRWgeopVE=
345
+
346
+ $ keygen jwt-secret # 256 bits, base64url
347
+ k3rT9xQ2mZ8vLp5wNb7cYf4dHs6gJa1eRu0iXo2tVq
348
+
349
+ $ keygen jwt-secret -b 512 -E hex
350
+ 7f3a... (128 hex chars)
351
+
352
+ $ keygen htpasswd ana # generates the password and prints it to stderr
353
+ password: Hx7kQm3NpW9vRt2YbJ4cZf8g
354
+ ana:$2y$12$Qm8vT2xLp9Zr4WnK1cHbYeB3e7Fg2KjN5tRw8XqLm3ZpVc6HsN7uK
355
+
356
+ $ keygen htpasswd ana --prompt -o .htpasswd # asks twice, appends the line
357
+ $ keygen htpasswd ana -P 's3cret' --rounds 10
358
+ ```
359
+
360
+ `ssh` refuses to overwrite an existing file and writes the private key with mode 0600 on POSIX. `wireguard` prints lines you paste straight into `[Interface]` and `[Peer]`. `htpasswd` emits bcrypt with the `$2y$` prefix Apache documents, which nginx, Caddy and Traefik accept too. These four need the `[infra]` extra and are deliberately not exposed by the REST API.
361
+
362
+ ### Self-test
363
+
364
+ ```bash
365
+ $ keygen selftest
366
+ ok hex chi2 p=0.412 over 25600 symbols
367
+ ok base64url chi2 p=0.187 over 25600 symbols
368
+ ok alnum chi2 p=0.903 over 25600 symbols
369
+ ok recovery chi2 p=0.655 over 12800 symbols
370
+ ok pin chi2 p=0.271 over 12800 symbols
371
+ ok password chi2 p=0.544 over 25600 symbols
372
+ ok password classes one of each class at length 4
373
+ ok wordlist en 7776 words, 7776 unique
374
+ ok wordlist es 7776 words, 7776 unique
375
+ ok wordlist de 7776 words, 7776 unique
376
+ ok passphrase draw chi2 p=0.731 over 16 buckets
377
+ ok uuid v4 version/variant bits, no repeats
378
+ 12/12 passed
379
+ ```
380
+
381
+ A chi-squared goodness-of-fit test per alphabet plus the structural guarantees. It cannot prove randomness; it catches the bugs that matter (a truncated alphabet, modulo bias, a wordlist that lost a line). CI runs it on every push.
382
+
383
+ ---
384
+
385
+ ## REST API
386
+
387
+ ```bash
388
+ keygen serve # http://127.0.0.1:8000
389
+ ```
390
+
391
+ All endpoints are `GET`, return JSON, accept `count`, and accept `download=1` to receive a `text/plain` attachment instead. Bad parameters return `400` with a message.
392
+
393
+ | Endpoint | Parameters |
394
+ |---|---|
395
+ | `/health` | |
396
+ | `/api-key` | `format`, `length`, `prefix` |
397
+ | `/password` | `length`, `upper`, `lower`, `digits`, `symbols`, `exclude_ambiguous` (0/1) |
398
+ | `/passphrase` | `words`, `sep`, `lang` (en/es/de), `capitalize`, `digit` |
399
+ | `/pronounceable` | `syllables`, `capitalize` |
400
+ | `/pin` | `length` |
401
+ | `/recovery-codes` | `length`, `groups` |
402
+
403
+ ```bash
404
+ $ curl "http://localhost:8000/api-key?prefix=sk_live_&length=32"
405
+ {"keys":["sk_live_aBcDeFgHiJkLmNoP..."],"count":1,"format":"base64url"}
406
+
407
+ $ curl "http://localhost:8000/passphrase?words=5&lang=es&sep=%20"
408
+ {"passphrases":["tortuga nevera bosque almohada faro"],"count":1,"words":5,"lang":"es","entropy_bits":64.6}
409
+
410
+ $ curl -O -J "http://localhost:8000/recovery-codes?download=1"
411
+ ```
412
+
413
+ The API binds to localhost, has no authentication and no rate limiting. It is a local convenience, not a service to expose. Key material (SSH, WireGuard, htpasswd, TOTP) is CLI-only on purpose.
414
+
415
+ ---
416
+
417
+ ## Web page
418
+
419
+ [aelena.com/yapg](https://aelena.com/yapg/), or open `web/index.html`, or `keygen web`. Seven tabs (API key, password, passphrase in three languages, pronounceable, PIN, recovery, the built-in presets), copy and download buttons, an entropy meter. No build step, no network access; the wordlists ship inside the page.
420
+
421
+ The page reimplements the generators in `web/app.js` by hand; it does not call the API. Keep it in step with `src/keygen/core.py`.
422
+
423
+ ---
424
+
425
+ ## How it works
426
+
427
+ **Randomness.** `secrets` in Python; `crypto.getRandomValues()` in the browser with rejection sampling in `randInt`, so there is no modulo bias.
428
+
429
+ **Passwords.** One guaranteed character per enabled class, the rest uniform over the combined alphabet, then a Fisher-Yates shuffle with the CSPRNG. Reported entropy is `length × log2(pool)`, an upper bound; the one-of-each rule costs a few bits at short lengths.
430
+
431
+ **Passphrases.** Diceware: 7776 words, 12.925 bits each, six words is 77.5 bits.
432
+
433
+ **Recovery codes.** 31-symbol alphabet, 4.95 bits per symbol, ten symbols is 49.5 bits. Enough for a code that is single-use and rate-limited.
434
+
435
+ **Limits.**
436
+
437
+ | | Min | Max | Default |
438
+ |---|---|---|---|
439
+ | Key length | 8 | 256 | 40 |
440
+ | Password length | 4 | 128 | 20 |
441
+ | Passphrase words | 3 | 20 | 6 |
442
+ | Pronounceable syllables | 2 | 16 | 4 |
443
+ | PIN digits | 4 | 32 | 6 |
444
+ | Recovery code length | 6 | 32 | 10 |
445
+ | Batch (`count`) | 1 | 500 | 1 (16 for recovery) |
446
+ | Prefix | | 64 chars, printable, no whitespace | |
447
+
448
+ ---
449
+
450
+ ## Project layout
451
+
452
+ ```
453
+ yapg/
454
+ ├── src/keygen/
455
+ │ ├── core.py the generators; pure, no I/O
456
+ │ ├── check.py scoring + HIBP lookup
457
+ │ ├── totp.py RFC 6238
458
+ │ ├── infra.py ssh, wireguard, jwt-secret, htpasswd
459
+ │ ├── presets.py built-in + user presets
460
+ │ ├── selftest.py chi-squared checks
461
+ │ ├── cli.py typer app -> keygen / yapg
462
+ │ ├── server.py sanic app -> keygen serve
463
+ │ └── data/ wordlists + licences
464
+ ├── web/ static page, deployed to GitHub Pages
465
+ ├── api/server.py compatibility shim
466
+ ├── tests/ pytest
467
+ └── .github/workflows/ CI (ubuntu + windows, 3.12 + 3.13) and Pages
468
+ ```
469
+
470
+ ## Development
471
+
472
+ ```bash
473
+ uv venv && uv pip install -e ".[dev]" # add --link-mode=copy on OneDrive-synced folders
474
+ pytest -p no:warnings
475
+ ruff check src tests
476
+ keygen selftest
477
+ ```
478
+
479
+ ## Licence
480
+
481
+ [MIT](LICENSE). Wordlists: EFF (CC-BY 3.0), dadoware-bonito-es (GFDL 1.3), dys2p (CC0); see [WORDLISTS.md](src/keygen/data/WORDLISTS.md).