Skip to content

Commit

Permalink
Merge pull request #287 from chrisccoulson/add-hybrid-argon2-mode
Browse files Browse the repository at this point in the history
Add support for the hybrid mode of Argon2 for passphrases.

Passphrase support currently hardcodes the use of argon2i, which is
the data-independent version of Argon2. Whilst this has better
side-channel resistance than data-dependent versions, it requires
more iterations to protect against time-memory tradeoff attacks.

Cryptsetup recently switch to argon2id by default for passphrase
hashing, which is the hybrid version that provides a good balance
between side-channel resistance and time-memory tradeoff.

This introduces support for selecting between the 2 versions for
passphrase support.

There are other versions of Argon2, such as the data-dependent version
which sacrifices side-channel resistance for better resistance against
time-memory tradeoff attacks, but this isn't supported by the argon2 go
package.
  • Loading branch information
chrisccoulson authored Apr 29, 2024
2 parents 21595ba + 970059b commit 3b38f56
Show file tree
Hide file tree
Showing 8 changed files with 399 additions and 148 deletions.
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 = argon2.Mode

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 = argon2.ModeI

// Argon2id is the hybrid mode of Argon2.
Argon2id = argon2.ModeID
)

// 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

0 comments on commit 3b38f56

Please sign in to comment.