haveibeenfiltered 0.1.2 → 0.1.3
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/README.md +17 -8
- package/bin/cli.js +42 -10
- package/lib/datasets.js +24 -0
- package/lib/download.js +8 -0
- package/lib/index.js +12 -3
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ See [haveibeenfiltered.com](https://haveibeenfiltered.com) for more information
|
|
|
16
16
|
| Full hash DB | Fast | Full | Yes | 30+ GB |
|
|
17
17
|
| **haveibeenfiltered** | **~14 microseconds** | **Full** | **Yes** | **1.8 GB** |
|
|
18
18
|
|
|
19
|
-
A ribbon filter compresses 2 billion SHA-1 hashes into 1.8 GB with a ~0.78% false positive rate and zero false negatives.
|
|
19
|
+
A ribbon filter compresses 2 billion SHA-1 hashes into 1.8 GB with a ~0.78% false positive rate and **zero false negatives**. Smaller filtered subsets are available for constrained environments.
|
|
20
20
|
|
|
21
21
|
## Quick Start
|
|
22
22
|
|
|
@@ -75,7 +75,7 @@ const filter = await hbf.load({ autoDownload: true })
|
|
|
75
75
|
|
|
76
76
|
| Option | Type | Default | Description |
|
|
77
77
|
|--------|------|---------|-------------|
|
|
78
|
-
| `dataset` | `string` | `'hibp'` | Dataset name (`hibp`, `rockyou`) |
|
|
78
|
+
| `dataset` | `string` | `'hibp'` | Dataset name (`hibp`, `hibp-min5`, `hibp-min10`, `hibp-min20`, `rockyou`) |
|
|
79
79
|
| `path` | `string` | — | Explicit path to `.bin` file |
|
|
80
80
|
| `autoDownload` | `boolean` | `false` | Download from CDN if file is missing |
|
|
81
81
|
|
|
@@ -192,7 +192,10 @@ npx haveibeenfiltered status
|
|
|
192
192
|
|
|
193
193
|
| Dataset | Passwords | Filter Size | FP Rate | Description |
|
|
194
194
|
|---------|-----------|-------------|---------|-------------|
|
|
195
|
-
| `hibp` | 2,048,908,128 | 1.8 GB | ~0.78% | [Have I Been Pwned](https://haveibeenpwned.com/)
|
|
195
|
+
| `hibp` | 2,048,908,128 | 1.8 GB | ~0.78% | [Have I Been Pwned](https://haveibeenpwned.com/) — all passwords |
|
|
196
|
+
| `hibp-min5` | 812,290,707 | 726 MB | ~0.78% | HIBP — passwords seen in 5+ breaches |
|
|
197
|
+
| `hibp-min10` | 486,611,978 | 435 MB | ~0.78% | HIBP — passwords seen in 10+ breaches |
|
|
198
|
+
| `hibp-min20` | 290,029,936 | 259 MB | ~0.78% | HIBP — passwords seen in 20+ breaches |
|
|
196
199
|
| `rockyou` | 14,344,391 | 12.8 MB | ~0.78% | [RockYou](https://en.wikipedia.org/wiki/RockYou#Data_breach) breach (2009) |
|
|
197
200
|
|
|
198
201
|
### CDN
|
|
@@ -202,16 +205,21 @@ Filter binaries are hosted at `https://files.haveibeenfiltered.com/v0.1/`:
|
|
|
202
205
|
| File | Size | SHA-256 |
|
|
203
206
|
|------|------|---------|
|
|
204
207
|
| [`ribbon-hibp-v1.bin`](https://files.haveibeenfiltered.com/v0.1/ribbon-hibp-v1.bin) | 1.8 GB | `4eeb8608fa8541a51a952ecda91ad2f86e6f7457b0dbe34b88ba8a7ed33750ce` |
|
|
208
|
+
| [`ribbon-hibp-v1-min5.bin`](https://files.haveibeenfiltered.com/v0.1/ribbon-hibp-v1-min5.bin) | 726 MB | `4422f5659cb5fe39cf284b844328bfd3f7ab37fac0fe649b4cff216ffd2ac5da` |
|
|
209
|
+
| [`ribbon-hibp-v1-min10.bin`](https://files.haveibeenfiltered.com/v0.1/ribbon-hibp-v1-min10.bin) | 435 MB | `8c71d6a3696d27bcf21a30ddcd67f7e290a71210800db86810ffb84a426fe93e` |
|
|
210
|
+
| [`ribbon-hibp-v1-min20.bin`](https://files.haveibeenfiltered.com/v0.1/ribbon-hibp-v1-min20.bin) | 259 MB | `31a2c7942698fce74d95ce54dfb61f383ef1a33dce496b88c672e1ac07c71c43` |
|
|
205
211
|
| [`ribbon-rockyou-v1.bin`](https://files.haveibeenfiltered.com/v0.1/ribbon-rockyou-v1.bin) | 12.8 MB | `777d3c1640e7067bc7fb222488199c3371de5360639561f1f082db6b7c16a447` |
|
|
206
212
|
|
|
207
213
|
The CLI downloads to `~/.haveibeenfiltered/` by default. Integrity is verified via SHA-256 after each download.
|
|
208
214
|
|
|
209
|
-
### False
|
|
215
|
+
### False Positives and False Negatives
|
|
210
216
|
|
|
211
|
-
|
|
217
|
+
A ribbon filter is a probabilistic data structure. It has two possible error types:
|
|
212
218
|
|
|
213
|
-
-
|
|
214
|
-
-
|
|
219
|
+
- **False positive (FP):** A safe password is incorrectly reported as breached. This can happen because the filter stores compressed fingerprints, not exact hashes. The filter uses 7-bit fingerprints, giving a rate of 1/128 (~0.78%).
|
|
220
|
+
- **False negative (FN):** A breached password is missed and reported as safe. **This cannot happen.** If a password is in the dataset, the filter will always detect it.
|
|
221
|
+
|
|
222
|
+
In practice this means: if `check()` returns `false`, the password is **definitely not** in the dataset. If it returns `true`, there is a ~0.78% chance it's a false alarm. For security applications this is the right tradeoff — you never miss a breached password.
|
|
215
223
|
|
|
216
224
|
## How It Works
|
|
217
225
|
|
|
@@ -242,7 +250,7 @@ Benchmarked on a single core. The filter loads into memory once (~1.8 GB RAM for
|
|
|
242
250
|
## Requirements
|
|
243
251
|
|
|
244
252
|
- **Node.js** >= 16.0.0
|
|
245
|
-
- **Disk space** — 1.8 GB for HIBP, 13 MB for RockYou
|
|
253
|
+
- **Disk space** — 1.8 GB for HIBP (full), 726 MB (min5), 435 MB (min10), 259 MB (min20), 13 MB for RockYou
|
|
246
254
|
- **RAM** — same as disk (filter is loaded into memory)
|
|
247
255
|
|
|
248
256
|
## Links
|
|
@@ -251,6 +259,7 @@ Benchmarked on a single core. The filter loads into memory once (~1.8 GB RAM for
|
|
|
251
259
|
- [GitHub](https://github.com/kolobus/haveibeenfiltered) — Source code
|
|
252
260
|
- [npm](https://www.npmjs.com/package/haveibeenfiltered) — Package registry
|
|
253
261
|
- [Have I Been Pwned](https://haveibeenpwned.com/) — Password breach data source
|
|
262
|
+
- [Buy Me a Coffee](https://buymeacoffee.com/kolobus) — Support the project
|
|
254
263
|
|
|
255
264
|
## License
|
|
256
265
|
|
package/bin/cli.js
CHANGED
|
@@ -9,7 +9,7 @@ const { RibbonFilter } = require('../lib/filter');
|
|
|
9
9
|
const { DATASETS } = require('../lib/datasets');
|
|
10
10
|
const { download } = require('../lib/download');
|
|
11
11
|
|
|
12
|
-
const BOOLEAN_FLAGS = new Set(['stdin', 'hash', 'json', 'quiet', 'force']);
|
|
12
|
+
const BOOLEAN_FLAGS = new Set(['stdin', 'hash', 'json', 'quiet', 'force', 'help', 'version']);
|
|
13
13
|
|
|
14
14
|
function parseArgs(argv) {
|
|
15
15
|
const args = Object.create(null);
|
|
@@ -74,15 +74,34 @@ async function cmdDownload(args) {
|
|
|
74
74
|
console.log(' Expected size: ' + (ds.expectedBytes / 1048576).toFixed(1) + ' MB');
|
|
75
75
|
console.log(' SHA-256: ' + ds.sha256);
|
|
76
76
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
77
|
+
const tmpPath = filterPath + '.tmp';
|
|
78
|
+
const onSigint = () => {
|
|
79
|
+
process.stderr.write('\n');
|
|
80
|
+
try { fs.unlinkSync(tmpPath); } catch (_) {}
|
|
81
|
+
process.exit(130);
|
|
82
|
+
};
|
|
83
|
+
process.on('SIGINT', onSigint);
|
|
84
|
+
|
|
85
|
+
let lastReported = -1;
|
|
86
|
+
try {
|
|
87
|
+
await download(ds.url, filterPath, {
|
|
88
|
+
expectedBytes: ds.expectedBytes,
|
|
89
|
+
sha256: ds.sha256,
|
|
90
|
+
onProgress: process.stderr.isTTY ? ({ downloaded, total, percent }) => {
|
|
91
|
+
const mb = (downloaded / 1048576).toFixed(1);
|
|
92
|
+
const totalMb = (total / 1048576).toFixed(1);
|
|
93
|
+
process.stderr.write('\r ' + mb + ' / ' + totalMb + ' MB (' + percent + '%)');
|
|
94
|
+
} : ({ downloaded, total, percent }) => {
|
|
95
|
+
const step = percent - (percent % 10);
|
|
96
|
+
if (step > lastReported) {
|
|
97
|
+
lastReported = step;
|
|
98
|
+
console.error(' ' + (downloaded / 1048576).toFixed(1) + ' / ' + (total / 1048576).toFixed(1) + ' MB (' + percent + '%)');
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
} finally {
|
|
103
|
+
process.removeListener('SIGINT', onSigint);
|
|
104
|
+
}
|
|
86
105
|
console.log((process.stderr.isTTY ? '\n' : '') + 'Done. Integrity verified.');
|
|
87
106
|
}
|
|
88
107
|
|
|
@@ -184,6 +203,8 @@ function usage() {
|
|
|
184
203
|
console.log(' --hash Input is SHA-1 hex, not plaintext (check command)');
|
|
185
204
|
console.log(' --json Output results as JSON (check command)');
|
|
186
205
|
console.log(' --quiet No output, exit code 1 if any found (check command)');
|
|
206
|
+
console.log(' --help Show this help message');
|
|
207
|
+
console.log(' --version Show version number');
|
|
187
208
|
console.log('');
|
|
188
209
|
console.log('Examples:');
|
|
189
210
|
console.log(' npx haveibeenfiltered download --dataset rockyou');
|
|
@@ -198,6 +219,17 @@ function usage() {
|
|
|
198
219
|
|
|
199
220
|
async function main() {
|
|
200
221
|
const args = parseArgs(process.argv.slice(2));
|
|
222
|
+
|
|
223
|
+
if (args.version) {
|
|
224
|
+
const pkg = require('../package.json');
|
|
225
|
+
console.log(pkg.version);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
if (args.help) {
|
|
229
|
+
usage();
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
201
233
|
const command = args._.shift();
|
|
202
234
|
|
|
203
235
|
switch (command) {
|
package/lib/datasets.js
CHANGED
|
@@ -9,6 +9,30 @@ const DATASETS = {
|
|
|
9
9
|
expectedBytes: 1918974105,
|
|
10
10
|
sha256: '4eeb8608fa8541a51a952ecda91ad2f86e6f7457b0dbe34b88ba8a7ed33750ce',
|
|
11
11
|
},
|
|
12
|
+
'hibp-min5': {
|
|
13
|
+
name: 'hibp-min5',
|
|
14
|
+
version: 1,
|
|
15
|
+
filename: 'ribbon-hibp-v1-min5.bin',
|
|
16
|
+
url: 'https://files.haveibeenfiltered.com/v0.1/ribbon-hibp-v1-min5.bin',
|
|
17
|
+
expectedBytes: 760791541,
|
|
18
|
+
sha256: '4422f5659cb5fe39cf284b844328bfd3f7ab37fac0fe649b4cff216ffd2ac5da',
|
|
19
|
+
},
|
|
20
|
+
'hibp-min10': {
|
|
21
|
+
name: 'hibp-min10',
|
|
22
|
+
version: 1,
|
|
23
|
+
filename: 'ribbon-hibp-v1-min10.bin',
|
|
24
|
+
url: 'https://files.haveibeenfiltered.com/v0.1/ribbon-hibp-v1-min10.bin',
|
|
25
|
+
expectedBytes: 455760736,
|
|
26
|
+
sha256: '8c71d6a3696d27bcf21a30ddcd67f7e290a71210800db86810ffb84a426fe93e',
|
|
27
|
+
},
|
|
28
|
+
'hibp-min20': {
|
|
29
|
+
name: 'hibp-min20',
|
|
30
|
+
version: 1,
|
|
31
|
+
filename: 'ribbon-hibp-v1-min20.bin',
|
|
32
|
+
url: 'https://files.haveibeenfiltered.com/v0.1/ribbon-hibp-v1-min20.bin',
|
|
33
|
+
expectedBytes: 271649178,
|
|
34
|
+
sha256: '31a2c7942698fce74d95ce54dfb61f383ef1a33dce496b88c672e1ac07c71c43',
|
|
35
|
+
},
|
|
12
36
|
rockyou: {
|
|
13
37
|
name: 'rockyou',
|
|
14
38
|
version: 1,
|
package/lib/download.js
CHANGED
|
@@ -26,11 +26,17 @@ function download(url, destPath, opts) {
|
|
|
26
26
|
const tmpPath = destPath + '.tmp';
|
|
27
27
|
const file = fs.createWriteStream(tmpPath);
|
|
28
28
|
|
|
29
|
+
let done = false;
|
|
29
30
|
function cleanup(err) {
|
|
31
|
+
if (done) return;
|
|
32
|
+
done = true;
|
|
33
|
+
file.destroy();
|
|
30
34
|
try { fs.unlinkSync(tmpPath); } catch (_) {}
|
|
31
35
|
reject(err);
|
|
32
36
|
}
|
|
33
37
|
|
|
38
|
+
file.on('error', cleanup);
|
|
39
|
+
|
|
34
40
|
https.get(url, (res) => {
|
|
35
41
|
/* Refuse redirects — the CDN URL must resolve directly */
|
|
36
42
|
if (res.statusCode >= 300 && res.statusCode < 400) {
|
|
@@ -109,11 +115,13 @@ function download(url, destPath, opts) {
|
|
|
109
115
|
));
|
|
110
116
|
return;
|
|
111
117
|
}
|
|
118
|
+
done = true;
|
|
112
119
|
fs.renameSync(tmpPath, destPath);
|
|
113
120
|
resolve(destPath);
|
|
114
121
|
});
|
|
115
122
|
stream.on('error', cleanup);
|
|
116
123
|
} else {
|
|
124
|
+
done = true;
|
|
117
125
|
fs.renameSync(tmpPath, destPath);
|
|
118
126
|
resolve(destPath);
|
|
119
127
|
}
|
package/lib/index.js
CHANGED
|
@@ -48,11 +48,20 @@ async function load(opts) {
|
|
|
48
48
|
await download(ds.url, filterPath, {
|
|
49
49
|
expectedBytes: ds.expectedBytes,
|
|
50
50
|
sha256: ds.sha256,
|
|
51
|
-
onProgress: ({ percent }) => {
|
|
51
|
+
onProgress: process.stderr.isTTY ? ({ percent }) => {
|
|
52
52
|
process.stderr.write('\r ' + percent + '%');
|
|
53
|
-
}
|
|
53
|
+
} : (() => {
|
|
54
|
+
let lastReported = -1;
|
|
55
|
+
return ({ percent }) => {
|
|
56
|
+
const step = percent - (percent % 10);
|
|
57
|
+
if (step > lastReported) {
|
|
58
|
+
lastReported = step;
|
|
59
|
+
process.stderr.write(' ' + percent + '%\n');
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
})(),
|
|
54
63
|
});
|
|
55
|
-
process.stderr.write('\n');
|
|
64
|
+
if (process.stderr.isTTY) process.stderr.write('\n');
|
|
56
65
|
}
|
|
57
66
|
|
|
58
67
|
const buffer = fs.readFileSync(filterPath);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "haveibeenfiltered",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Offline password breach checking using ribbon filters. Check passwords against HIBP (2B+ passwords) and other breach datasets locally, with zero API calls.",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -38,6 +38,10 @@
|
|
|
38
38
|
"bugs": {
|
|
39
39
|
"url": "https://github.com/kolobus/haveibeenfiltered/issues"
|
|
40
40
|
},
|
|
41
|
+
"funding": {
|
|
42
|
+
"type": "individual",
|
|
43
|
+
"url": "https://buymeacoffee.com/kolobus"
|
|
44
|
+
},
|
|
41
45
|
"homepage": "https://haveibeenfiltered.com",
|
|
42
46
|
"engines": {
|
|
43
47
|
"node": ">=16.0.0"
|