Skip to content

Commit 772f605

Browse files
committed
chore: consider worst cases
1 parent acf9f89 commit 772f605

File tree

2 files changed

+151
-74
lines changed

2 files changed

+151
-74
lines changed

README.md

Lines changed: 87 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -112,35 +112,46 @@ All commands share the Argon2id cost flags. For release mode we have:
112112
* 🖥️ **16 threads** (e.g. 16-core/64GB RAM desktop machine)
113113
* 🏭 **2048 threads** (e.g. 64×32-core/128GB RAM machines on some cloud provider)
114114

115-
| Bits | 16 threads 🖥️<br>(**systematic search**) | 2048 threads 🏭<br>(**random search**) |
116-
| ---- | ----------------------------------------- | -------------------------------------- |
117-
| 1‑6 | 30 s | 30 s |
118-
| 7 | 1 min 0 s | 30 s |
119-
| 8 | 2 min 0 s | 30 s |
120-
| 9 | 4 min 3 s | 30 s |
121-
| 10 | 8 min 29 s | 31 s |
122-
| 11 | 16 min 21 s | 35 s |
123-
| 12 | 32 min 2 s | 47 s |
124-
| 13 | 1 h 4 m | 1 min 17 s |
125-
| 14 | 2 h 8 m | 2 min 17 s |
126-
| 15 | 4 h 16 m | 4 min 17 s |
127-
| 16 | 8 h 32 m | 8 min 20 s |
128-
| 17 | 17 h 4 m | 16 min 19 s |
129-
| 18 | 1 d 10 h | 32 min 20 s |
130-
| 19 | 2 d 20 h | 1 h 4 m |
131-
| 20 | 5 d 17 h | 2 h 8 m |
132-
| 21 | 11 d 9 h | 4 h 16 m |
133-
| 22 | 22 d 18 h | 8 h 32 m |
134-
| 23 | 45 d 12 h | 17 h 4 m |
135-
| 24 | 91 d 1 h | 1 d 10 h |
136-
| 25 | 182 d 17 h | 2 d 20 h |
137-
| 26 | 364 d 2 h | 5 d 16 h |
138-
| 27 | 1 y 363 d | 11 d 9 h |
139-
| 28 | 3 y 361 d | 22 d 18 h |
140-
| 29 | 7 y 358 d | 45 d 12 h |
141-
| 30 | 15 y 355 d | 91 d |
142-
| 31 | 31 y 336 d | 182 d |
143-
| 32 | 63 y 284 d | 364 d |
115+
| Bits | 16 threads 🖥️<br>(**systematic search**) | 2048 threads 🏭<br>(**random search**) | |
116+
|------|------------------------------------------|-----------------------------------------|-------------------|
117+
| | Worst-case time | Expected time | 99th percentile |
118+
| 1‑6 | 31 s | 30 s | 2 min 19 s |
119+
| 7 | 2 min 4 s | 30 s | 2 min 19 s |
120+
| 8 | 4 min 8 s | 30 s | 2 min 19 s |
121+
| 9 | 8 min 15 s | 30 s | 2 min 19 s |
122+
| 10 | 16 min 30 s | 31 s | 2 min 23 s |
123+
| 11 | 33 min 1 s | 35 s | 2 min 41 s |
124+
| 12 | 1 h 6 min | 47 s | 3 min 36 s |
125+
| 13 | 2 h 12 min | 1 min 17 s | 5 min 55 s |
126+
| 14 | 4 h 24 min | 2 min 17 s | 10 min 31 s |
127+
| 15 | 8 h 48 min | 4 min 17 s | 19 min 44 s |
128+
| 16 | 17 h 36 min | 8 min 20 s | 38 min 24 s |
129+
| 17 | 1 d 11 h | 16 min 19 s | 1 h 15 m |
130+
| 18 | 2 d 22 h | 32 min 20 s | 2 h 29 m |
131+
| 19 | 5 d 21 h | 1 h 4 m | 4 h 55 m |
132+
| 20 | 11 d 18 h | 2 h 8 m | 9 h 52 m |
133+
| 21 | 23 d 11 h | 4 h 16 m | 19 h 44 m |
134+
| 22 | 46 d 23 h | 8 h 32 m | 1 d 15 h |
135+
| 23 | 93 d 22 h | 17 h 4 m | 3 d 6 h |
136+
| 24 | 187 d 19 h | 1 d 10 h | 6 d 13 h |
137+
| 25 | 1 y 10 d | 2 d 20 h | 13 d 2 h |
138+
| 26 | 2 y 21 d | 5 d 16 h | 26 d 1 h |
139+
| 27 | 4 y 41 d | 11 d 9 h | 52 d 4 h |
140+
| 28 | 8 y 83 d | 22 d 18 h | 104 d 8 h |
141+
| 29 | 16 y 165 d | 45 d 12 h | 208 d 16 h |
142+
| 30 | 32 y 331 d | 91 d | 417 d 8 h |
143+
| 31 | 65 y 297 d | 182 d | 2 y 105 d |
144+
| 32 | 131 y 228 d | 364 d | 4 y 212 d |
145+
146+
## Understanding Random Search Variance
147+
148+
Random search follows a geometric distribution with high variance. While the table shows expected times, actual recovery can vary significantly:
149+
150+
* **50% chance** of finding the key in ~0.7× the expected time
151+
* **90% chance** it will take longer than ~2.3× the expected time
152+
* **99% chance** it will take longer than ~4.6× the expected time
153+
154+
For planning purposes, consider the 99th percentile times shown in the table above to understand worst-case scenarios.
144155

