Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add support for the hybrid mode of Argon2 for passphrases #287

Merged
merged 4 commits into from
Apr 29, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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
133 changes: 91 additions & 42 deletions argon2.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ package secboot

import (
"errors"
"fmt"
"math"
"runtime"
"sync"
"time"
Expand Down Expand Up @@ -63,12 +65,30 @@ func argon2KDF() Argon2KDF {
return argon2Impl
}

// Argon2Options specifies parameters for the Argon2 KDF used by cryptsetup
// and for passphrase support.
// Argon2Mode describes the Argon2 mode to use.
type Argon2Mode string

const (
// Argon2Default is used by Argon2Options to select the default
// Argon2 mode, which is currently Argon2id.
Argon2Default Argon2Mode = ""

// Argon2i is the data-independent mode of Argon2.
Argon2i Argon2Mode = "argon2i"

// Argon2id is the hybrid mode of Argon2.
Argon2id Argon2Mode = "argon2id"
)

// Argon2Options specifies parameters for the Argon2 KDF used for passphrase support.
type Argon2Options struct {
// Mode specifies the KDF mode to use.
Mode Argon2Mode

// MemoryKiB specifies the maximum memory cost in KiB when ForceIterations
// is zero. If ForceIterations is not zero, then this is used as the
// memory cost.
// is zero. In this case, it will be capped at 4GiB or half of the available
// memory, whichever is less. If ForceIterations is not zero, then this is
// used as the memory cost and is not limited.
MemoryKiB uint32

// TargetDuration specifies the target duration for the KDF which
Expand All @@ -82,55 +102,78 @@ type Argon2Options struct {
// parameters are benchmarked based on the value of TargetDuration.
ForceIterations uint32

// Parallel sets the maximum number of parallel threads for the
// KDF (up to 4). This will be adjusted downwards based on the
// actual number of CPUs.
// Parallel sets the maximum number of parallel threads for the KDF. If
// it is zero, then it is set to the number of CPUs or 4, whichever is
// less. This will always be automatically limited to 4 when ForceIterations
// is zero.
Parallel uint8
}

func (o *Argon2Options) deriveCostParams(keyLen int) (*Argon2CostParams, error) {
func (o *Argon2Options) kdfParams(keyLen uint32) (*kdfParams, error) {
switch o.Mode {
case Argon2Default, Argon2i, Argon2id:
// ok
default:
return nil, errors.New("invalid argon2 mode")
}

mode := o.Mode
if mode == Argon2Default {
mode = Argon2id
}

switch {
case o.ForceIterations > 0:
threads := runtimeNumCPU()
if threads > 4 {
threads = 4
// The non-benchmarked path. Ensure that ForceIterations
// and MemoryKiB fit into an int32 so that it always fits
// into an int
switch {
case o.ForceIterations > math.MaxInt32:
return nil, fmt.Errorf("invalid iterations count %d", o.ForceIterations)
case o.MemoryKiB > math.MaxInt32:
return nil, fmt.Errorf("invalid memory cost %dKiB", o.MemoryKiB)
}
params := &Argon2CostParams{
Time: o.ForceIterations,
MemoryKiB: 1 * 1024 * 1024,
Threads: uint8(threads)}

defaultThreads := runtimeNumCPU()
if defaultThreads > 4 {
// limit the default threads to 4
defaultThreads = 4
}

params := &kdfParams{
Type: string(mode),
Time: int(o.ForceIterations), // no limit to the time cost.
Memory: 1 * 1024 * 1024, // the default memory cost is 1GiB.
CPUs: defaultThreads, // the default number of threads is min(4,nr_of_cpus).
}
if o.MemoryKiB != 0 {
params.MemoryKiB = o.MemoryKiB
// no limit to the memory cost.
params.Memory = int(o.MemoryKiB)
}
if o.Parallel != 0 {
params.Threads = o.Parallel
if o.Parallel > 4 {
params.Threads = 4
}
// no limit to the threads if set explicitly.
params.CPUs = int(o.Parallel)
}

return params, nil
default:
benchmarkParams := &argon2.BenchmarkParams{
MaxMemoryCostKiB: 1 * 1024 * 1024,
TargetDuration: 2 * time.Second}
MaxMemoryCostKiB: 1 * 1024 * 1024, // the default maximum memory cost is 1GiB.
TargetDuration: 2 * time.Second, // the default target duration is 2s.
}

if o.MemoryKiB != 0 {
benchmarkParams.MaxMemoryCostKiB = o.MemoryKiB
benchmarkParams.MaxMemoryCostKiB = o.MemoryKiB // this is capped to 4GiB by internal/argon2.
}
if o.TargetDuration != 0 {
benchmarkParams.TargetDuration = o.TargetDuration
}
if o.Parallel != 0 {
benchmarkParams.Threads = o.Parallel
if o.Parallel > 4 {
benchmarkParams.Threads = 4
}
benchmarkParams.Threads = o.Parallel // this is capped to 4 by internal/argon2.
}

params, err := argon2.Benchmark(benchmarkParams, func(params *argon2.CostParams) (time.Duration, error) {
return argon2KDF().Time(&Argon2CostParams{
return argon2KDF().Time(mode, &Argon2CostParams{
Time: params.Time,
MemoryKiB: params.MemoryKiB,
Threads: params.Threads})
Expand All @@ -139,10 +182,12 @@ func (o *Argon2Options) deriveCostParams(keyLen int) (*Argon2CostParams, error)
return nil, xerrors.Errorf("cannot benchmark KDF: %w", err)
}

return &Argon2CostParams{
Time: params.Time,
MemoryKiB: params.MemoryKiB,
Threads: params.Threads}, nil
o = &Argon2Options{
Mode: mode,
MemoryKiB: params.MemoryKiB,
ForceIterations: params.Time,
Parallel: params.Threads}
return o.kdfParams(keyLen)
}
}

Expand Down Expand Up @@ -172,18 +217,20 @@ func (p *Argon2CostParams) internalParams() *argon2.CostParams {
// to delegate execution to a short-lived utility process where required.
type Argon2KDF interface {
// Derive derives a key of the specified length in bytes, from the supplied
// passphrase and salt and using the supplied cost parameters.
Derive(passphrase string, salt []byte, params *Argon2CostParams, keyLen uint32) ([]byte, error)
// passphrase and salt and using the supplied mode and cost parameters.
Derive(passphrase string, salt []byte, mode Argon2Mode, params *Argon2CostParams, keyLen uint32) ([]byte, error)

// Time measures the amount of time the KDF takes to execute with the
// specified cost parameters.
Time(params *Argon2CostParams) (time.Duration, error)
// specified cost parameters and mode.
Time(mode Argon2Mode, params *Argon2CostParams) (time.Duration, error)
}

type inProcessArgon2KDFImpl struct{}

func (_ inProcessArgon2KDFImpl) Derive(passphrase string, salt []byte, params *Argon2CostParams, keyLen uint32) ([]byte, error) {
func (_ inProcessArgon2KDFImpl) Derive(passphrase string, salt []byte, mode Argon2Mode, params *Argon2CostParams, keyLen uint32) ([]byte, error) {
switch {
case mode != Argon2i && mode != Argon2id:
return nil, errors.New("invalid mode")
case params == nil:
return nil, errors.New("nil params")
case params.Time == 0:
Expand All @@ -192,11 +239,13 @@ func (_ inProcessArgon2KDFImpl) Derive(passphrase string, salt []byte, params *A
return nil, errors.New("invalid number of threads")
}

return argon2.Key(passphrase, salt, params.internalParams(), keyLen), nil
return argon2.Key(passphrase, salt, argon2.Mode(mode), params.internalParams(), keyLen), nil
}

func (_ inProcessArgon2KDFImpl) Time(params *Argon2CostParams) (time.Duration, error) {
func (_ inProcessArgon2KDFImpl) Time(mode Argon2Mode, params *Argon2CostParams) (time.Duration, error) {
switch {
case mode != Argon2i && mode != Argon2id:
return 0, errors.New("invalid mode")
case params == nil:
return 0, errors.New("nil params")
case params.Time == 0:
Expand All @@ -205,7 +254,7 @@ func (_ inProcessArgon2KDFImpl) Time(params *Argon2CostParams) (time.Duration, e
return 0, errors.New("invalid number of threads")
}

return argon2.KeyDuration(params.internalParams()), nil
return argon2.KeyDuration(argon2.Mode(mode), params.internalParams()), nil
}

// InProcessArgon2KDF is the in-process implementation of the Argon2 KDF. This
Expand All @@ -216,10 +265,10 @@ var InProcessArgon2KDF = inProcessArgon2KDFImpl{}

type nullArgon2KDFImpl struct{}

func (_ nullArgon2KDFImpl) Derive(passphrase string, salt []byte, params *Argon2CostParams, keyLen uint32) ([]byte, error) {
func (_ nullArgon2KDFImpl) Derive(passphrase string, salt []byte, mode Argon2Mode, params *Argon2CostParams, keyLen uint32) ([]byte, error) {
return nil, errors.New("no argon2 KDF: please call secboot.SetArgon2KDF")
}

func (_ nullArgon2KDFImpl) Time(params *Argon2CostParams) (time.Duration, error) {
func (_ nullArgon2KDFImpl) Time(mode Argon2Mode, params *Argon2CostParams) (time.Duration, error) {
return 0, errors.New("no argon2 KDF: please call secboot.SetArgon2KDF")
}
Loading
Loading