Skip to content

feat: --guesses (zxcvbn) and --memory (argon2id) flags#8

Merged
kanywst merged 5 commits intomainfrom
feat/external-guesses-and-memory
May 5, 2026
Merged

feat: --guesses (zxcvbn) and --memory (argon2id) flags#8
kanywst merged 5 commits intomainfrom
feat/external-guesses-and-memory

Conversation

@kanywst
Copy link
Copy Markdown
Owner

@kanywst kanywst commented May 5, 2026

Reopens superseded #7. The original PR was auto-closed when its stacked base (chore/externalize-hashrates) was deleted on merge. This PR's branch has been rebased onto the post-merge main.

Summary

Adds the two highest-leverage usability gaps from the post-merge review of #1:

  • --memory accepts an Argon2id memory parameter (64m, 128m, 1g, or bare MB) and routes it through the hashrate model. Argon2id is a memory-hard function — bumping memory is the single most effective defender lever against GPU attackers — but the previous code only divided by workFactor (time iterations), silently underestimating defender strength for any policy above the 64MB baseline.
  • --guesses accepts an external guess count, typically the guesses field from a zxcvbn report, and short-circuits the built-in naive entropy estimator. This finally lets brtc act as a pure cost converter on top of a real strength meter, addressing the README positioning.

When --guesses is set the password argument becomes optional and the budget/max-length feature is skipped (it needs a charset assumption that an external estimator does not provide).

How to use

guesses=$(zxcvbn-cli "P@ssw0rd!" --json | jq -r .guesses)
brtc --guesses "$guesses" --algo bcrypt --cost 12 --hw aws-p5.48xlarge

brtc --algo argon2id --memory 64m  -o json "P@ssw0rd!" | jq .cost_usd
brtc --algo argon2id --memory 256m -o json "P@ssw0rd!" | jq .cost_usd
brtc --algo argon2id --memory 1g   -o json "P@ssw0rd!" | jq .cost_usd

Verification (local)

$ brtc --guesses 1e10 -o json | jq '{combinations, entropy_bits, time_to_crack_seconds, cost_usd}'
{ "combinations": 10000000000, "entropy_bits": 33.22, "time_to_crack_seconds": 1600000, "cost_usd": 133.33 }

$ brtc --algo argon2id --memory 64m  -o json "P@ssw0rd!" | jq .hash_rate_per_sec   # 100
$ brtc --algo argon2id --memory 256m -o json "P@ssw0rd!" | jq .hash_rate_per_sec   # 25  (4x slower)
  • go test -count=1 ./...
  • golangci-lint run ./... ✓ 0 issues

Test additions

  • TestParseMemory / TestParseGuesses — unit boundaries, scientific notation, rejection of malformed inputs.
  • TestCalculateHashRate_Argon2MemoryScaling — baseline, doubling, sub-baseline clamp, multiplicative composition with time.
  • TestFromGuesses and TestFromGuesses_HugeNumberDoesNotOverflow — bit-length fallback for guess counts beyond float64 range.

kanywst added 4 commits May 5, 2026 16:04
ParseMemory accepts the Argon2id memory parameter as 64m, 1g, or a
bare integer (MB). KB intentionally omitted because Argon2id is
never configured below MB granularity.

ParseGuesses accepts the 'guesses' field from a zxcvbn report,
including scientific notation (1.234e+10). Returns *big.Int because
guess counts for long passwords routinely exceed int64.

Both helpers are wired up in the next commit.
Argon2id is a memory-hard function: its actual attacker throughput
depends almost as much on memory pressure as on time iterations. The
previous CalculateHashRate formula divided only by workFactor (t),
treating memory as a fixed baseline. That underestimated defender
strength for any policy that bumps memory above the baseline.

Adds memoryMB as a new parameter on CalculateHashRate and
MaxLengthForBudget. The argon2id branch now divides by both the time
factor and memoryMB / argon2BaselineMemoryMB (64MB), so doubling
memory roughly halves attacker throughput. memoryMB <= baseline is
clamped to the baseline so an unrealistic input never appears to
speed the attacker up.

This commit is just the signature/scaling change. All call sites pass
0 (= use baseline), preserving the prior behavior. The next commit
wires up the --memory CLI flag.
--memory accepts an Argon2id memory parameter (e.g. 64m, 128m, 1g)
that flows through CalculateHashRate and MaxLengthForBudget so
defenders running heavier memory settings see the corresponding cost
benefit.

--guesses accepts an external guess count, typically the 'guesses'
field from a zxcvbn report, and short-circuits the built-in
character-class entropy estimator. This lets brtc act as a pure
cost converter on top of a real strength meter:

  guesses=$(zxcvbn-cli 'P@ssw0rd!' --json | jq -r .guesses)
  brtc --guesses "$guesses" --algo bcrypt --cost 12 'P@ssw0rd!'

When --guesses is set the password argument becomes optional (so it
can be omitted entirely if the caller does not want to pass the
literal string), and budget/max-length is skipped because the
external estimator does not give us a charset to scale against.

