Skip to content

Commit

Permalink
feat: add helper for choosing argon2 parameters (#803)
Browse files Browse the repository at this point in the history
This patch adds the new command "hashers argon2 calibrate" which allows one to pick the desired hashing time for password hashing and then chooses the optimal parameters for the hardware the command is running on:

```
$ kratos hashers argon2 calibrate 500ms
Increasing memory to get over 500ms:
    took 2.846592732s in try 0
    took 6.006488824s in try 1
  took 4.42657975s with 4.00GB of memory
[...]
Decreasing iterations to get under 500ms:
    took 484.257775ms in try 0
    took 488.784192ms in try 1
  took 486.534204ms with 3 iterations
Settled on 3 iterations.

{
  "memory": 1048576,
  "iterations": 3,
  "parallelism": 32,
  "salt_length": 16,
  "key_length": 32
}
```

Closes #723
Closes #572
Closes #647
  • Loading branch information
zepatrik committed Nov 6, 2020
1 parent 1595eda commit ca5a69b
Show file tree
Hide file tree
Showing 34 changed files with 719 additions and 50 deletions.
14 changes: 2 additions & 12 deletions cmd/clidoc/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,13 @@ import (
"fmt"
"os"

"github.com/spf13/cobra"

"github.com/ory/kratos/cmd/remote"
"github.com/ory/kratos/cmd"

"github.com/ory/x/clidoc"

"github.com/ory/kratos/cmd/identities"
"github.com/ory/kratos/cmd/jsonnet"
)

func main() {
rootCmd := &cobra.Command{Use: "kratos"}
identities.RegisterCommandRecursive(rootCmd)
jsonnet.RegisterCommandRecursive(rootCmd)
remote.RegisterCommandRecursive(rootCmd)

if err := clidoc.Generate(rootCmd, os.Args[1:]); err != nil {
if err := clidoc.Generate(cmd.RootCmd, os.Args[1:]); err != nil {
_, _ = fmt.Fprintf(os.Stderr, "%+v", err)
os.Exit(1)
}
Expand Down
237 changes: 237 additions & 0 deletions cmd/hashers/argon2/calibrate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
package argon2

import (
"encoding/json"
"fmt"
"time"

"github.com/fatih/color"
"github.com/inhies/go-bytesize"
"github.com/spf13/cobra"

"github.com/ory/kratos/driver/configuration"
"github.com/ory/kratos/hash"
"github.com/ory/x/cmdx"
)

type (
argon2Config struct {
c configuration.HasherArgon2Config
}
)

func (c *argon2Config) HasherArgon2() *configuration.HasherArgon2Config {
return &c.c
}

func (c *argon2Config) getMemFormat() string {
return (bytesize.ByteSize(c.c.Memory) * bytesize.KB).String()
}

const (
FlagStartMemory = "start-memory"
FlagMaxMemory = "max-memory"
FlagAdjustMemory = "adjust-memory-by"
FlagStartIterations = "start-iterations"
FlagParallelism = "parallelism"
FlagSaltLength = "salt-length"
FlagKeyLength = "key-length"

FlagQuiet = "quiet"
FlagRuns = "probe-runs"
)

var resultColor = color.New(color.FgGreen)

func newCalibrateCmd() *cobra.Command {
var (
maxMemory, adjustMemory, startMemory bytesize.ByteSize = 0, 1 * bytesize.GB, 4 * bytesize.GB
quiet bool
runs int
)

config := &argon2Config{
c: configuration.HasherArgon2Config{},
}

cmd := &cobra.Command{
Use: "calibrate [<desired-duration>]",
Args: cobra.ExactArgs(1),
Short: "Computes Optimal Argon2 Parameters.",
Long: `This command helps you calibrate the configuration parameters for Argon2. Password hashing is a trade-off between security, resource consumption, and user experience. Resource consumption should not be too high and the login should not take too long.
We recommend that the login process takes between half a second and one second for password hashing, giving a good balance between security and user experience.
Please note that the values depend on the machine you run the hashing on. If you have RAM constraints please choose lower memory targets to avoid out of memory panics.`,
RunE: func(cmd *cobra.Command, args []string) error {
desiredDuration, err := time.ParseDuration(args[0])
if err != nil {
return err
}

config.c.Memory = toKB(startMemory)

hasher := hash.NewHasherArgon2(config)

var currentDuration time.Duration

if !quiet {
fmt.Fprintf(cmd.ErrOrStderr(), "Increasing memory to get over %s:\n", desiredDuration)
}

for {
if maxMemory != 0 && config.c.Memory > toKB(maxMemory) {
// don't further increase memory
if !quiet {
fmt.Fprintln(cmd.ErrOrStderr(), " ouch, hit the memory limit there")
}
config.c.Memory = toKB(maxMemory)
break
}

currentDuration, err = probe(cmd, hasher, runs, quiet)
if err != nil {
return err
}

if !quiet {
fmt.Fprintf(cmd.ErrOrStderr(), " took %s with %s of memory\n", currentDuration, config.getMemFormat())
}

if currentDuration > desiredDuration {
if config.c.Memory <= toKB(adjustMemory) {
// adjusting the memory would now result in <= 0B
adjustMemory = adjustMemory >> 1
}
config.c.Memory -= toKB(adjustMemory)
break
}

// adjust config
config.c.Memory += toKB(adjustMemory)
}

if !quiet {
fmt.Fprintf(cmd.ErrOrStderr(), "Decreasing memory to get under %s:\n", desiredDuration)
}

for {
currentDuration, err = probe(cmd, hasher, runs, quiet)
if err != nil {
return err
}

if !quiet {
fmt.Fprintf(cmd.ErrOrStderr(), " took %s with %s of memory\n", currentDuration, config.getMemFormat())
}

if currentDuration < desiredDuration {
break
}

if config.c.Memory <= toKB(adjustMemory) {
// adjusting the memory would now result in <= 0B
adjustMemory = adjustMemory >> 1
}

// adjust config
config.c.Memory -= toKB(adjustMemory)
}

if !quiet {
_, _ = resultColor.Fprintf(cmd.ErrOrStderr(), "Settled on %s of memory.\n", config.getMemFormat())
fmt.Fprintf(cmd.ErrOrStderr(), "Increasing iterations to get over %s:\n", desiredDuration)
}

for {
currentDuration, err = probe(cmd, hasher, runs, quiet)
if err != nil {
return err
}

if !quiet {
fmt.Fprintf(cmd.ErrOrStderr(), " took %s with %d iterations\n", currentDuration, config.c.Iterations)
}

if currentDuration > desiredDuration {
config.c.Iterations -= 1
break
}

// adjust config
config.c.Iterations += 1
}

if !quiet {
fmt.Fprintf(cmd.ErrOrStderr(), "Decreasing iterations to get under %s:\n", desiredDuration)
}

for {
currentDuration, err = probe(cmd, hasher, runs, quiet)
if err != nil {
return err
}

if !quiet {
fmt.Fprintf(cmd.ErrOrStderr(), " took %s with %d iterations\n", currentDuration, config.c.Iterations)
}

// break also when iterations is 1; this catches the case where 1 was only slightly under the desired time and took longer a bit longer on another run
if currentDuration < desiredDuration || config.c.Iterations == 1 {
break
}

// adjust config
config.c.Iterations -= 1
}
if !quiet {
_, _ = resultColor.Fprintf(cmd.ErrOrStderr(), "Settled on %d iterations.\n\n", config.c.Iterations)
}

e := json.NewEncoder(cmd.OutOrStdout())
e.SetIndent("", " ")
return e.Encode(config.c)
},
}

flags := cmd.Flags()

flags.BoolVarP(&quiet, FlagQuiet, "q", false, "Quiet output.")
flags.IntVarP(&runs, FlagRuns, "r", 2, "Runs per probe, median of all runs is taken as the result.")

flags.VarP(&startMemory, FlagStartMemory, "m", "Amount of memory to start probing at.")
flags.Var(&maxMemory, FlagMaxMemory, "Maximum memory allowed (default no limit).")
flags.Var(&adjustMemory, FlagAdjustMemory, "Amount by which the memory is adjusted in every step while probing.")

flags.Uint32VarP(&config.c.Iterations, FlagStartIterations, "i", 1, "Number of iterations to start probing at.")

flags.Uint8Var(&config.c.Parallelism, FlagParallelism, configuration.Argon2DefaultParallelism, "Number of threads to use.")

flags.Uint32Var(&config.c.SaltLength, FlagSaltLength, configuration.Argon2DefaultSaltLength, "Length of the salt in bytes.")
flags.Uint32Var(&config.c.KeyLength, FlagKeyLength, configuration.Argon2DefaultKeyLength, "Length of the key in bytes.")

return cmd
}

func toKB(b bytesize.ByteSize) uint32 {
return uint32(b / bytesize.KB)
}

func probe(cmd *cobra.Command, hasher hash.Hasher, runs int, quiet bool) (time.Duration, error) {
start := time.Now()

var mid time.Time
for i := 0; i < runs; i++ {
mid = time.Now()
_, err := hasher.Generate([]byte("password"))
if err != nil {
fmt.Fprintf(cmd.ErrOrStderr(), "Could not generate a hash: %s\n", err)
return 0, cmdx.FailSilently(cmd)
}
if !quiet {
fmt.Fprintf(cmd.OutOrStdout(), " took %s in try %d\n", time.Since(mid), i)
}
}

return time.Duration(int64(time.Since(start)) / int64(runs)), nil
}
13 changes: 13 additions & 0 deletions cmd/hashers/argon2/root.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package argon2

import "github.com/spf13/cobra"

var rootCmd = &cobra.Command{
Use: "argon2",
}

func RegisterCommandRecursive(parent *cobra.Command) {
parent.AddCommand(rootCmd)

rootCmd.AddCommand(newCalibrateCmd())
}
18 changes: 18 additions & 0 deletions cmd/hashers/root.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package hashers

import (
"github.com/spf13/cobra"

"github.com/ory/kratos/cmd/hashers/argon2"
)

var rootCmd = &cobra.Command{
Use: "hashers",
Short: "This command contains helpers around hashing.",
}

func RegisterCommandRecursive(parent *cobra.Command) {
parent.AddCommand(rootCmd)

argon2.RegisterCommandRecursive(rootCmd)
}
27 changes: 15 additions & 12 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"fmt"
"os"

"github.com/ory/kratos/cmd/hashers"

"github.com/ory/kratos/cmd/remote"

"github.com/ory/kratos/cmd/identities"
Expand All @@ -19,30 +21,31 @@ import (
"github.com/spf13/cobra"
)

// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
// RootCmd represents the base command when called without any subcommands
var RootCmd = &cobra.Command{
Use: "kratos",
}

// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
// This is called by main.main(). It only needs to happen once to the RootCmd.
func Execute() {
if err := rootCmd.Execute(); err != nil {
if err := RootCmd.Execute(); err != nil {
if !errors.Is(err, cmdx.ErrNoPrintButFail) {
fmt.Println(err)
fmt.Fprintln(RootCmd.ErrOrStderr(), err)
}
os.Exit(1)
}
}

func init() {
viperx.RegisterConfigFlag(rootCmd, "kratos")
viperx.RegisterConfigFlag(RootCmd, "kratos")

identities.RegisterCommandRecursive(rootCmd)
jsonnet.RegisterCommandRecursive(rootCmd)
serve.RegisterCommandRecursive(rootCmd)
migrate.RegisterCommandRecursive(rootCmd)
remote.RegisterCommandRecursive(rootCmd)
identities.RegisterCommandRecursive(RootCmd)
jsonnet.RegisterCommandRecursive(RootCmd)
serve.RegisterCommandRecursive(RootCmd)
migrate.RegisterCommandRecursive(RootCmd)
remote.RegisterCommandRecursive(RootCmd)
hashers.RegisterCommandRecursive(RootCmd)

rootCmd.AddCommand(cmdx.Version(&clihelpers.BuildVersion, &clihelpers.BuildGitHash, &clihelpers.BuildTime))
RootCmd.AddCommand(cmdx.Version(&clihelpers.BuildVersion, &clihelpers.BuildGitHash, &clihelpers.BuildTime))
}

0 comments on commit ca5a69b

Please sign in to comment.