From b48505c0b20da656b7f19791f1459b88545fd41d Mon Sep 17 00:00:00 2001 From: Chris Coulson Date: Sat, 9 Mar 2024 01:16:26 +0000 Subject: [PATCH] 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. --- argon2.go | 119 ++++++++++++++------ argon2_test.go | 194 +++++++++++++++++++++++++-------- export_test.go | 9 +- internal/argon2/argon2.go | 39 +++++-- internal/argon2/argon2_test.go | 38 ++++++- internal/testutil/argon2.go | 34 ++++-- keydata.go | 54 ++++----- keydata_test.go | 27 +++-- 8 files changed, 375 insertions(+), 139 deletions(-) diff --git a/argon2.go b/argon2.go index de6389d0..86e852fd 100644 --- a/argon2.go +++ b/argon2.go @@ -21,6 +21,8 @@ package secboot import ( "errors" + "fmt" + "math" "runtime" "sync" "time" @@ -63,9 +65,26 @@ 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. 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 @@ -74,8 +93,8 @@ type Argon2Options struct { // TargetDuration specifies the target duration for the KDF which // is used to benchmark the time and memory cost parameters. If it - // is zero then the default is used (2 seconds). If ForceIterations is not - // zero then this field is ignored. + // is zero then the default is used. If ForceIterations is not zero + // then this field is ignored. TargetDuration time.Duration // ForceIterations can be used to turn off KDF benchmarking by @@ -90,43 +109,71 @@ type Argon2Options struct { 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 + // 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 + 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}) @@ -135,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) } } @@ -168,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: @@ -188,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: @@ -201,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 @@ -212,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") } diff --git a/argon2_test.go b/argon2_test.go index 5b8fee9f..509703e2 100644 --- a/argon2_test.go +++ b/argon2_test.go @@ -71,28 +71,38 @@ func (s *argon2Suite) SetUpTest(c *C) { s.AddCleanup(func() { SetArgon2KDF(origKdf) }) } -func (s *argon2Suite) checkParams(c *C, opts *Argon2Options, ncpus uint8, params *Argon2CostParams) { +func (s *argon2Suite) checkParams(c *C, opts *Argon2Options, ncpus uint8, params *KdfParams) { + expectedMode := Argon2id + if opts.Mode != Argon2Default { + expectedMode = opts.Mode + } + c.Check(params.Type, Equals, string(expectedMode)) + if opts.ForceIterations != 0 { - c.Check(params.Time, Equals, opts.ForceIterations) + c.Check(params.Time, Equals, int(opts.ForceIterations)) expectedMem := opts.MemoryKiB if expectedMem == 0 { expectedMem = 1 * 1024 * 1024 } - c.Check(params.MemoryKiB, Equals, expectedMem) + c.Check(params.Memory, Equals, int(expectedMem)) expectedThreads := opts.Parallel if expectedThreads == 0 { - expectedThreads = ncpus + expectedThreads = uint8(ncpus) } - c.Check(params.Threads, Equals, expectedThreads) + c.Check(params.CPUs, Equals, int(expectedThreads)) } else { targetDuration := opts.TargetDuration if targetDuration == 0 { targetDuration = 2 * time.Second } var kdf testutil.MockArgon2KDF - duration, _ := kdf.Time(params) + duration, _ := kdf.Time(Argon2Default, &Argon2CostParams{ + Time: uint32(params.Time), + MemoryKiB: uint32(params.Memory), + Threads: uint8(params.CPUs), + }) c.Check(duration, Equals, targetDuration) maxMem := opts.MemoryKiB @@ -102,110 +112,188 @@ func (s *argon2Suite) checkParams(c *C, opts *Argon2Options, ncpus uint8, params if maxMem > s.halfTotalRamKiB { maxMem = s.halfTotalRamKiB } - c.Check(int(params.MemoryKiB), snapd_testutil.IntLessEqual, int(maxMem)) + c.Check(params.Memory, snapd_testutil.IntLessEqual, int(maxMem)) expectedThreads := opts.Parallel if expectedThreads == 0 { - expectedThreads = ncpus + expectedThreads = uint8(ncpus) } if expectedThreads > 4 { expectedThreads = 4 } - c.Check(params.Threads, Equals, expectedThreads) + c.Check(params.CPUs, Equals, int(expectedThreads)) } } var _ = Suite(&argon2Suite{}) -func (s *argon2Suite) TestDeriveCostParamsDefault(c *C) { +func (s *argon2Suite) TestKDFParamsDefault(c *C) { var opts Argon2Options - params, err := opts.DeriveCostParams(0) + params, err := opts.KdfParams(0) c.Assert(err, IsNil) + c.Check(s.kdf.BenchmarkMode, Equals, Argon2id) s.checkParams(c, &opts, s.cpus, params) } -func (s *argon2Suite) TestDeriveCostParamsMemoryLimit(c *C) { +func (s *argon2Suite) TestKDFParamsExplicitMode(c *C) { + var opts Argon2Options + opts.Mode = Argon2i + params, err := opts.KdfParams(9) + c.Assert(err, IsNil) + c.Check(s.kdf.BenchmarkMode, Equals, Argon2i) + + s.checkParams(c, &opts, s.cpus, params) +} + +func (s *argon2Suite) TestKDFParamsMemoryLimit(c *C) { var opts Argon2Options opts.MemoryKiB = 32 * 1024 - params, err := opts.DeriveCostParams(0) + params, err := opts.KdfParams(0) c.Assert(err, IsNil) + c.Check(s.kdf.BenchmarkMode, Equals, Argon2id) s.checkParams(c, &opts, s.cpus, params) } -func (s *argon2Suite) TestDeriveCostParamsForceBenchmarkedThreads(c *C) { +func (s *argon2Suite) TestKDFParamsForceBenchmarkedThreads(c *C) { var opts Argon2Options opts.Parallel = 1 - params, err := opts.DeriveCostParams(0) + params, err := opts.KdfParams(0) c.Assert(err, IsNil) + c.Check(s.kdf.BenchmarkMode, Equals, Argon2id) s.checkParams(c, &opts, s.cpus, params) } -func (s *argon2Suite) TestDeriveCostParamsForceIterations(c *C) { +func (s *argon2Suite) TestKDFParamsForceIterations(c *C) { restore := MockRuntimeNumCPU(2) defer restore() var opts Argon2Options opts.ForceIterations = 3 - params, err := opts.DeriveCostParams(0) + params, err := opts.KdfParams(0) c.Assert(err, IsNil) + c.Check(s.kdf.BenchmarkMode, Equals, Argon2Default) s.checkParams(c, &opts, 2, params) } -func (s *argon2Suite) TestDeriveCostParamsForceMemory(c *C) { +func (s *argon2Suite) TestKDFParamsForceMemory(c *C) { restore := MockRuntimeNumCPU(2) defer restore() var opts Argon2Options opts.ForceIterations = 3 opts.MemoryKiB = 32 * 1024 - params, err := opts.DeriveCostParams(0) + params, err := opts.KdfParams(0) c.Assert(err, IsNil) + c.Check(s.kdf.BenchmarkMode, Equals, Argon2Default) s.checkParams(c, &opts, 2, params) } -func (s *argon2Suite) TestDeriveCostParamsForceIterationsDifferentCPUNum(c *C) { +func (s *argon2Suite) TestKDFParamsForceIterationsDifferentCPUNum(c *C) { restore := MockRuntimeNumCPU(8) defer restore() var opts Argon2Options opts.ForceIterations = 3 - params, err := opts.DeriveCostParams(0) + params, err := opts.KdfParams(0) c.Assert(err, IsNil) + c.Check(s.kdf.BenchmarkMode, Equals, Argon2Default) s.checkParams(c, &opts, 4, params) } -func (s *argon2Suite) TestDeriveCostParamsForceThreads(c *C) { +func (s *argon2Suite) TestKDFParamsForceThreads(c *C) { restore := MockRuntimeNumCPU(8) defer restore() var opts Argon2Options opts.ForceIterations = 3 opts.Parallel = 1 - params, err := opts.DeriveCostParams(0) + params, err := opts.KdfParams(9) c.Assert(err, IsNil) + c.Check(s.kdf.BenchmarkMode, Equals, Argon2Default) s.checkParams(c, &opts, 1, params) } -func (s *argon2Suite) TestDeriveCostParamsForceThreadsGreatherThanCPUNum(c *C) { +func (s *argon2Suite) TestKDFParamsForceThreadsGreatherThanCPUNum(c *C) { restore := MockRuntimeNumCPU(2) defer restore() var opts Argon2Options opts.ForceIterations = 3 opts.Parallel = 8 - params, err := opts.DeriveCostParams(0) + params, err := opts.KdfParams(0) c.Assert(err, IsNil) + c.Check(s.kdf.BenchmarkMode, Equals, Argon2Default) s.checkParams(c, &opts, 8, params) } +func (s *argon2Suite) TestKDFParamsInvalidForceIterations(c *C) { + var opts Argon2Options + opts.ForceIterations = math.MaxUint32 + _, err := opts.KdfParams(0) + c.Check(err, ErrorMatches, `invalid iterations count 4294967295`) +} + +func (s *argon2Suite) TestKDFParamsInvalidMemoryKiB(c *C) { + var opts Argon2Options + opts.ForceIterations = 4 + opts.MemoryKiB = math.MaxUint32 + _, err := opts.KdfParams(0) + c.Check(err, ErrorMatches, `invalid memory cost 4294967295KiB`) +} + +func (s *argon2Suite) TestInProcessKDFDeriveInvalidMode(c *C) { + _, err := InProcessArgon2KDF.Derive("foo", nil, Argon2Default, &Argon2CostParams{Time: 4, MemoryKiB: 32, Threads: 1}, 32) + c.Check(err, ErrorMatches, `invalid mode`) +} + +func (s *argon2Suite) TestInProcessKDFDeriveInvalidParams(c *C) { + _, err := InProcessArgon2KDF.Derive("foo", nil, Argon2id, nil, 32) + c.Check(err, ErrorMatches, `nil params`) +} + +func (s *argon2Suite) TestInProcessKDFDeriveInvalidTime(c *C) { + _, err := InProcessArgon2KDF.Derive("foo", nil, Argon2id, &Argon2CostParams{Time: 0, MemoryKiB: 32, Threads: 1}, 32) + c.Check(err, ErrorMatches, `invalid time cost`) +} + +func (s *argon2Suite) TestInProcessKDFDeriveInvalidThreads(c *C) { + _, err := InProcessArgon2KDF.Derive("foo", nil, Argon2id, &Argon2CostParams{Time: 4, MemoryKiB: 32, Threads: 0}, 32) + c.Check(err, ErrorMatches, `invalid number of threads`) +} + +func (s *argon2Suite) TestInProcessKDFTimeInvalidMode(c *C) { + _, err := InProcessArgon2KDF.Time(Argon2Default, &Argon2CostParams{Time: 4, MemoryKiB: 32, Threads: 1}) + c.Check(err, ErrorMatches, `invalid mode`) +} + +func (s *argon2Suite) TestInProcessKDFTimeInvalidParams(c *C) { + _, err := InProcessArgon2KDF.Time(Argon2id, nil) + c.Check(err, ErrorMatches, `nil params`) +} + +func (s *argon2Suite) TestInProcessKDFTimeInvalidTime(c *C) { + _, err := InProcessArgon2KDF.Time(Argon2id, &Argon2CostParams{Time: 0, MemoryKiB: 32, Threads: 1}) + c.Check(err, ErrorMatches, `invalid time cost`) +} + +func (s *argon2Suite) TestInProcessKDFTimeInvalidThreads(c *C) { + _, err := InProcessArgon2KDF.Time(Argon2id, &Argon2CostParams{Time: 4, MemoryKiB: 32, Threads: 0}) + c.Check(err, ErrorMatches, `invalid number of threads`) +} + +func (s *argon2Suite) TestModeConstants(c *C) { + c.Check(Argon2i, Equals, Argon2Mode(argon2.ModeI)) + c.Check(Argon2id, Equals, Argon2Mode(argon2.ModeID)) +} + type argon2SuiteExpensive struct{} func (s *argon2SuiteExpensive) SetUpSuite(c *C) { @@ -216,19 +304,20 @@ func (s *argon2SuiteExpensive) SetUpSuite(c *C) { var _ = Suite(&argon2SuiteExpensive{}) -type testArgon2iKDFDeriveData struct { +type testInProcessArgon2KDFDeriveData struct { passphrase string salt []byte + mode Argon2Mode params *Argon2CostParams keyLen uint32 } -func (s *argon2SuiteExpensive) testArgon2iKDFDerive(c *C, data *testArgon2iKDFDeriveData) { - key, err := InProcessArgon2KDF.Derive(data.passphrase, data.salt, data.params, data.keyLen) +func (s *argon2SuiteExpensive) testInProcessKDFDerive(c *C, data *testInProcessArgon2KDFDeriveData) { + key, err := InProcessArgon2KDF.Derive(data.passphrase, data.salt, data.mode, data.params, data.keyLen) c.Check(err, IsNil) runtime.GC() - expected := argon2.Key(data.passphrase, data.salt, &argon2.CostParams{ + expected := argon2.Key(data.passphrase, data.salt, argon2.Mode(data.mode), &argon2.CostParams{ Time: data.params.Time, MemoryKiB: data.params.MemoryKiB, Threads: data.params.Threads}, data.keyLen) @@ -237,10 +326,11 @@ func (s *argon2SuiteExpensive) testArgon2iKDFDerive(c *C, data *testArgon2iKDFDe c.Check(key, DeepEquals, expected) } -func (s *argon2SuiteExpensive) TestArgon2iKDFDerive(c *C) { - s.testArgon2iKDFDerive(c, &testArgon2iKDFDeriveData{ +func (s *argon2SuiteExpensive) TestInProcessKDFDerive(c *C) { + s.testInProcessKDFDerive(c, &testInProcessArgon2KDFDeriveData{ passphrase: "foo", salt: []byte("0123456789abcdefghijklmnopqrstuv"), + mode: Argon2id, params: &Argon2CostParams{ Time: 4, MemoryKiB: 32, @@ -248,10 +338,11 @@ func (s *argon2SuiteExpensive) TestArgon2iKDFDerive(c *C) { keyLen: 32}) } -func (s *argon2SuiteExpensive) TestArgon2iKDFDeriveDifferentPassphrase(c *C) { - s.testArgon2iKDFDerive(c, &testArgon2iKDFDeriveData{ +func (s *argon2SuiteExpensive) TestInProcessKDFDeriveDifferentPassphrase(c *C) { + s.testInProcessKDFDerive(c, &testInProcessArgon2KDFDeriveData{ passphrase: "bar", salt: []byte("0123456789abcdefghijklmnopqrstuv"), + mode: Argon2id, params: &Argon2CostParams{ Time: 4, MemoryKiB: 32, @@ -259,10 +350,23 @@ func (s *argon2SuiteExpensive) TestArgon2iKDFDeriveDifferentPassphrase(c *C) { keyLen: 32}) } -func (s *argon2SuiteExpensive) TestArgon2iKDFiDeriveDifferentSalt(c *C) { - s.testArgon2iKDFDerive(c, &testArgon2iKDFDeriveData{ +func (s *argon2SuiteExpensive) TestInProcessKDFiDeriveDifferentSalt(c *C) { + s.testInProcessKDFDerive(c, &testInProcessArgon2KDFDeriveData{ passphrase: "foo", salt: []byte("zyxwvutsrqponmlkjihgfedcba987654"), + mode: Argon2id, + params: &Argon2CostParams{ + Time: 4, + MemoryKiB: 32, + Threads: 4}, + keyLen: 32}) +} + +func (s *argon2SuiteExpensive) TestInProcessKDFDeriveDifferentMode(c *C) { + s.testInProcessKDFDerive(c, &testInProcessArgon2KDFDeriveData{ + passphrase: "foo", + salt: []byte("0123456789abcdefghijklmnopqrstuv"), + mode: Argon2i, params: &Argon2CostParams{ Time: 4, MemoryKiB: 32, @@ -270,10 +374,11 @@ func (s *argon2SuiteExpensive) TestArgon2iKDFiDeriveDifferentSalt(c *C) { keyLen: 32}) } -func (s *argon2SuiteExpensive) TestArgon2iKDFDeriveDifferentParams(c *C) { - s.testArgon2iKDFDerive(c, &testArgon2iKDFDeriveData{ +func (s *argon2SuiteExpensive) TestInProcessKDFDeriveDifferentParams(c *C) { + s.testInProcessKDFDerive(c, &testInProcessArgon2KDFDeriveData{ passphrase: "foo", salt: []byte("0123456789abcdefghijklmnopqrstuv"), + mode: Argon2id, params: &Argon2CostParams{ Time: 48, MemoryKiB: 32 * 1024, @@ -281,10 +386,11 @@ func (s *argon2SuiteExpensive) TestArgon2iKDFDeriveDifferentParams(c *C) { keyLen: 32}) } -func (s *argon2SuiteExpensive) TestArgon2iKDFDeriveDifferentKeyLen(c *C) { - s.testArgon2iKDFDerive(c, &testArgon2iKDFDeriveData{ +func (s *argon2SuiteExpensive) TestInProcessKDFDeriveDifferentKeyLen(c *C) { + s.testInProcessKDFDerive(c, &testInProcessArgon2KDFDeriveData{ passphrase: "foo", salt: []byte("0123456789abcdefghijklmnopqrstuv"), + mode: Argon2id, params: &Argon2CostParams{ Time: 4, MemoryKiB: 32, @@ -292,26 +398,26 @@ func (s *argon2SuiteExpensive) TestArgon2iKDFDeriveDifferentKeyLen(c *C) { keyLen: 64}) } -func (s *argon2SuiteExpensive) TestArgon2iKDFTime(c *C) { - time1, err := InProcessArgon2KDF.Time(&Argon2CostParams{Time: 4, MemoryKiB: 32 * 1024, Threads: 4}) +func (s *argon2SuiteExpensive) TestInProcessKDFTime(c *C) { + time1, err := InProcessArgon2KDF.Time(Argon2id, &Argon2CostParams{Time: 4, MemoryKiB: 32 * 1024, Threads: 4}) runtime.GC() c.Check(err, IsNil) - time2, err := InProcessArgon2KDF.Time(&Argon2CostParams{Time: 16, MemoryKiB: 32 * 1024, Threads: 4}) + time2, err := InProcessArgon2KDF.Time(Argon2id, &Argon2CostParams{Time: 16, MemoryKiB: 32 * 1024, Threads: 4}) runtime.GC() c.Check(err, IsNil) // XXX: this needs a checker like go-tpm2/testutil's IntGreater, which copes with // types of int64 kind c.Check(time2 > time1, testutil.IsTrue) - time2, err = InProcessArgon2KDF.Time(&Argon2CostParams{Time: 4, MemoryKiB: 128 * 1024, Threads: 4}) + time2, err = InProcessArgon2KDF.Time(Argon2id, &Argon2CostParams{Time: 4, MemoryKiB: 128 * 1024, Threads: 4}) runtime.GC() c.Check(err, IsNil) // XXX: this needs a checker like go-tpm2/testutil's IntGreater, which copes with // types of int64 kind c.Check(time2 > time1, testutil.IsTrue) - time2, err = InProcessArgon2KDF.Time(&Argon2CostParams{Time: 4, MemoryKiB: 32 * 1024, Threads: 1}) + time2, err = InProcessArgon2KDF.Time(Argon2id, &Argon2CostParams{Time: 4, MemoryKiB: 32 * 1024, Threads: 1}) runtime.GC() c.Check(err, IsNil) // XXX: this needs a checker like go-tpm2/testutil's IntGreater, which copes with diff --git a/export_test.go b/export_test.go index c2b4762f..86e2cf1c 100644 --- a/export_test.go +++ b/export_test.go @@ -31,10 +31,13 @@ var ( UnmarshalProtectedKeys = unmarshalProtectedKeys ) -type ProtectedKeys = protectedKeys +type ( + KdfParams = kdfParams + ProtectedKeys = protectedKeys +) -func (o *Argon2Options) DeriveCostParams(keyLen int) (*Argon2CostParams, error) { - return o.deriveCostParams(keyLen) +func (o *Argon2Options) KdfParams(keyLen uint32) (*KdfParams, error) { + return o.kdfParams(keyLen) } func MockLUKS2Activate(fn func(string, string, []byte, int) error) (restore func()) { diff --git a/internal/argon2/argon2.go b/internal/argon2/argon2.go index 0d50f224..ed4a144f 100644 --- a/internal/argon2/argon2.go +++ b/internal/argon2/argon2.go @@ -48,6 +48,28 @@ var ( benchmarkSalt = []byte("0123456789abcdefghijklmnopqrstuv") ) +// Mode describes an Argon2 mode. +type Mode string + +const ( + // ModeI is the data-independent mode of Argon2. + ModeI Mode = "argon2i" + + // ModeID is the hybrid mode of Argon2. + ModeID Mode = "argon2id" +) + +func (m Mode) keyFn() func([]byte, []byte, uint32, uint32, uint8, uint32) []byte { + switch m { + case ModeI: + return argon2.Key + case ModeID: + return argon2.IDKey + default: + panic("invalid mode") + } +} + // BenchmarkParams defines the parameters for benchmarking the Argon2 algorithm type BenchmarkParams struct { // MaxMemoryCostKiB sets the upper memory usage limit in KiB. The actual @@ -274,13 +296,14 @@ func (c *benchmarkContext) run(params *BenchmarkParams, keyFn KeyDurationFunc, s } // KeyDuration runs the key derivation with the built-in benchmarking values for the -// supplied set of cost parameters, and then returns the amount of time taken to execute. +// specified mode and supplied set of cost parameters, and then returns the amount +// of time taken to execute. // -// By design, this function consumes a lot of memory depending on the supplied parameters. -// It may be desirable to execute it in a short-lived utility process. -func KeyDuration(params *CostParams) time.Duration { +// By design, this function consumes a lot of memory depending on the supplied +// parameters. It may be desirable to execute it in a short-lived utility process. +func KeyDuration(mode Mode, params *CostParams) time.Duration { start := time.Now() - Key(benchmarkPassword, benchmarkSalt, params, benchmarkKeyLen) + Key(benchmarkPassword, benchmarkSalt, mode, params, benchmarkKeyLen) return time.Now().Sub(start) } @@ -321,12 +344,12 @@ func Benchmark(params *BenchmarkParams, keyFn KeyDurationFunc) (*CostParams, err } // Key derives a key of the desired length from the supplied passphrase and salt using the -// Argon2i algorithm with the supplied cost parameters. +// specified mode with the supplied cost parameters. // // By design, this function consumes a lot of memory depending on the supplied parameters. // It may be desirable to execute it in a short-lived utility process. // // This will panic if the time or threads cost parameter are zero. -func Key(passphrase string, salt []byte, params *CostParams, keyLen uint32) []byte { - return argon2.Key([]byte(passphrase), salt, params.Time, params.MemoryKiB, params.Threads, keyLen) +func Key(passphrase string, salt []byte, mode Mode, params *CostParams, keyLen uint32) []byte { + return mode.keyFn()([]byte(passphrase), salt, params.Time, params.MemoryKiB, params.Threads, keyLen) } diff --git a/internal/argon2/argon2_test.go b/internal/argon2/argon2_test.go index 9ec58ab3..556f2504 100644 --- a/internal/argon2/argon2_test.go +++ b/internal/argon2/argon2_test.go @@ -340,6 +340,7 @@ func (s *argon2SuiteExpensive) SetUpSuite(c *C) { } type testKeyData struct { + mode Mode passphrase string saltLen int params *CostParams @@ -359,13 +360,21 @@ func (s *argon2SuiteExpensive) testKey(c *C, data *testKeyData) { data.params.Threads = maxThreads } - key := Key(data.passphrase, salt, data.params, data.keyLen) - expectedKey := argon2.Key([]byte(data.passphrase), salt, data.params.Time, data.params.MemoryKiB, data.params.Threads, data.keyLen) + key := Key(data.passphrase, salt, data.mode, data.params, data.keyLen) + + var expectedKey []byte + switch data.mode { + case ModeI: + expectedKey = argon2.Key([]byte(data.passphrase), salt, data.params.Time, data.params.MemoryKiB, data.params.Threads, data.keyLen) + case ModeID: + expectedKey = argon2.IDKey([]byte(data.passphrase), salt, data.params.Time, data.params.MemoryKiB, data.params.Threads, data.keyLen) + } c.Check(key, DeepEquals, expectedKey) } func (s *argon2SuiteExpensive) TestKey1(c *C) { s.testKey(c, &testKeyData{ + mode: ModeI, passphrase: "ubuntu", saltLen: 16, params: &CostParams{ @@ -377,6 +386,7 @@ func (s *argon2SuiteExpensive) TestKey1(c *C) { func (s *argon2SuiteExpensive) TestKey2(c *C) { s.testKey(c, &testKeyData{ + mode: ModeI, passphrase: "bar", saltLen: 16, params: &CostParams{ @@ -388,6 +398,7 @@ func (s *argon2SuiteExpensive) TestKey2(c *C) { func (s *argon2SuiteExpensive) TestKey3(c *C) { s.testKey(c, &testKeyData{ + mode: ModeI, passphrase: "ubuntu", saltLen: 16, params: &CostParams{ @@ -399,6 +410,7 @@ func (s *argon2SuiteExpensive) TestKey3(c *C) { func (s *argon2SuiteExpensive) TestKey4(c *C) { s.testKey(c, &testKeyData{ + mode: ModeI, passphrase: "ubuntu", saltLen: 16, params: &CostParams{ @@ -410,6 +422,7 @@ func (s *argon2SuiteExpensive) TestKey4(c *C) { func (s *argon2SuiteExpensive) TestKey5(c *C) { s.testKey(c, &testKeyData{ + mode: ModeI, passphrase: "ubuntu", saltLen: 16, params: &CostParams{ @@ -421,6 +434,7 @@ func (s *argon2SuiteExpensive) TestKey5(c *C) { func (s *argon2SuiteExpensive) TestKey6(c *C) { s.testKey(c, &testKeyData{ + mode: ModeI, passphrase: "ubuntu", saltLen: 16, params: &CostParams{ @@ -430,23 +444,35 @@ func (s *argon2SuiteExpensive) TestKey6(c *C) { keyLen: 32}) } +func (s *argon2SuiteExpensive) TestKey7(c *C) { + s.testKey(c, &testKeyData{ + mode: ModeID, + passphrase: "ubuntu", + saltLen: 16, + params: &CostParams{ + Time: 4, + MemoryKiB: 32 * 1024, + Threads: 4}, + keyLen: 32}) +} + func (s *argon2SuiteExpensive) TestKeyDuration(c *C) { - time1 := KeyDuration(&CostParams{Time: 4, MemoryKiB: 32 * 1024, Threads: 4}) + time1 := KeyDuration(ModeID, &CostParams{Time: 4, MemoryKiB: 32 * 1024, Threads: 4}) runtime.GC() - time2 := KeyDuration(&CostParams{Time: 16, MemoryKiB: 32 * 1024, Threads: 4}) + time2 := KeyDuration(ModeID, &CostParams{Time: 16, MemoryKiB: 32 * 1024, Threads: 4}) runtime.GC() // XXX: this needs a checker like go-tpm2/testutil's IntGreater, which copes with // types of int64 kind c.Check(time2 > time1, testutil.IsTrue) - time2 = KeyDuration(&CostParams{Time: 4, MemoryKiB: 128 * 1024, Threads: 4}) + time2 = KeyDuration(ModeID, &CostParams{Time: 4, MemoryKiB: 128 * 1024, Threads: 4}) runtime.GC() // XXX: this needs a checker like go-tpm2/testutil's IntGreater, which copes with // types of int64 kind c.Check(time2 > time1, testutil.IsTrue) - time2 = KeyDuration(&CostParams{Time: 4, MemoryKiB: 32 * 1024, Threads: 1}) + time2 = KeyDuration(ModeID, &CostParams{Time: 4, MemoryKiB: 32 * 1024, Threads: 1}) runtime.GC() // XXX: this needs a checker like go-tpm2/testutil's IntGreater, which copes with // types of int64 kind diff --git a/internal/testutil/argon2.go b/internal/testutil/argon2.go index d3038f78..211b812c 100644 --- a/internal/testutil/argon2.go +++ b/internal/testutil/argon2.go @@ -23,6 +23,7 @@ import ( "crypto" _ "crypto/sha256" "encoding/binary" + "errors" "time" kdf "github.com/canonical/go-sp800.108-kdf" @@ -30,26 +31,43 @@ import ( "github.com/snapcore/secboot" ) -// MockArgon2KDF provides a mock implementation of secboot.Argon2KDF that isn't +// MockArgon2KDF provides a mock implementation of secboot.KDF that isn't // memory intensive. -type MockArgon2KDF struct{} +type MockArgon2KDF struct { + // BenchmarkMode is the mode that Time was last called with. Set this + // to Argon2Default before running a mock benchmark. + BenchmarkMode secboot.Argon2Mode +} // Derive implements secboot.KDF.Derive and derives a key from the supplied // passphrase and parameters. This is only intended for testing and is not // meant to be secure in any way. -func (_ *MockArgon2KDF) Derive(passphrase string, salt []byte, params *secboot.Argon2CostParams, keyLen uint32) ([]byte, error) { - context := make([]byte, len(salt)+9) +func (_ *MockArgon2KDF) Derive(passphrase string, salt []byte, mode secboot.Argon2Mode, params *secboot.Argon2CostParams, keyLen uint32) ([]byte, error) { + context := make([]byte, len(salt)+10) copy(context, salt) - binary.LittleEndian.PutUint32(context[len(salt):], params.Time) - binary.LittleEndian.PutUint32(context[len(salt)+4:], params.MemoryKiB) - context[len(salt)+8] = params.Threads + switch mode { + case secboot.Argon2i: + context[len(salt)] = 0 + case secboot.Argon2id: + context[len(salt)] = 1 + default: + return nil, errors.New("invalid mode") + } + binary.LittleEndian.PutUint32(context[len(salt)+1:], params.Time) + binary.LittleEndian.PutUint32(context[len(salt)+5:], params.MemoryKiB) + context[len(salt)+9] = params.Threads return kdf.CounterModeKey(kdf.NewHMACPRF(crypto.SHA256), []byte(passphrase), nil, context, keyLen*8), nil } // Time implements secboot.KDF.Time and returns a time that is linearly // related to the specified cost parameters, suitable for mocking benchmarking. -func (_ *MockArgon2KDF) Time(params *secboot.Argon2CostParams) (time.Duration, error) { +func (k *MockArgon2KDF) Time(mode secboot.Argon2Mode, params *secboot.Argon2CostParams) (time.Duration, error) { + if k.BenchmarkMode != secboot.Argon2Default && k.BenchmarkMode != mode { + return 0, errors.New("unexpected mode") + } + k.BenchmarkMode = mode + const memBandwidthKiBPerMs = 2048 duration := (time.Duration(float64(params.MemoryKiB)/float64(memBandwidthKiBPerMs)) * time.Duration(params.Time)) * time.Millisecond return duration, nil diff --git a/keydata.go b/keydata.go index 0bb9c373..281f5332 100644 --- a/keydata.go +++ b/keydata.go @@ -38,7 +38,6 @@ import ( ) const ( - kdfType = "argon2i" nilHash hashAlg = 0 passphraseKeyLen = 32 passphraseEncryptionKeyLen = 32 @@ -275,16 +274,20 @@ func (a hashAlg) marshalASN1(b *cryptobyte.Builder) { }) } -// kdfData corresponds to the arguments to a KDF and matches the -// corresponding object in the LUKS2 specification. -type kdfData struct { +type kdfParams struct { Type string `json:"type"` - Salt []byte `json:"salt"` Time int `json:"time"` Memory int `json:"memory"` CPUs int `json:"cpus"` } +// kdfData corresponds to the arguments to a KDF and matches the +// corresponding object in the LUKS2 specification. +type kdfData struct { + Salt []byte `json:"salt"` + kdfParams +} + // passphraseParams contains parameters for passphrase authentication. type passphraseParams struct { // KDF contains the key derivation parameters used to derive @@ -370,10 +373,6 @@ func (d *KeyData) derivePassphraseKeys(passphrase string) (key, iv, auth []byte, } params := d.data.PassphraseParams - if params.KDF.Type != kdfType { - // Only Argon2i is supported - return nil, nil, nil, fmt.Errorf("unexpected intermediate KDF type \"%s\"", params.KDF.Type) - } if params.DerivedKeySize < 0 { return nil, nil, nil, fmt.Errorf("invalid derived key size (%d bytes)", params.DerivedKeySize) } @@ -406,16 +405,24 @@ func (d *KeyData) derivePassphraseKeys(passphrase string) (key, iv, auth []byte, return nil, nil, nil, xerrors.Errorf("cannot serialize salt: %w", err) } - costParams := &Argon2CostParams{ - Time: uint32(params.KDF.Time), - MemoryKiB: uint32(params.KDF.Memory), - Threads: uint8(params.KDF.CPUs)} - derived, err := argon2KDF().Derive(passphrase, salt, costParams, uint32(params.DerivedKeySize)) - if err != nil { - return nil, nil, nil, xerrors.Errorf("cannot derive key from passphrase: %w", err) - } - if len(derived) != params.DerivedKeySize { - return nil, nil, nil, errors.New("KDF returned unexpected key length") + var derived []byte + + switch params.KDF.Type { + case string(Argon2i), string(Argon2id): + mode := Argon2Mode(params.KDF.Type) + costParams := &Argon2CostParams{ + Time: uint32(params.KDF.Time), + MemoryKiB: uint32(params.KDF.Memory), + Threads: uint8(params.KDF.CPUs)} + derived, err = argon2KDF().Derive(passphrase, salt, mode, costParams, uint32(params.DerivedKeySize)) + if err != nil { + return nil, nil, nil, xerrors.Errorf("cannot derive key from passphrase: %w", err) + } + if len(derived) != params.DerivedKeySize { + return nil, nil, nil, errors.New("KDF returned unexpected key length") + } + default: + return nil, nil, nil, fmt.Errorf("unexpected intermediate KDF type \"%s\"", params.KDF.Type) } key = make([]byte, params.EncryptionKeySize) @@ -743,7 +750,7 @@ func NewKeyDataWithPassphrase(params *KeyWithPassphraseParams, passphrase string kdfOptions = &defaultOptions } - costParams, err := kdfOptions.deriveCostParams(passphraseEncryptionKeyLen + aes.BlockSize) + kdfParams, err := kdfOptions.kdfParams(passphraseKeyLen) if err != nil { return nil, xerrors.Errorf("cannot derive KDF cost parameters: %w", err) } @@ -755,11 +762,8 @@ func NewKeyDataWithPassphrase(params *KeyWithPassphraseParams, passphrase string kd.data.PassphraseParams = &passphraseParams{ KDF: kdfData{ - Type: kdfType, - Salt: salt[:], - Time: int(costParams.Time), - Memory: int(costParams.MemoryKiB), - CPUs: int(costParams.Threads), + Salt: salt[:], + kdfParams: *kdfParams, }, Encryption: passphraseEncryption, DerivedKeySize: passphraseKeyLen, diff --git a/keydata_test.go b/keydata_test.go index ba585f41..03ba13a7 100644 --- a/keydata_test.go +++ b/keydata_test.go @@ -435,7 +435,7 @@ func (s *keyDataTestBase) checkKeyDataJSONDecodedAuthModePassphrase(c *C, j map[ kdfOpts = &def } - costParams, err := kdfOpts.DeriveCostParams(0) + kdfParams, err := kdfOpts.KdfParams(0) c.Assert(err, IsNil) s.checkKeyDataJSONCommon(c, j, &creationParams.KeyParams, nmodels) @@ -464,7 +464,7 @@ func (s *keyDataTestBase) checkKeyDataJSONDecodedAuthModePassphrase(c *C, j map[ str, ok := k["type"].(string) c.Check(ok, testutil.IsTrue) - c.Check(str, Equals, "argon2i") + c.Check(str, Equals, "argon2id") str, ok = k["salt"].(string) c.Check(ok, testutil.IsTrue) @@ -473,15 +473,15 @@ func (s *keyDataTestBase) checkKeyDataJSONDecodedAuthModePassphrase(c *C, j map[ time, ok := k["time"].(float64) c.Check(ok, testutil.IsTrue) - c.Check(time, Equals, float64(costParams.Time)) + c.Check(time, Equals, float64(kdfParams.Time)) memory, ok := k["memory"].(float64) c.Check(ok, testutil.IsTrue) - c.Check(memory, Equals, float64(costParams.MemoryKiB)) + c.Check(memory, Equals, float64(kdfParams.Memory)) cpus, ok := k["cpus"].(float64) c.Check(ok, testutil.IsTrue) - c.Check(cpus, Equals, float64(costParams.Threads)) + c.Check(cpus, Equals, float64(kdfParams.CPUs)) str, ok = j["encrypted_payload"].(string) c.Check(ok, testutil.IsTrue) @@ -511,7 +511,12 @@ func (s *keyDataTestBase) checkKeyDataJSONDecodedAuthModePassphrase(c *C, j map[ c.Assert(err, IsNil) var kdf testutil.MockArgon2KDF - derived, _ := kdf.Derive(passphrase, asnsalt, costParams, uint32(derivedKeySize)) + costParams := &Argon2CostParams{ + Time: uint32(kdfParams.Time), + MemoryKiB: uint32(kdfParams.Memory), + Threads: uint8(kdfParams.CPUs), + } + derived, _ := kdf.Derive(passphrase, asnsalt, Argon2Mode(kdfParams.Type), costParams, uint32(derivedKeySize)) key := make([]byte, int(encryptionKeySize)) @@ -1626,12 +1631,10 @@ func (s *keyDataSuite) TestKeyDataDerivePassphraseKeysExpectedInfoFields(c *C) { `"digest":"8sVvLZOkRD6RWjLFSp/pOPrKoibsr+VWyGhv4M2aph8="},` + `"hmacs":null}} `) - expectedKey, err := base64.StdEncoding.DecodeString("C058QWvAAc5sp6Ef2NeQwk0mJk8OS4wrcceYEruHXno=") - c.Check(err, IsNil) - expectedIV, err := base64.StdEncoding.DecodeString("x78OL7OTqRQfONsOb8yaPQ==") - c.Check(err, IsNil) - expectedAuth, err := base64.StdEncoding.DecodeString("+AdPOck2Ek8CyCVfSOV3eYClrQMiNqAri0Ra4Ldbohc=") - c.Check(err, IsNil) + + expectedKey := testutil.DecodeHexString(c, "89e97e7c427f54805a25c2bd1224865218aa5a985e5ac4c44fbc2c53b4bdfae2") + expectedIV := testutil.DecodeHexString(c, "b5835d62838a8bef63f37389ae782308") + expectedAuth := testutil.DecodeHexString(c, "2e46344ee30895da0d8e11cbb86bb67aeeccca0f6c6489009619593cca00722e") kd, err := ReadKeyData(&mockKeyDataReader{"foo", bytes.NewReader(j)}) c.Assert(err, IsNil)