Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,6 @@ TODO.md

# Demo build output
_site/

# Local notes and plans
tmp/
13 changes: 9 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ can't predict without running the whole chain, and each collision mixes in an ea
digest, so the hasher has to be able to recover earlier digests on demand.

▶️ **Live demo:** [karl.preisner.us/chip](https://karl.preisner.us/chip/) runs the real
library in your browser, showing each salt attempt and collision as it happens.
library in your browser, showing each salt attempt and collision as it happens. Below it, an
interactive explainer walks through how CHIP works, step by step.

📄 **Paper:** [CHIP: Confounding Hash Iteration for Passwords](docs/paper.md), covering
how it works, measured costs, and its security limitations.
Expand Down Expand Up @@ -49,7 +50,7 @@ return salt, SHA256(h)
- If a salt needs more than an upper bound of iterations, a new random salt is drawn.

With `b = 32, C = 5`, one chain takes about 200,000 iterations on average. The default upper
bound (250,000) rejects about 15% of salts, so a full hash costs about 235,000 iterations
bound (250,000) rejects about 15% of salts, so a full hash costs about 230,000 iterations
including retries.

## Quick start
Expand Down Expand Up @@ -169,7 +170,8 @@ Run `uv run chip <command> --help` for all options.
| `task bench` | quick benchmark run |
| `task bench-full` | full iteration benchmark; regenerates the `iterations.*` results |
| `task bench-memory` | memory benchmark; regenerates the `memory.*` results |
| `task demo` | build the browser demo and serve it at http://localhost:8000 |
| `task bench-distribution` | chain-length distribution at the defaults; regenerates `distribution.csv` |
| `task site` | build the demo and explainer page and serve it at http://localhost:8000 |

The same checks run in GitHub Actions on every push and pull request.

Expand All @@ -184,10 +186,13 @@ tests/ unit tests (pytest)
benchmarks/
iterations.py measures iterations for every (b, C) pair
memory.py measures memory for a range of (b, C) pairs
distribution.py records every chain's length at the defaults, for the explainer
time_memory_tradeoff.py computes CHIP with about 4× less memory
results/ committed results: CSV, Markdown tables, charts
demo/
index.html the browser demo (GitHub Pages)
index.html the browser demo and explainer (GitHub Pages)
chain.js the CHIP chain in JavaScript, for the explainer's step-through widgets
explainer.js the explainer's interactive widgets
worker.js runs the chip package in Pyodide, off the main thread
build.sh assembles the site from the page, src/chip, and benchmark data
docs/
Expand Down
10 changes: 8 additions & 2 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,14 @@ tasks:
cmds:
- uv run python benchmarks/memory.py

demo:
desc: Build the browser demo and serve it at http://localhost:8000
bench-distribution:
desc: Iterations of 1,000 chains at b 32, C 5 into benchmarks/results/ (for the site's histograms)
cmds:
- uv run python benchmarks/distribution.py

site:
desc: Build the GitHub Pages site (demo and explainer) and serve it at http://localhost:8000
aliases: [demo]
cmds:
- sh demo/build.sh _site
- uv run python -m http.server 8000 --directory _site
51 changes: 51 additions & 0 deletions benchmarks/distribution.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"""Record the iteration count of every chain at one (b, C), for the demo page's histograms.

`iterations.py` keeps only the mean and standard deviation. The demo page's explainer also
needs the shape of the distribution: how many salts a given upper bound rejects, and how
unevenly chains finish when guesses run side by side. Runs unbounded chains with random
inputs and writes one row per chain:

- distribution.csv iterations at each collision (columns c1 ... cC), one row per chain

Usage: uv run python benchmarks/distribution.py [--bits 32] [--collisions 5] [--runs 1000]
"""

import argparse
import csv
import os
import statistics
from concurrent.futures import ProcessPoolExecutor
from pathlib import Path

from iterations import measure


def main() -> None:
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
parser.add_argument("--bits", type=int, default=32)
parser.add_argument("--collisions", type=int, default=5)
parser.add_argument("--runs", type=int, default=1000)
parser.add_argument("--workers", type=int, default=os.cpu_count())
parser.add_argument("--out", type=Path, default=Path(__file__).parent / "results")
args = parser.parse_args()

with ProcessPoolExecutor(args.workers) as pool:
results = measure(pool, args.bits, args.collisions, args.runs)

args.out.mkdir(parents=True, exist_ok=True)
path = args.out / "distribution.csv"
with path.open("w", newline="") as f:
writer = csv.writer(f)
writer.writerow([f"c{c}" for c in range(1, args.collisions + 1)])
writer.writerows(results)

final = [row[-1] for row in results]
print(
f"b={args.bits} C={args.collisions}: {args.runs:,} chains, "
f"mean {statistics.mean(final):,.0f}, std {statistics.pstdev(final):,.0f}"
)
print(f"Wrote {path}")


if __name__ == "__main__":
main()
Loading
Loading