145156
**Interpretation**
146157

@@ -156,47 +167,56 @@ Starting benchmark with 1 iterations across 16 threads...
156167

157168
Benchmark results:
158169
Threads: 16
159-
Total time: 29.80s
170+
Total time: 30.95s
160171
Total iterations: 16
161-
Global average time per derivation: 1862.77ms
162-
Global derivations per second: 0.54
163-
Thread average time per derivation: 29.80s
172+
Global average time per derivation: 1934.24ms
173+
Global derivations per second: 0.52
174+
Thread average time per derivation: 30.95s
164175
Thread derivations per second: 0.03
165176

166177
Estimated time to brute-force one preimage/key pair:
167-
bits │ expected time
168-
-----┼-------------
169-
1 │ 30s
170-
2 │ 30s
171-
3 │ 30s
172-
4 │ 30s
173-
5 │ 30s
174-
6 │ 30s
175-
7 │ 60s
176-
8 │ 1min 59s
177-
9 │ 3min 58s
178-
10 │ 7min 57s
179-
11 │ 15min 54s
180-
12 │ 31min 47s
181-
13 │ 1h 4min
182-
14 │ 2h 7min
183-
15 │ 4h 14min
184-
16 │ 8h 29min
185-
17 │ 16h 57min
186-
18 │ 1d 10h
187-
19 │ 2d 20h
188-
20 │ 5d 16h
189-
21 │ 11d 7h
190-
22 │ 22d 15h
191-
23 │ 45d 5h
192-
24 │ 90d 10h
193-
25 │ 180d 21h
194-
26 │ 361d 17h
195-
27 │ 1y 358d
196-
28 │ 3y 351d
197-
29 │ 7y 337d
198-
30 │ 15y 309d
199-
31 │ 31y 252d
200-
32 │ 63y 139d
178+
bits │ systematic (worst) │ random (expected) │ random (99th %ile)
179+
-----┼--------------------┼--------------------┼-------------------
180+
1 │ 31s │ 31s │ 2min 23s
181+
2 │ 31s │ 31s │ 2min 23s
182+
3 │ 31s │ 31s │ 2min 23s
183+
4 │ 31s │ 31s │ 2min 23s
184+
5 │ 31s │ 31s │ 2min 23s
185+
6 │ 1min 2s │ 31s │ 2min 23s
186+
7 │ 2min 4s │ 1min 2s │ 4min 45s
187+
8 │ 4min 8s │ 2min 4s │ 9min 30s
188+
9 │ 8min 15s │ 4min 8s │ 19min 0s
189+
10 │ 16min 30s │ 8min 15s │ 38min 0s
190+
11 │ 33min 1s │ 16min 30s │ 1h 16min
191+
12 │ 1h 6min │ 33min 1s │ 2h 32min
192+
13 │ 2h 12min │ 1h 6min │ 5h 4min
193+
14 │ 4h 24min │ 2h 12min │ 10h 8min
194+
15 │ 8h 48min │ 4h 24min │ 20h 16min
195+
16 │ 17h 36min │ 8h 48min │ 1d 17h
196+
17 │ 1d 11h │ 17h 36min │ 3d 9h
197+
18 │ 2d 22h │ 1d 11h │ 6d 18h
198+
19 │ 5d 21h │ 2d 22h │ 13d 12h
199+
20 │ 11d 18h │ 5d 21h │ 27d 1h
200+
21 │ 23d 11h │ 11d 18h │ 54d 1h
201+
22 │ 46d 23h │ 23d 11h │ 108d 2h
202+
23 │ 93d 22h │ 46d 23h │ 216d 5h
203+
24 │ 187d 19h │ 93d 22h │ 1y 67d
204+
25 │ 1y 10d │ 187d 19h │ 2y 134d
205+
26 │ 2y 21d │ 1y 10d │ 4y 269d
206+
27 │ 4y 41d │ 2y 21d │ 9y 172d
207+
28 │ 8y 83d │ 4y 41d │ 18y 344d
208+
29 │ 16y 165d │ 8y 83d │ 37y 323d
209+
30 │ 32y 331d │ 16y 165d │ 75y 281d
210+
31 │ 65y 297d │ 32y 331d │ 151y 197d
211+
32 │ 131y 228d │ 65y 297d │ 303y 28d
212+
213+
Search strategy explanation:
214+
• Systematic search: Partitions search space among threads (worst-case time shown)
215+
• Random search: Each thread picks candidates randomly (follows geometric distribution)
216+
217+
Random search variance:
218+
• 50th percentile (median): ~0.7× expected time
219+
• 90th percentile: ~2.3× expected time
220+
• 99th percentile: ~4.6× expected time
201221
```
202222
---

wskdf-cli/src/main.rs

Lines changed: 64 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -215,8 +215,31 @@ fn main() -> anyhow::Result<()> {
215215
.build()
216216
.context("failed to build rayon pool")?;
217217
eprintln!("Starting parallel search");
218-
let now = std::time::Instant::now();
218+
219+
// Estimate search completion times
219220
let space = 1u64 << (n_bits - 1); // 2^(n-1)
221+
let expected_trials = space as f64 / (2.0 * threads as f64);
222+
// Rough estimate based on typical KDF performance - this could be calibrated
223+
let est_time_per_trial = 0.1; // seconds - rough placeholder
224+
let expected_time = expected_trials * est_time_per_trial;
225+
226+
eprintln!("\nTime estimates for full search:");
227+
eprintln!(
228+
" 50% chance by: {}",
229+
pretty(expected_time * percentile_multiplier(0.50))
230+
);
231+
eprintln!(
232+
" 90% chance by: {}",
233+
pretty(expected_time * percentile_multiplier(0.90))
234+
);
235+
eprintln!(
236+
" 99% chance by: {}",
237+
pretty(expected_time * percentile_multiplier(0.99))
238+
);
239+
eprintln!(" Expected time: {}", pretty(expected_time));
240+
eprintln!();
241+
242+
let now = std::time::Instant::now();
220243
let start = {
221244
let mut rng = rand::rngs::ThreadRng::default();
222245
rand::Rng::random_range(&mut rng, 0..space)
@@ -342,17 +365,47 @@ fn main() -> anyhow::Result<()> {
342365
eprintln!("Thread derivations per second: {thread_derivations_per_second:.2?}");
343366

344367
eprintln!("\nEstimated time to brute-force one preimage/key pair:");
345-
eprintln!("{:>4} │ {:>12}", "bits", "expected time");
346-
eprintln!("{:->4}-┼-{:->12}", "", "");
368+
eprintln!(
369+
"{:>4} │ {:>18} │ {:>18} │ {:>18}",
370+
"bits", "systematic (worst)", "random (expected)", "random (99th %ile)"
371+
);
372+
eprintln!("{:->4}-┼-{:->18}-┼-{:->18}-┼-{:->18}", "", "", "", "");
347373

348374
for bits in 1u8..=32 {
349375
// space = 2^(bits-1) because MSB is always 1
350376
let space: f64 = 2f64.powi(bits as i32 - 1); // 2^(n-1) candidates
351-
let per_thread_work = (space / (2.0 * threads as f64)).max(1.0); // ≥ 1 round
352-
let exp_secs = per_thread_work * thread_avg_time;
353-
let human = pretty(exp_secs);
354-
eprintln!("{bits:>4} │ {human:>12}");
377+
378+
// Systematic search: divide space among threads, worst case is entire partition
379+
let systematic_work = (space / threads as f64).max(1.0);
380+
let systematic_secs = systematic_work * thread_avg_time;
381+
382+
// Random search: expected trials = space/2, but distributed among threads
383+
let random_expected_work = (space / (2.0 * threads as f64)).max(1.0);
384+
let random_expected_secs = random_expected_work * thread_avg_time;
385+
let random_99th_secs = random_expected_secs * percentile_multiplier(0.99);
386+
387+
let systematic_human = pretty(systematic_secs);
388+
let random_human = pretty(random_expected_secs);
389+
let random_99th_human = pretty(random_99th_secs);
390+
eprintln!("{bits:>4} │ {systematic_human:>18} │ {random_human:>18} │ {random_99th_human:>18}");
355391
}
392+
393+
eprintln!("\nSearch strategy explanation:");
394+
eprintln!("• Systematic search: Partitions search space among threads (worst-case time shown)");
395+
eprintln!("• Random search: Each thread picks candidates randomly (follows geometric distribution)");
396+
eprintln!("\nRandom search variance:");
397+
eprintln!(
398+
"• 50th percentile (median): ~{:.1}× expected time",
399+
percentile_multiplier(0.50)
400+
);
401+
eprintln!(
402+
"• 90th percentile: ~{:.1}× expected time",
403+
percentile_multiplier(0.90)
404+
);
405+
eprintln!(
406+
"• 99th percentile: ~{:.1}× expected time",
407+
percentile_multiplier(0.99)
408+
);
356409
}
357410
Commands::GenerateSalt { output } => {
358411
ensure_file_does_not_exists(&output, "output file already exists")?;
@@ -443,6 +496,10 @@ fn exec_and_send_to_stdin(
443496
Ok(child.wait()?)
444497
}
445498

499+
fn percentile_multiplier(percentile: f64) -> f64 {
500+
-((1.0 - percentile).ln())
501+
}
502+
446503
fn pretty(secs: f64) -> String {
447504
const MIN: f64 = 60.0;
448505
const H: f64 = 60.0 * MIN;

0 commit comments

Comments
 (0)