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 PBKDF2 for passphrases #288

Merged
merged 2 commits into from
Sep 19, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
16 changes: 16 additions & 0 deletions argon2_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"math"
"os"
"runtime"
"time"

snapd_testutil "github.com/snapcore/snapd/testutil"

Expand Down Expand Up @@ -99,6 +100,21 @@ func (s *argon2Suite) TestKDFParamsExplicitMode(c *C) {
})
}

func (s *argon2Suite) TestKDFParamsTargetDuration(c *C) {
var opts Argon2Options
opts.TargetDuration = 1 * time.Second
params, err := opts.KdfParams(32)
c.Assert(err, IsNil)
c.Check(s.kdf.BenchmarkMode, Equals, Argon2id)

c.Check(params, DeepEquals, &KdfParams{
Type: "argon2id",
Time: 4,
Memory: 512031,
CPUs: s.cpusAuto,
})
}

func (s *argon2Suite) TestKDFParamsMemoryLimit(c *C) {
var opts Argon2Options
opts.MemoryKiB = 32 * 1024
Expand Down
6 changes: 4 additions & 2 deletions crypt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -409,9 +409,11 @@ func (s *cryptSuite) SetUpTest(c *C) {
activated: make(map[string]string)}

s.AddCleanup(s.luks2.enableMocks())
}

origKdf := SetArgon2KDF(&testutil.MockArgon2KDF{})
s.AddCleanup(func() { SetArgon2KDF(origKdf) })
func (s *cryptSuite) TearDownTest(c *C) {
s.keyDataTestBase.TearDownTest(c)
s.KeyringTestBase.TearDownTest(c)
}

