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 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/) full password list |
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 Positive Rate
215
+ ### False Positives and False Negatives
210
216
 
211
- The filter uses 7-bit fingerprints, giving a theoretical false positive rate of 1/128 (~0.78%). This means:
217
+ A ribbon filter is a probabilistic data structure. It has two possible error types:
212
218
 
213
- - Zero false negatives every breached password is detected
214
- - ~0.78% of safe passwords will incorrectly report as breached
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
- await download(ds.url, filterPath, {
78
- expectedBytes: ds.expectedBytes,
79
- sha256: ds.sha256,
80
- onProgress: process.stderr.isTTY ? ({ downloaded, total, percent }) => {
81
- const mb = (downloaded / 1048576).toFixed(1);
82
- const totalMb = (total / 1048576).toFixed(1);
83
- process.stderr.write('\r ' + mb + ' / ' + totalMb + ' MB (' + percent + '%)');
84
- } : null,
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.2",
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"