diceware-pass-gen 1.0.0
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.
- package/LICENSE +21 -0
- package/README.md +96 -0
- package/bin/cli.js +129 -0
- package/lib/index.js +102 -0
- package/lib/wordlist.js +7782 -0
- package/package.json +44 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ricco020
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# diceware-pass-gen
|
|
2
|
+
|
|
3
|
+
Generate strong, memorable **diceware passphrases** from the official [EFF large wordlist](https://www.eff.org/dice) (7776 words), with the resulting **entropy computed and displayed**. CLI **and** library, **zero runtime dependencies**, cryptographically secure.
|
|
4
|
+
|
|
5
|
+
A diceware passphrase is built by picking random words from a fixed list. With the 7776-word EFF list, each word contributes `log2(7776) ≈ 12.92` bits of entropy, so the default 6-word passphrase carries about **77.5 bits** — easy to type and remember, hard to guess.
|
|
6
|
+
|
|
7
|
+
## Why this exists
|
|
8
|
+
|
|
9
|
+
Random character strings are strong but hard to remember; people end up reusing weak passwords. Diceware passphrases are both strong and memorable. This tool makes the entropy explicit so you can see exactly how strong a passphrase is, instead of trusting a vague "strength meter".
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# Run without installing
|
|
15
|
+
npx diceware-pass-gen
|
|
16
|
+
|
|
17
|
+
# Or install globally for the CLI
|
|
18
|
+
npm install -g diceware-pass-gen
|
|
19
|
+
|
|
20
|
+
# Or add as a library
|
|
21
|
+
npm install diceware-pass-gen
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Requires Node.js >= 14.
|
|
25
|
+
|
|
26
|
+
## CLI usage
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
$ diceware-pass-gen
|
|
30
|
+
truffle-hazard-rewire-skincare-payback-mascot
|
|
31
|
+
entropy: 77.55 bits (6 words from 7776-word EFF list)
|
|
32
|
+
|
|
33
|
+
$ diceware-pass-gen --words 5 --separator " " --capitalize
|
|
34
|
+
Anchor Voucher Mural Reissue Linger
|
|
35
|
+
entropy: 64.62 bits (5 words from 7776-word EFF list)
|
|
36
|
+
|
|
37
|
+
$ diceware-pass-gen -w 7 -n -C 3 -q
|
|
38
|
+
unzip-cardboard-shrunk-eclipse-stunt-sappy-zucchini-4
|
|
39
|
+
...
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Options
|
|
43
|
+
|
|
44
|
+
| Option | Alias | Description | Default |
|
|
45
|
+
| --- | --- | --- | --- |
|
|
46
|
+
| `--words <n>` | `-w` | Number of words | `6` |
|
|
47
|
+
| `--separator <str>` | `-s` | String between words | `-` |
|
|
48
|
+
| `--capitalize` | `-c` | Capitalize the first letter of each word | off |
|
|
49
|
+
| `--number` | `-n` | Append a random digit at the end | off |
|
|
50
|
+
| `--count <n>` | `-C` | How many passphrases to generate | `1` |
|
|
51
|
+
| `--quiet` | `-q` | Print only the passphrase(s) | off |
|
|
52
|
+
| `--help` | `-h` | Show help | |
|
|
53
|
+
| `--version` | `-v` | Show version | |
|
|
54
|
+
|
|
55
|
+
## Library usage
|
|
56
|
+
|
|
57
|
+
```js
|
|
58
|
+
const { generate, entropyBits } = require('diceware-pass-gen');
|
|
59
|
+
|
|
60
|
+
const result = generate({ words: 6, separator: '-', capitalize: false, number: false });
|
|
61
|
+
console.log(result.passphrase); // e.g. "truffle-hazard-rewire-skincare-payback-mascot"
|
|
62
|
+
console.log(result.entropyBits); // 77.55
|
|
63
|
+
console.log(result.words); // ["truffle", "hazard", ...]
|
|
64
|
+
|
|
65
|
+
// Compute entropy for an arbitrary word count
|
|
66
|
+
entropyBits(8); // 103.4 (8 * log2(7776))
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## How it works
|
|
70
|
+
|
|
71
|
+
- **Wordlist**: the official EFF large wordlist (7776 words), chosen for memorable, distinct, typo-resistant words.
|
|
72
|
+
- **Randomness**: words are selected with Node's `crypto.randomBytes`, using **rejection sampling** to eliminate modulo bias, so every word is equally likely. There is no `Math.random()` anywhere in the generation path.
|
|
73
|
+
- **Entropy**: reported as `wordCount × log2(7776)` bits (plus `log2(10)` for the optional trailing digit). This assumes uniform, independent word selection — which is exactly what this tool does.
|
|
74
|
+
|
|
75
|
+
## Choosing a length
|
|
76
|
+
|
|
77
|
+
| Words | Entropy | Rough guidance |
|
|
78
|
+
| --- | --- | --- |
|
|
79
|
+
| 4 | ~51.7 bits | low — only for low-value accounts |
|
|
80
|
+
| 5 | ~64.6 bits | reasonable for everyday accounts |
|
|
81
|
+
| 6 | ~77.5 bits | strong, good default |
|
|
82
|
+
| 7+ | ~90.5+ bits | high-value accounts, master passwords |
|
|
83
|
+
|
|
84
|
+
## Storing your passphrases
|
|
85
|
+
|
|
86
|
+
A strong passphrase is only useful if you don't reuse it and store it safely. For a hands-on **password generator**, an explanation of how password strength is measured, and guidance on using a password manager, see PwdFortress:
|
|
87
|
+
|
|
88
|
+
**[PwdFortress — Password Generator & strength guidance](https://www.pwdfortress.com/en/tools/password-generator)**
|
|
89
|
+
|
|
90
|
+
## Credits
|
|
91
|
+
|
|
92
|
+
The EFF large wordlist is created by the [Electronic Frontier Foundation](https://www.eff.org/dice) and released under [CC BY 3.0](https://creativecommons.org/licenses/by/3.0/). This project bundles that wordlist unmodified.
|
|
93
|
+
|
|
94
|
+
## License
|
|
95
|
+
|
|
96
|
+
[MIT](./LICENSE)
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const { generate, WORDLIST_SIZE } = require('../lib/index');
|
|
5
|
+
const pkg = require('../package.json');
|
|
6
|
+
|
|
7
|
+
function printHelp() {
|
|
8
|
+
console.log(`diceware-pass-gen v${pkg.version}
|
|
9
|
+
Generate strong, memorable passphrases from the EFF large wordlist (${WORDLIST_SIZE} words).
|
|
10
|
+
|
|
11
|
+
USAGE:
|
|
12
|
+
diceware-pass-gen [options]
|
|
13
|
+
|
|
14
|
+
OPTIONS:
|
|
15
|
+
-w, --words <n> Number of words (default: 6)
|
|
16
|
+
-s, --separator <str> Separator between words (default: "-")
|
|
17
|
+
-c, --capitalize Capitalize the first letter of each word
|
|
18
|
+
-n, --number Append a random digit at the end
|
|
19
|
+
-C, --count <n> Number of passphrases to generate (default: 1)
|
|
20
|
+
-q, --quiet Print only the passphrase(s), no entropy line
|
|
21
|
+
-h, --help Show this help
|
|
22
|
+
-v, --version Show version
|
|
23
|
+
|
|
24
|
+
EXAMPLES:
|
|
25
|
+
diceware-pass-gen
|
|
26
|
+
diceware-pass-gen -w 5 -s " " --capitalize
|
|
27
|
+
diceware-pass-gen --words 7 --number --count 3 --quiet
|
|
28
|
+
|
|
29
|
+
Entropy is computed as words x log2(${WORDLIST_SIZE}) ~ 12.9 bits per word.
|
|
30
|
+
Randomness uses Node's crypto.randomBytes with rejection sampling (no modulo bias).
|
|
31
|
+
|
|
32
|
+
Learn how to create and store strong passwords:
|
|
33
|
+
https://www.pwdfortress.com/en/tools/password-generator
|
|
34
|
+
`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function parseArgs(argv) {
|
|
38
|
+
const opts = {
|
|
39
|
+
words: 6,
|
|
40
|
+
separator: '-',
|
|
41
|
+
capitalize: false,
|
|
42
|
+
number: false,
|
|
43
|
+
count: 1,
|
|
44
|
+
quiet: false,
|
|
45
|
+
help: false,
|
|
46
|
+
version: false,
|
|
47
|
+
};
|
|
48
|
+
for (let i = 0; i < argv.length; i++) {
|
|
49
|
+
const a = argv[i];
|
|
50
|
+
switch (a) {
|
|
51
|
+
case '-w':
|
|
52
|
+
case '--words':
|
|
53
|
+
opts.words = parseInt(argv[++i], 10);
|
|
54
|
+
break;
|
|
55
|
+
case '-s':
|
|
56
|
+
case '--separator':
|
|
57
|
+
opts.separator = argv[++i];
|
|
58
|
+
break;
|
|
59
|
+
case '-c':
|
|
60
|
+
case '--capitalize':
|
|
61
|
+
opts.capitalize = true;
|
|
62
|
+
break;
|
|
63
|
+
case '-n':
|
|
64
|
+
case '--number':
|
|
65
|
+
opts.number = true;
|
|
66
|
+
break;
|
|
67
|
+
case '-C':
|
|
68
|
+
case '--count':
|
|
69
|
+
opts.count = parseInt(argv[++i], 10);
|
|
70
|
+
break;
|
|
71
|
+
case '-q':
|
|
72
|
+
case '--quiet':
|
|
73
|
+
opts.quiet = true;
|
|
74
|
+
break;
|
|
75
|
+
case '-h':
|
|
76
|
+
case '--help':
|
|
77
|
+
opts.help = true;
|
|
78
|
+
break;
|
|
79
|
+
case '-v':
|
|
80
|
+
case '--version':
|
|
81
|
+
opts.version = true;
|
|
82
|
+
break;
|
|
83
|
+
default:
|
|
84
|
+
console.error(`Unknown option: ${a}`);
|
|
85
|
+
console.error('Run "diceware-pass-gen --help" for usage.');
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return opts;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function main() {
|
|
93
|
+
const opts = parseArgs(process.argv.slice(2));
|
|
94
|
+
|
|
95
|
+
if (opts.help) {
|
|
96
|
+
printHelp();
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (opts.version) {
|
|
100
|
+
console.log(pkg.version);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (!Number.isInteger(opts.words) || opts.words < 1) {
|
|
105
|
+
console.error('Error: --words must be a positive integer.');
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
if (!Number.isInteger(opts.count) || opts.count < 1) {
|
|
109
|
+
console.error('Error: --count must be a positive integer.');
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
for (let i = 0; i < opts.count; i++) {
|
|
114
|
+
const result = generate({
|
|
115
|
+
words: opts.words,
|
|
116
|
+
separator: opts.separator,
|
|
117
|
+
capitalize: opts.capitalize,
|
|
118
|
+
number: opts.number,
|
|
119
|
+
});
|
|
120
|
+
if (opts.quiet) {
|
|
121
|
+
console.log(result.passphrase);
|
|
122
|
+
} else {
|
|
123
|
+
console.log(result.passphrase);
|
|
124
|
+
console.log(` entropy: ${result.entropyBits} bits (${result.words.length} words from ${result.wordlistSize}-word EFF list)`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
main();
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const WORDLIST = require('./wordlist');
|
|
5
|
+
|
|
6
|
+
const WORDLIST_SIZE = WORDLIST.length; // 7776 (EFF large wordlist)
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Return a cryptographically secure random integer in [0, max)
|
|
10
|
+
* using rejection sampling to avoid modulo bias.
|
|
11
|
+
* @param {number} max - exclusive upper bound (must be > 0)
|
|
12
|
+
* @returns {number}
|
|
13
|
+
*/
|
|
14
|
+
function secureRandomInt(max) {
|
|
15
|
+
if (!Number.isInteger(max) || max <= 0) {
|
|
16
|
+
throw new RangeError('max must be a positive integer');
|
|
17
|
+
}
|
|
18
|
+
// Smallest number of bytes that can represent (max - 1).
|
|
19
|
+
const bytesNeeded = Math.ceil(Math.log2(max) / 8) || 1;
|
|
20
|
+
const range = 2 ** (8 * bytesNeeded);
|
|
21
|
+
// Largest multiple of `max` that fits in `range`; values at or above are rejected.
|
|
22
|
+
const limit = range - (range % max);
|
|
23
|
+
let value;
|
|
24
|
+
do {
|
|
25
|
+
value = 0;
|
|
26
|
+
const buf = crypto.randomBytes(bytesNeeded);
|
|
27
|
+
for (let i = 0; i < bytesNeeded; i++) {
|
|
28
|
+
value = value * 256 + buf[i];
|
|
29
|
+
}
|
|
30
|
+
} while (value >= limit);
|
|
31
|
+
return value % max;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Entropy in bits for a passphrase drawn uniformly from a wordlist.
|
|
36
|
+
* Each word contributes log2(wordlistSize) bits.
|
|
37
|
+
* @param {number} wordCount
|
|
38
|
+
* @param {number} [wordlistSize=WORDLIST_SIZE]
|
|
39
|
+
* @returns {number} entropy in bits
|
|
40
|
+
*/
|
|
41
|
+
function entropyBits(wordCount, wordlistSize = WORDLIST_SIZE) {
|
|
42
|
+
if (!Number.isInteger(wordCount) || wordCount <= 0) {
|
|
43
|
+
throw new RangeError('wordCount must be a positive integer');
|
|
44
|
+
}
|
|
45
|
+
return wordCount * Math.log2(wordlistSize);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Generate a diceware passphrase from the EFF large wordlist.
|
|
50
|
+
* @param {object} [opts]
|
|
51
|
+
* @param {number} [opts.words=6] - number of words
|
|
52
|
+
* @param {string} [opts.separator='-'] - string between words
|
|
53
|
+
* @param {boolean}[opts.capitalize=false] - capitalize the first letter of each word
|
|
54
|
+
* @param {boolean}[opts.number=false] - append a random digit (0-9) at the end
|
|
55
|
+
* @returns {{ passphrase: string, words: string[], entropyBits: number, wordlistSize: number }}
|
|
56
|
+
*/
|
|
57
|
+
function generate(opts = {}) {
|
|
58
|
+
const {
|
|
59
|
+
words = 6,
|
|
60
|
+
separator = '-',
|
|
61
|
+
capitalize = false,
|
|
62
|
+
number = false,
|
|
63
|
+
} = opts;
|
|
64
|
+
|
|
65
|
+
if (!Number.isInteger(words) || words < 1) {
|
|
66
|
+
throw new RangeError('words must be a positive integer');
|
|
67
|
+
}
|
|
68
|
+
if (typeof separator !== 'string') {
|
|
69
|
+
throw new TypeError('separator must be a string');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const picked = [];
|
|
73
|
+
for (let i = 0; i < words; i++) {
|
|
74
|
+
let w = WORDLIST[secureRandomInt(WORDLIST_SIZE)];
|
|
75
|
+
if (capitalize) w = w.charAt(0).toUpperCase() + w.slice(1);
|
|
76
|
+
picked.push(w);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
let passphrase = picked.join(separator);
|
|
80
|
+
let bits = entropyBits(words);
|
|
81
|
+
|
|
82
|
+
if (number) {
|
|
83
|
+
const digit = secureRandomInt(10);
|
|
84
|
+
passphrase += separator + digit;
|
|
85
|
+
bits += Math.log2(10); // one extra uniform digit
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
passphrase,
|
|
90
|
+
words: picked,
|
|
91
|
+
entropyBits: Math.round(bits * 100) / 100,
|
|
92
|
+
wordlistSize: WORDLIST_SIZE,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
module.exports = {
|
|
97
|
+
generate,
|
|
98
|
+
entropyBits,
|
|
99
|
+
secureRandomInt,
|
|
100
|
+
WORDLIST,
|
|
101
|
+
WORDLIST_SIZE,
|
|
102
|
+
};
|