func (s *cryptSuite) addMockToken(path string, token luks2.Token) int {
Expand Down
22 changes: 22 additions & 0 deletions export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,18 @@
package secboot

import (
"crypto"
"io"
"time"

"github.com/snapcore/secboot/internal/luks2"
"github.com/snapcore/secboot/internal/luksview"
)

const (
NilHash = nilHash
)

var (
UnmarshalV1KeyPayload = unmarshalV1KeyPayload
UnmarshalProtectedKeys = unmarshalProtectedKeys
Expand All @@ -36,10 +42,18 @@ type (
ProtectedKeys = protectedKeys
)

func KDFOptionsKdfParams(o KDFOptions, keyLen uint32) (*KdfParams, error) {
return o.kdfParams(keyLen)
}

func (o *Argon2Options) KdfParams(keyLen uint32) (*KdfParams, error) {
return o.kdfParams(keyLen)
}

func (o *PBKDF2Options) KdfParams(keyLen uint32) (*KdfParams, error) {
return o.kdfParams(keyLen)
}

func MockLUKS2Activate(fn func(string, string, []byte, int) error) (restore func()) {
origActivate := luks2Activate
luks2Activate = fn
Expand Down Expand Up @@ -112,6 +126,14 @@ func MockNewLUKSView(fn func(string, luks2.LockMode) (*luksview.View, error)) (r
}
}

func MockPBKDF2Benchmark(fn func(time.Duration, crypto.Hash) (uint, error)) (restore func()) {
orig := pbkdf2Benchmark
pbkdf2Benchmark = fn
return func() {
pbkdf2Benchmark = orig
}
}

func MockRuntimeNumCPU(n int) (restore func()) {
orig := runtimeNumCPU
runtimeNumCPU = func() int {
Expand Down
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ require (
github.com/canonical/tcglog-parser v0.0.0-20240820013904-60cf7cbc7c5d
github.com/intel-go/cpuid v0.0.0-20220614022739-219e067757cb
github.com/snapcore/snapd v0.0.0-20220714152900-4a1f4c93fc85
golang.org/x/crypto v0.9.0
golang.org/x/crypto v0.21.0
golang.org/x/sys v0.19.0
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c
Expand All @@ -24,6 +24,6 @@ require (
github.com/kr/text v0.1.0 // indirect
github.com/snapcore/go-gettext v0.0.0-20191107141714-82bbea49e785 // indirect
golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f // indirect
golang.org/x/net v0.10.0 // indirect
golang.org/x/net v0.21.0 // indirect
gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637 // indirect
)
8 changes: 4 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,14 @@ github.com/snapcore/snapd v0.0.0-20220714152900-4a1f4c93fc85/go.mod h1:Ab4TsNgVa
go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f h1:99ci1mjWVBWwJiEKYY6jWa4d2nTQVIEhZIptnrVb1XY=
golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20201002202402-0a1ea396d57c/go.mod h1:iQL9McJNjoIa5mjH6nYTCTZXUN6RP+XW3eib7Ya3XcI=
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
Expand Down
30 changes: 30 additions & 0 deletions internal/pbkdf2/export_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// -*- Mode: Go; indent-tabs-mode: t -*-

/*
* Copyright (C) 2024 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

package pbkdf2

import "time"

func MockTimeExecution(fn func(*Params) time.Duration) (restore func()) {
orig := timeExecution
timeExecution = fn
return func() {
timeExecution = orig
}
}
145 changes: 145 additions & 0 deletions internal/pbkdf2/pbkdf2.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
// -*- Mode: Go; indent-tabs-mode: t -*-

/*
* Copyright (C) 2024 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

package pbkdf2

import (
"crypto"
"errors"
"math"
"time"

"golang.org/x/crypto/pbkdf2"
)

const (
benchmarkPassword = "foo"
)

var (
benchmarkSalt = []byte("0123456789abcdefghijklmnopqrstuv")
)

var timeExecution = func(params *Params) time.Duration {
start := time.Now()
if _, err := Key(benchmarkPassword, benchmarkSalt, params, uint(params.HashAlg.Size())); err != nil {
panic(err)
}
return time.Now().Sub(start)
}
Comment on lines +39 to +45
Copy link
Collaborator

Choose a reason for hiding this comment

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

this bit of code in itself is not reached by tests

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I've added a test that doesn't mock this now.


// Benchmark computes the number of iterations for desired duration
// with the specified digest algorithm. The specified algorithm must
// be available. This benchmark is largely based on that implemented
// by cryptsetup.
//
// When producing keys that are larger than the output size of the
// digest algorithm, PBKDF2 runs the specified number of iterations
// multiple times - eg, to produce a 64-byte key with SHA-256, PBKDF2
// runs the specified number of iterations twice to produce the key in
// 2 rounds and this takes twice as long as it takes to produce a
// 32-byte key. This runs the benchmark for a single round by selecting a
// key length that is the same size as the output of the digest algorithm,
// which means that if SHA-256 is selected with a target duration of 1 second
// and the result is subsequently used to derive a 64-byte key, it will take 2
// seconds. This is safer than the alternative which is that all rounds are
// benchmarked (eg, using SHA-256 to produce a 64-byte key) for 1 second, and
// then it's subsequently possible to run a single round in order to produce
// 32-bytes of output key material in 500ms.
func Benchmark(targetDuration time.Duration, hashAlg crypto.Hash) (uint, error) {
if !hashAlg.Available() {
return 0, errors.New("unavailable digest algorithm")
}

// Start with 1000 iterations.
iterationsOut := uint(1000)
iterations := iterationsOut

for i := 1; ; i++ {
// time the key derivation
duration := timeExecution(&Params{Iterations: iterations, HashAlg: hashAlg})
if duration > 0 {
// calculate the required number of iterations to return, based on the tested
// iterations, measured duration and target duration.
newIterationsOut := (int64(iterations) * int64(targetDuration)) / int64(duration)
if newIterationsOut > math.MaxInt {
return 0, errors.New("iteration count result overflow")
}
iterationsOut = uint(newIterationsOut)
}

// scale up the number of iterations to test next
var scale uint
switch {
case i > 10:
return 0, errors.New("insufficient progress")
case duration > 500*time.Millisecond:
return iterationsOut, nil
case duration <= 62*time.Millisecond:
scale = 16
case duration <= 125*time.Millisecond:
scale = 8
case duration <= 250*time.Millisecond:
scale = 4
default:
scale = 2
}
if math.MaxInt/scale < iterations {
// It's only possible to hit this on 32-bit platforms. On
// 64-bit platforms, we'll always hit the "insufficient progress"
// branch first.
return 0, errors.New("test iteration count overflow")
}
iterations *= scale
if int64(iterations) > math.MaxInt64/int64(targetDuration) {
return 0, errors.New("iteration count result will overflow")
}
}
}

// Params are the key derivation parameters for PBKDF2.
type Params struct {
// Iterations are the number of iterations. This can't be
// greater than math.MaxInt.
Iterations uint

// HashAlg is the digest algorithm to use. The algorithm
// must be available
HashAlg crypto.Hash
}

// Key derives a key of the desired length from the supplied passphrase and salt,
// using the supplied parameters.
//
// This will return an error if the key length or number of iterations are greater
// than the maximum value of a signed integer, or the supplied digest algorithm is
// not available.
func Key(passphrase string, salt []byte, params *Params, keyLen uint) ([]byte, error) {
switch {
case params == nil:
return nil, errors.New("nil params")
case params.Iterations > math.MaxInt:
return nil, errors.New("too many iterations")
case !params.HashAlg.Available():
return nil, errors.New("unavailable digest algorithm")
case keyLen > math.MaxInt:
return nil, errors.New("invalid key length")
Comment on lines +139 to +142
Copy link
Collaborator

Choose a reason for hiding this comment

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

isn't there anything to check between keyLen and HashAlg?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

There isn't really any need to do that - one could use any hash algorithm to produce a key of any length, eg, if you request a key of 64 bytes with SHA-256 as the digest and 10000 iterations, it runs 10000 iterations twice to produce 2 blocks of 32-bytes and there's nothing in the design of PBKDF2 that disallows this.

}
return pbkdf2.Key([]byte(passphrase), salt, int(params.Iterations), int(keyLen), params.HashAlg.New), nil
}
Loading
Loading