Adds calc.FromGuesses for the construction path and a log2 helper
that falls back to bit length so log2(huge *big.Int) does not blow
up float64. MemoryMB is exposed on OutputData JSON via 'memory_mb'
(omitempty) so JSON consumers see the parameter.
Adds the two new flags to the options table, plus a worked example
of the recommended brtc + zxcvbn combo (zxcvbn does pattern detection,
brtc converts the resulting guess count into time and USD), and an
Argon2id memory-sweep example showing how memory hardens against GPU
attackers.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 5, 2026

Warning

Rate limit exceeded

@kanywst has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 11 minutes before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 81fb4265-f9b0-414f-9536-41830ab974c5

📥 Commits

Reviewing files that changed from the base of the PR and between 4f7e4fd and 2a9e547.

📒 Files selected for processing (11)
  • README.md
  • cmd/root.go
  • internal/calc/entropy.go
  • internal/calc/entropy_test.go
  • internal/calc/parse.go
  • internal/calc/parse_test.go
  • internal/cost/cloud.go
  • internal/cost/cloud_test.go
  • internal/cost/hardware.go
  • internal/cost/hardware_test.go
  • internal/ui/output.go
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/external-guesses-and-memory

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces support for Argon2id memory parameters and external entropy estimates via the new --memory and --guesses flags. The changes include updated hash rate calculation logic to account for memory-hard functions, a new parsing utility for memory units and scientific notation, and expanded documentation and tests. The review feedback identifies several opportunities to improve the robustness of the new parsing logic, specifically by making the memory unit regex more flexible, preventing potential integer overflows during unit conversion, and handling infinite values when parsing external guess counts.

Comment thread internal/calc/parse.go
"strings"
)

var memoryRe = regexp.MustCompile(`^(\d+)([mg])?$`)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The regular expression for parsing memory is quite restrictive. It doesn't allow for common variations like mb, gb, or spaces between the number and the unit (e.g., 64 mb). Improving the regex to be more flexible would enhance the user experience.

Suggested change
var memoryRe = regexp.MustCompile(`^(\d+)([mg])?$`)
var memoryRe = regexp.MustCompile(`^(\d+)\s*([mg]b?)?$`)

Comment thread internal/calc/parse.go
Comment on lines +29 to +35
switch m[2] {
case "", "m":
return n, nil
case "g":
return n * 1024, nil
}
return 0, fmt.Errorf("unknown unit %s", m[2])
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The unit parsing logic should be updated to handle the more flexible units (like mb or gb) allowed by the suggested regex. Additionally, when converting GB to MB, there is a potential for integer overflow if a very large value is provided. Adding a check for this ensures robustness, especially on 32-bit architectures.

	switch {
	case m[2] == "" || strings.HasPrefix(m[2], "m"):
		return n, nil
	case strings.HasPrefix(m[2], "g"):
		if n > 2097151 { // Limit to ~2TB to prevent overflow on 32-bit systems
			return 0, fmt.Errorf("memory value too large: %d GB", n)
		}
		return n * 1024, nil
	}
	return 0, fmt.Errorf("unknown unit %s", m[2])

Comment thread internal/calc/parse.go
Comment on lines +53 to +56
f, _, err := big.ParseFloat(s, 10, 256, big.ToNearestEven)
if err != nil {
return nil, fmt.Errorf("guesses: cannot parse %q", s)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The big.ParseFloat function can return infinite values if the input string is "inf" or if the value exceeds the maximum representable exponent. Since f.Int(nil) has undefined behavior for infinite values, it is important to check for this condition explicitly.

Suggested change
f, _, err := big.ParseFloat(s, 10, 256, big.ToNearestEven)
if err != nil {
return nil, fmt.Errorf("guesses: cannot parse %q", s)
}
f, _, err := big.ParseFloat(s, 10, 256, big.ToNearestEven)
if err != nil {
return nil, fmt.Errorf("guesses: cannot parse %q", s)
}
if f.IsInf() {
return nil, fmt.Errorf("guesses: value cannot be infinite")
}

big.ParseFloat happily accepts 'inf' / '+inf' / '-inf'. The
subsequent big.Float.Int(nil) on +Inf returns a nil *big.Int rather
than an error, and that nil value would then panic downstream in
TimeToCrack via big.Float.SetInt(nil).

Adds an explicit IsInf check after sign validation, plus tests for
'inf' and '+inf'.
@kanywst
Copy link
Copy Markdown
Owner Author

kanywst commented May 5, 2026

Addressed @gemini-code-assist's three suggestions:

@gemini-code-assist
Copy link
Copy Markdown

Thanks for the update and for addressing the inf check in ParseGuesses. That fix looks solid and correctly handles the potential panic scenario. I understand your reasoning regarding the memory regex and the 32-bit overflow concerns; given the practical constraints and the focus on usability, your decisions make sense.

@kanywst kanywst merged commit ae226f0 into main May 5, 2026
2 checks passed
@kanywst kanywst deleted the feat/external-guesses-and-memory branch May 5, 2026 07:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant