diff --git a/argon2.go b/argon2.go index 20479ae5..93cba295 100644 --- a/argon2.go +++ b/argon2.go @@ -109,7 +109,7 @@ type Argon2Options struct { Parallel uint8 } -func (o *Argon2Options) kdfParams(keyLen uint32) (*kdfParams, error) { +func (o *Argon2Options) kdfParams(defaultTargetDuration time.Duration, keyLen uint32) (*kdfParams, error) { switch o.Mode { case Argon2Default, Argon2i, Argon2id: // ok @@ -159,7 +159,7 @@ func (o *Argon2Options) kdfParams(keyLen uint32) (*kdfParams, error) { default: benchmarkParams := &argon2.BenchmarkParams{ MaxMemoryCostKiB: 1 * 1024 * 1024, // the default maximum memory cost is 1GiB. - TargetDuration: 2 * time.Second, // the default target duration is 2s. + TargetDuration: defaultTargetDuration, } if o.MemoryKiB != 0 { @@ -187,7 +187,7 @@ func (o *Argon2Options) kdfParams(keyLen uint32) (*kdfParams, error) { MemoryKiB: params.MemoryKiB, ForceIterations: params.Time, Parallel: params.Threads} - return o.kdfParams(keyLen) + return o.kdfParams(defaultTargetDuration, keyLen) } } diff --git a/argon2_test.go b/argon2_test.go index ae8269b9..935b2533 100644 --- a/argon2_test.go +++ b/argon2_test.go @@ -73,7 +73,7 @@ var _ = Suite(&argon2Suite{}) func (s *argon2Suite) TestKDFParamsDefault(c *C) { var opts Argon2Options - params, err := opts.KdfParams(0) + params, err := opts.KdfParams(2*time.Second, 0) c.Assert(err, IsNil) c.Check(s.kdf.BenchmarkMode, Equals, Argon2id) @@ -85,10 +85,24 @@ func (s *argon2Suite) TestKDFParamsDefault(c *C) { }) } +func (s *argon2Suite) TestKDFParamsDefaultWithDifferentTargetDuration(c *C) { + var opts Argon2Options + params, err := opts.KdfParams(200*time.Millisecond, 32) + c.Assert(err, IsNil) + c.Check(s.kdf.BenchmarkMode, Equals, Argon2id) + + c.Check(params, DeepEquals, &KdfParams{ + Type: "argon2id", + Time: 4, + Memory: 102406, + CPUs: s.cpusAuto, + }) +} + func (s *argon2Suite) TestKDFParamsExplicitMode(c *C) { var opts Argon2Options opts.Mode = Argon2i - params, err := opts.KdfParams(0) + params, err := opts.KdfParams(2*time.Second, 0) c.Assert(err, IsNil) c.Check(s.kdf.BenchmarkMode, Equals, Argon2i) @@ -103,7 +117,7 @@ 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) + params, err := opts.KdfParams(2*time.Second, 32) c.Assert(err, IsNil) c.Check(s.kdf.BenchmarkMode, Equals, Argon2id) @@ -118,7 +132,7 @@ func (s *argon2Suite) TestKDFParamsTargetDuration(c *C) { func (s *argon2Suite) TestKDFParamsMemoryLimit(c *C) { var opts Argon2Options opts.MemoryKiB = 32 * 1024 - params, err := opts.KdfParams(0) + params, err := opts.KdfParams(2*time.Second, 0) c.Assert(err, IsNil) c.Check(s.kdf.BenchmarkMode, Equals, Argon2id) @@ -133,7 +147,7 @@ func (s *argon2Suite) TestKDFParamsMemoryLimit(c *C) { func (s *argon2Suite) TestKDFParamsForceBenchmarkedThreads(c *C) { var opts Argon2Options opts.Parallel = 1 - params, err := opts.KdfParams(0) + params, err := opts.KdfParams(2*time.Second, 0) c.Assert(err, IsNil) c.Check(s.kdf.BenchmarkMode, Equals, Argon2id) @@ -151,7 +165,7 @@ func (s *argon2Suite) TestKDFParamsForceIterations(c *C) { var opts Argon2Options opts.ForceIterations = 3 - params, err := opts.KdfParams(0) + params, err := opts.KdfParams(2*time.Second, 0) c.Assert(err, IsNil) c.Check(s.kdf.BenchmarkMode, Equals, Argon2Default) @@ -170,7 +184,7 @@ func (s *argon2Suite) TestKDFParamsForceMemory(c *C) { var opts Argon2Options opts.ForceIterations = 3 opts.MemoryKiB = 32 * 1024 - params, err := opts.KdfParams(0) + params, err := opts.KdfParams(2*time.Second, 0) c.Assert(err, IsNil) c.Check(s.kdf.BenchmarkMode, Equals, Argon2Default) @@ -188,7 +202,7 @@ func (s *argon2Suite) TestKDFParamsForceIterationsDifferentCPUNum(c *C) { var opts Argon2Options opts.ForceIterations = 3 - params, err := opts.KdfParams(0) + params, err := opts.KdfParams(2*time.Second, 0) c.Assert(err, IsNil) c.Check(s.kdf.BenchmarkMode, Equals, Argon2Default) @@ -207,7 +221,7 @@ func (s *argon2Suite) TestKDFParamsForceThreads(c *C) { var opts Argon2Options opts.ForceIterations = 3 opts.Parallel = 1 - params, err := opts.KdfParams(9) + params, err := opts.KdfParams(2*time.Second, 0) c.Assert(err, IsNil) c.Check(s.kdf.BenchmarkMode, Equals, Argon2Default) @@ -226,7 +240,7 @@ func (s *argon2Suite) TestKDFParamsForceThreadsGreatherThanCPUNum(c *C) { var opts Argon2Options opts.ForceIterations = 3 opts.Parallel = 8 - params, err := opts.KdfParams(0) + params, err := opts.KdfParams(2*time.Second, 0) c.Assert(err, IsNil) c.Check(s.kdf.BenchmarkMode, Equals, Argon2Default) @@ -241,7 +255,7 @@ func (s *argon2Suite) TestKDFParamsForceThreadsGreatherThanCPUNum(c *C) { func (s *argon2Suite) TestKDFParamsInvalidForceIterations(c *C) { var opts Argon2Options opts.ForceIterations = math.MaxUint32 - _, err := opts.KdfParams(0) + _, err := opts.KdfParams(2*time.Second, 0) c.Check(err, ErrorMatches, `invalid iterations count 4294967295`) } @@ -249,7 +263,7 @@ func (s *argon2Suite) TestKDFParamsInvalidMemoryKiB(c *C) { var opts Argon2Options opts.ForceIterations = 4 opts.MemoryKiB = math.MaxUint32 - _, err := opts.KdfParams(0) + _, err := opts.KdfParams(2*time.Second, 0) c.Check(err, ErrorMatches, `invalid memory cost 4294967295KiB`) } diff --git a/export_test.go b/export_test.go index 22383c5d..1bdb1bf5 100644 --- a/export_test.go +++ b/export_test.go @@ -42,16 +42,16 @@ type ( ProtectedKeys = protectedKeys ) -func KDFOptionsKdfParams(o KDFOptions, keyLen uint32) (*KdfParams, error) { - return o.kdfParams(keyLen) +func KDFOptionsKdfParams(opts KDFOptions, defaultTargetDuration time.Duration, keyLen uint32) (*KdfParams, error) { + return opts.kdfParams(defaultTargetDuration, keyLen) } -func (o *Argon2Options) KdfParams(keyLen uint32) (*KdfParams, error) { - return o.kdfParams(keyLen) +func (o *Argon2Options) KdfParams(defaultTargetDuration time.Duration, keyLen uint32) (*KdfParams, error) { + return o.kdfParams(defaultTargetDuration, keyLen) } -func (o *PBKDF2Options) KdfParams(keyLen uint32) (*KdfParams, error) { - return o.kdfParams(keyLen) +func (o *PBKDF2Options) KdfParams(defaultTargetDuration time.Duration, keyLen uint32) (*KdfParams, error) { + return o.kdfParams(defaultTargetDuration, keyLen) } func MockLUKS2Activate(fn func(string, string, []byte, int) error) (restore func()) { diff --git a/kdf.go b/kdf.go index 70fc9c6d..264fbc13 100644 --- a/kdf.go +++ b/kdf.go @@ -19,8 +19,10 @@ package secboot +import "time" + // KDFOptions is an interface for supplying options for different // key derivation functions type KDFOptions interface { - kdfParams(keyLen uint32) (*kdfParams, error) + kdfParams(defaultTargetDuration time.Duration, keyLen uint32) (*kdfParams, error) } diff --git a/keydata.go b/keydata.go index 14dabfc3..2fbf9f7f 100644 --- a/keydata.go +++ b/keydata.go @@ -30,6 +30,7 @@ import ( "fmt" "hash" "io" + "time" "github.com/snapcore/secboot/internal/pbkdf2" "golang.org/x/crypto/cryptobyte" @@ -125,6 +126,7 @@ type AuthMode uint8 const ( AuthModeNone AuthMode = iota AuthModePassphrase + AuthModePIN ) // KeyParams provides parameters required to create a new KeyData object. @@ -167,6 +169,15 @@ type KeyWithPassphraseParams struct { AuthKeySize int } +type KeyWithPINParams struct { + KeyParams + KDFOptions *PBKDF2Options // The PIN KDF options + + // AuthKeySize is the size of key to derive from the PIN for + // use by the platform implementation. + AuthKeySize int +} + // KeyID is the unique ID for a KeyData object. It is used to facilitate the // sharing of state between the early boot environment and OS runtime. type KeyID []byte @@ -306,6 +317,11 @@ type passphraseParams struct { AuthKeySize int `json:"auth_key_size"` // Size of auth key to derive from passphrase derived key } +type pinParams struct { + KDF kdfData `json:"kdf"` + AuthKeySize int `json:"auth_key_size"` +} + type keyData struct { // Generation is a number used to differentiate between different key formats. // i.e Gen1 keys are binary serialized and include a primary and an unlock key while @@ -340,6 +356,7 @@ type keyData struct { EncryptedPayload []byte `json:"encrypted_payload"` PassphraseParams *passphraseParams `json:"passphrase_params,omitempty"` + PINParams *pinParams `json:"pin_params,omitempty"` // AuthorizedSnapModels contains information about the Snap models // that have been authorized to access the data protected by this key. @@ -398,7 +415,9 @@ func (d *KeyData) derivePassphraseKeys(passphrase string) (key, iv, auth []byte, return nil, nil, nil, fmt.Errorf("unavailable leaf KDF digest algorithm %v", kdfAlg) } - // Include derivation parameters in the Argon2 salt in order to protect them + // Include derivation parameters in the KDF salt in order to protect them. + // Ideally the extra parameters would be part of Argon2's additional data, but + // the go package doesn't expose this. builder := cryptobyte.NewBuilder(nil) builder.AddASN1(cryptobyte_asn1.SEQUENCE, func(b *cryptobyte.Builder) { // SEQUENCE { b.AddASN1OctetString(params.KDF.Salt) // salt OCTET STRING @@ -471,6 +490,36 @@ func (d *KeyData) derivePassphraseKeys(passphrase string) (key, iv, auth []byte, return key, iv, auth, nil } +func (d *KeyData) derivePINAuthKey(pin PIN) ([]byte, error) { + if d.data.PINParams == nil { + return nil, errors.New("no PIN params") + } + + params := d.data.PINParams + if params.AuthKeySize < 0 { + return nil, fmt.Errorf("invalid auth key size (%d bytes)", params.AuthKeySize) + } + if params.KDF.Time < 0 { + return nil, fmt.Errorf("invalid KDF time (%d)", params.KDF.Time) + } + if params.KDF.Type != pbkdf2Type { + return nil, fmt.Errorf("unexpected KDF type \"%s\"", params.KDF.Type) + } + + pbkdfParams := &pbkdf2.Params{ + Iterations: uint(params.KDF.Time), + HashAlg: crypto.Hash(params.KDF.Hash), + } + if !pbkdfParams.HashAlg.Available() { + return nil, fmt.Errorf("unavailable pbkdf2 digest algorithm %v", pbkdfParams.HashAlg) + } + key, err := pbkdf2.Key(string(pin.Bytes()), params.KDF.Salt, pbkdfParams, uint(params.AuthKeySize)) + if err != nil { + return nil, xerrors.Errorf("cannot derive auth key from PIN: %w", err) + } + return key, nil +} + func (d *KeyData) updatePassphrase(payload, oldAuthKey []byte, passphrase string) error { handler := handlers[d.data.PlatformName] if handler == nil { @@ -506,6 +555,26 @@ func (d *KeyData) updatePassphrase(payload, oldAuthKey []byte, passphrase string return nil } +func (d *KeyData) updatePIN(oldAuthKey []byte, pin PIN) error { + handler := handlers[d.data.PlatformName] + if handler == nil { + return ErrNoPlatformHandlerRegistered + } + + authKey, err := d.derivePINAuthKey(pin) + if err != nil { + return err + } + + handle, err := handler.ChangeAuthKey(d.platformKeyData(), oldAuthKey, authKey) + if err != nil { + return err + } + + d.data.PlatformHandle = handle + return nil +} + func (d *KeyData) openWithPassphrase(passphrase string) (payload []byte, authKey []byte, err error) { key, iv, authKey, err := d.derivePassphraseKeys(passphrase) if err != nil { @@ -598,6 +667,8 @@ func (d *KeyData) AuthMode() (out AuthMode) { switch { case d.data.PassphraseParams != nil: return AuthModePassphrase + case d.data.PINParams != nil: + return AuthModePIN default: return AuthModeNone } @@ -687,6 +758,29 @@ func (d *KeyData) RecoverKeysWithPassphrase(passphrase string) (DiskUnlockKey, P return d.recoverKeysCommon(c) } +func (d *KeyData) RecoverKeysWithPIN(pin PIN) (DiskUnlockKey, PrimaryKey, error) { + if d.AuthMode() != AuthModePIN { + return nil, nil, errors.New("cannot recover key with PIN") + } + + handler := handlers[d.data.PlatformName] + if handler == nil { + return nil, nil, ErrNoPlatformHandlerRegistered + } + + key, err := d.derivePINAuthKey(pin) + if err != nil { + return nil, nil, err + } + + c, err := handler.RecoverKeysWithAuthKey(d.platformKeyData(), d.data.EncryptedPayload, key) + if err != nil { + return nil, nil, processPlatformHandlerError(err) + } + + return d.recoverKeysCommon(c) +} + // ChangePassphrase updates the passphrase used to recover the keys from this key data // via the KeyData.RecoverKeysWithPassphrase API. This can only be called if a passhphrase // has been set previously (KeyData.AuthMode returns AuthModePassphrase). @@ -709,6 +803,23 @@ func (d *KeyData) ChangePassphrase(oldPassphrase, newPassphrase string) error { return nil } +func (d *KeyData) ChangePIN(oldPIN, newPIN PIN) error { + if d.AuthMode()&AuthModePIN == 0 { + return errors.New("cannot change PIN without setting an initial PIN") + } + + oldKey, err := d.derivePINAuthKey(oldPIN) + if err != nil { + return err + } + + if err := d.updatePIN(oldKey, newPIN); err != nil { + return processPlatformHandlerError(err) + } + + return nil +} + // WriteAtomic saves this key data to the supplied KeyDataWriter. func (d *KeyData) WriteAtomic(w KeyDataWriter) error { enc := json.NewEncoder(w) @@ -775,7 +886,7 @@ func NewKeyDataWithPassphrase(params *KeyWithPassphraseParams, passphrase string kdfOptions = &defaultOptions } - kdfParams, err := kdfOptions.kdfParams(passphraseKeyLen) + kdfParams, err := kdfOptions.kdfParams(2*time.Second, passphraseKeyLen) if err != nil { return nil, xerrors.Errorf("cannot derive KDF cost parameters: %w", err) } @@ -803,6 +914,47 @@ func NewKeyDataWithPassphrase(params *KeyWithPassphraseParams, passphrase string return kd, nil } +func NewKeyDataWithPIN(params *KeyWithPINParams, pin PIN) (*KeyData, error) { + kd, err := NewKeyData(¶ms.KeyParams) + if err != nil { + return nil, err + } + + kdfOptions := params.KDFOptions + if kdfOptions == nil { + var defaultOptions PBKDF2Options + kdfOptions = &defaultOptions + } + + if params.AuthKeySize < 0 { + return nil, errors.New("invalid auth key size") + } + + kdfParams, err := kdfOptions.kdfParams(200*time.Millisecond, uint32(params.AuthKeySize)) + if err != nil { + return nil, xerrors.Errorf("cannot derive KDF cost parameters: %w", err) + } + + var salt [16]byte + if _, err := rand.Read(salt[:]); err != nil { + return nil, xerrors.Errorf("cannot read salt: %w", err) + } + + kd.data.PINParams = &pinParams{ + KDF: kdfData{ + Salt: salt[:], + kdfParams: *kdfParams, + }, + AuthKeySize: params.AuthKeySize, + } + + if err := kd.updatePIN(make([]byte, params.AuthKeySize), pin); err != nil { + return nil, xerrors.Errorf("cannot set PIN: %w", err) + } + + return kd, nil +} + // protectedKeys is used to pack a primary key and a unique value from which // an unlock key is derived. type protectedKeys struct { diff --git a/keydata_test.go b/keydata_test.go index 05bb39b6..f9de3f8c 100644 --- a/keydata_test.go +++ b/keydata_test.go @@ -461,7 +461,7 @@ func (s *keyDataTestBase) checkKeyDataJSONDecodedAuthModePassphrase(c *C, j map[ kdfOpts = &def } - kdfParams, err := KDFOptionsKdfParams(kdfOpts, 0) + kdfParams, err := KDFOptionsKdfParams(kdfOpts, 2*time.Second, 0) c.Assert(err, IsNil) s.checkKeyDataJSONCommon(c, j, &creationParams.KeyParams, nmodels) diff --git a/pbkdf2.go b/pbkdf2.go index 8b911d9e..9c5ab9fa 100644 --- a/pbkdf2.go +++ b/pbkdf2.go @@ -46,7 +46,7 @@ type PBKDF2Options struct { HashAlg crypto.Hash } -func (o *PBKDF2Options) kdfParams(keyLen uint32) (*kdfParams, error) { +func (o *PBKDF2Options) kdfParams(defaultTargetDuration time.Duration, keyLen uint32) (*kdfParams, error) { if keyLen > math.MaxInt32 { return nil, errors.New("invalid key length") } @@ -84,24 +84,24 @@ func (o *PBKDF2Options) kdfParams(keyLen uint32) (*kdfParams, error) { return params, nil default: - targetDuration := 2 * time.Second // the default target duration is 2s. - HashAlg := defaultHashAlg + targetDuration := defaultTargetDuration + hashAlg := defaultHashAlg if o.TargetDuration != 0 { targetDuration = o.TargetDuration } if o.HashAlg != crypto.Hash(0) { - HashAlg = o.HashAlg + hashAlg = o.HashAlg } - iterations, err := pbkdf2Benchmark(targetDuration, HashAlg) + iterations, err := pbkdf2Benchmark(targetDuration, hashAlg) if err != nil { return nil, xerrors.Errorf("cannot benchmark KDF: %w", err) } o = &PBKDF2Options{ ForceIterations: uint32(iterations), - HashAlg: HashAlg} - return o.kdfParams(keyLen) + HashAlg: hashAlg} + return o.kdfParams(defaultTargetDuration, keyLen) } } diff --git a/pbkdf2_test.go b/pbkdf2_test.go index 0ae0c007..8194850c 100644 --- a/pbkdf2_test.go +++ b/pbkdf2_test.go @@ -34,73 +34,111 @@ type pbkdf2Suite struct{} var _ = Suite(&pbkdf2Suite{}) func (s *pbkdf2Suite) TestKDFParamsDefault(c *C) { + var expectedTime int restore := MockPBKDF2Benchmark(func(targetDuration time.Duration, hashAlg crypto.Hash) (uint, error) { c.Check(targetDuration, Equals, 2*time.Second) c.Check(hashAlg, Equals, crypto.SHA256) - return pbkdf2.Benchmark(targetDuration, hashAlg) + iter, err := pbkdf2.Benchmark(targetDuration, hashAlg) + c.Check(err, IsNil) + expectedTime = int(iter) + return iter, err }) defer restore() var opts PBKDF2Options - params, err := opts.KdfParams(32) + params, err := opts.KdfParams(2*time.Second, 32) c.Assert(err, IsNil) c.Check(params.Type, Equals, "pbkdf2") + c.Check(params.Time, Equals, expectedTime) + c.Check(params.Hash, Equals, HashAlg(crypto.SHA256)) + c.Check(params.Memory, Equals, 0) + c.Check(params.CPUs, Equals, 0) +} + +func (s *pbkdf2Suite) TestKDFParamsDefaultWithDifferentTargetDuration(c *C) { + var expectedTime int + restore := MockPBKDF2Benchmark(func(targetDuration time.Duration, hashAlg crypto.Hash) (uint, error) { + c.Check(targetDuration, Equals, 200*time.Millisecond) + c.Check(hashAlg, Equals, crypto.SHA256) + iter, err := pbkdf2.Benchmark(targetDuration, hashAlg) + c.Check(err, IsNil) + expectedTime = int(iter) + return iter, err + }) + defer restore() + + var opts PBKDF2Options + params, err := opts.KdfParams(200*time.Millisecond, 32) + c.Assert(err, IsNil) + c.Check(params.Type, Equals, "pbkdf2") + c.Check(params.Time, Equals, expectedTime) c.Check(params.Hash, Equals, HashAlg(crypto.SHA256)) c.Check(params.Memory, Equals, 0) c.Check(params.CPUs, Equals, 0) } func (s *pbkdf2Suite) TestKDFParamsDefault48(c *C) { + var expectedTime int restore := MockPBKDF2Benchmark(func(targetDuration time.Duration, hashAlg crypto.Hash) (uint, error) { c.Check(targetDuration, Equals, 2*time.Second) c.Check(hashAlg, Equals, crypto.SHA384) - return pbkdf2.Benchmark(targetDuration, hashAlg) + iter, err := pbkdf2.Benchmark(targetDuration, hashAlg) + c.Check(err, IsNil) + expectedTime = int(iter) + return iter, err }) defer restore() var opts PBKDF2Options - params, err := opts.KdfParams(48) + params, err := opts.KdfParams(2*time.Second, 48) c.Assert(err, IsNil) c.Check(params.Type, Equals, "pbkdf2") + c.Check(params.Time, Equals, expectedTime) c.Check(params.Hash, Equals, HashAlg(crypto.SHA384)) c.Check(params.Memory, Equals, 0) c.Check(params.CPUs, Equals, 0) } func (s *pbkdf2Suite) TestKDFParamsDefault64(c *C) { + var expectedTime int restore := MockPBKDF2Benchmark(func(targetDuration time.Duration, hashAlg crypto.Hash) (uint, error) { c.Check(targetDuration, Equals, 2*time.Second) c.Check(hashAlg, Equals, crypto.SHA512) - return pbkdf2.Benchmark(targetDuration, hashAlg) + iter, err := pbkdf2.Benchmark(targetDuration, hashAlg) + c.Check(err, IsNil) + expectedTime = int(iter) + return iter, err }) defer restore() var opts PBKDF2Options - params, err := opts.KdfParams(64) + params, err := opts.KdfParams(2*time.Second, 64) c.Assert(err, IsNil) c.Check(params.Type, Equals, "pbkdf2") + c.Check(params.Time, Equals, expectedTime) c.Check(params.Hash, Equals, HashAlg(crypto.SHA512)) c.Check(params.Memory, Equals, 0) c.Check(params.CPUs, Equals, 0) } func (s *pbkdf2Suite) TestKDFParamsTargetDuration(c *C) { + var expectedTime int restore := MockPBKDF2Benchmark(func(targetDuration time.Duration, hashAlg crypto.Hash) (uint, error) { - c.Logf("benchmarking (%d)", targetDuration) - if targetDuration != 200*time.Millisecond { - panic("") - } c.Check(targetDuration, Equals, 200*time.Millisecond) c.Check(hashAlg, Equals, crypto.SHA256) - return pbkdf2.Benchmark(targetDuration, hashAlg) + iter, err := pbkdf2.Benchmark(targetDuration, hashAlg) + c.Check(err, IsNil) + expectedTime = int(iter) + return iter, err }) defer restore() var opts PBKDF2Options opts.TargetDuration = 200 * time.Millisecond - params, err := opts.KdfParams(32) + params, err := opts.KdfParams(2*time.Second, 32) c.Assert(err, IsNil) c.Check(params.Type, Equals, "pbkdf2") + c.Check(params.Time, Equals, expectedTime) c.Check(params.Hash, Equals, HashAlg(crypto.SHA256)) c.Check(params.Memory, Equals, 0) c.Check(params.CPUs, Equals, 0) @@ -109,7 +147,7 @@ func (s *pbkdf2Suite) TestKDFParamsTargetDuration(c *C) { func (s *pbkdf2Suite) TestKDFParamsForceIterations(c *C) { var opts PBKDF2Options opts.ForceIterations = 2000 - params, err := opts.KdfParams(32) + params, err := opts.KdfParams(2*time.Second, 32) c.Assert(err, IsNil) c.Check(params, DeepEquals, &KdfParams{ Type: "pbkdf2", @@ -119,18 +157,23 @@ func (s *pbkdf2Suite) TestKDFParamsForceIterations(c *C) { } func (s *pbkdf2Suite) TestKDFParamsCustomHash(c *C) { + var expectedTime int restore := MockPBKDF2Benchmark(func(targetDuration time.Duration, hashAlg crypto.Hash) (uint, error) { c.Check(targetDuration, Equals, 2*time.Second) c.Check(hashAlg, Equals, crypto.SHA512) - return pbkdf2.Benchmark(targetDuration, hashAlg) + iter, err := pbkdf2.Benchmark(targetDuration, hashAlg) + c.Check(err, IsNil) + expectedTime = int(iter) + return iter, err }) defer restore() var opts PBKDF2Options opts.HashAlg = crypto.SHA512 - params, err := opts.KdfParams(32) + params, err := opts.KdfParams(2*time.Second, 32) c.Assert(err, IsNil) c.Check(params.Type, Equals, "pbkdf2") + c.Check(params.Time, Equals, expectedTime) c.Check(params.Hash, Equals, HashAlg(crypto.SHA512)) c.Check(params.Memory, Equals, 0) c.Check(params.CPUs, Equals, 0) diff --git a/pin.go b/pin.go new file mode 100644 index 00000000..ce711fa1 --- /dev/null +++ b/pin.go @@ -0,0 +1,72 @@ +// -*- 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 . + * + */ + +package secboot + +import ( + "errors" + "fmt" + "math" + "math/big" +) + +// PIN represents a numeric PIN. +type PIN struct { + length uint8 // the length of the input PIN. This is *not* the length of the encoded binary number + value big.Int // the PIN value. This is encoded in big-endian form without leading zeroes. +} + +// ParsePIN parses the supplied string and returns a PIN. If the supplied +// string is larger than 255 characters or contains anything other than +// base-10 digits, an error will be returned. +func ParsePIN(s string) (PIN, error) { + l := len(s) + if l > math.MaxUint8 { + return PIN{}, errors.New("invalid PIN: too long") + } + + val, ok := new(big.Int).SetString(s, 10) + if !ok { + return PIN{}, errors.New("invalid PIN") + } + + return PIN{ + length: uint8(l), + value: *val, + }, nil +} + +// String implements [fmt.Stringer]. +func (p PIN) String() string { + return fmt.Sprintf("%0*s", p.length, p.value.String()) +} + +// Bytes provides a binary representation of this PIN. The binary representation +// is length prefixed with a single byte, which represents the length of the +// original input string, followed by the binary representation of the base-10 PIN +// in big-endian form and represented by the minimum number of bytes. +func (p PIN) Bytes() []byte { + maxS := make([]byte, p.length) + for i := range maxS { + maxS[i] = '9' + } + max, _ := new(big.Int).SetString(string(maxS), 10) + b := make([]byte, len(max.Bytes())) + return append([]byte{p.length}, p.value.FillBytes(b)...) +} diff --git a/pin_test.go b/pin_test.go new file mode 100644 index 00000000..a254e66e --- /dev/null +++ b/pin_test.go @@ -0,0 +1,101 @@ +package secboot_test + +import ( + . "github.com/snapcore/secboot" + "github.com/snapcore/secboot/internal/testutil" + + . "gopkg.in/check.v1" +) + +type pinSuite struct{} + +var _ = Suite(&pinSuite{}) + +func (s *pinSuite) TestPIN(c *C) { + pin, err := ParsePIN("1234") + c.Assert(err, IsNil) + + c.Check(pin.String(), Equals, "1234") + c.Check(pin.Bytes(), DeepEquals, testutil.DecodeHexString(c, "0404d2")) +} + +func (s *pinSuite) TestPINZeroPaddedIsDifferent(c *C) { + pin, err := ParsePIN("00001234") + c.Assert(err, IsNil) + + c.Check(pin.String(), Equals, "00001234") + c.Check(pin.Bytes(), DeepEquals, testutil.DecodeHexString(c, "08000004d2")) +} + +func (s *pinSuite) TestPIN2(c *C) { + pin, err := ParsePIN("12345678") + c.Assert(err, IsNil) + + c.Check(pin.String(), Equals, "12345678") + c.Check(pin.Bytes(), DeepEquals, testutil.DecodeHexString(c, "0800bc614e")) +} + +func (s *pinSuite) TestPIN3(c *C) { + pin, err := ParsePIN("00000000") + c.Assert(err, IsNil) + + c.Check(pin.String(), Equals, "00000000") + c.Check(pin.Bytes(), DeepEquals, testutil.DecodeHexString(c, "0800000000")) +} + +func (s *pinSuite) TestPIN4(c *C) { + pin, err := ParsePIN("99999999") + c.Assert(err, IsNil) + + c.Check(pin.String(), Equals, "99999999") + c.Check(pin.Bytes(), DeepEquals, testutil.DecodeHexString(c, "0805f5e0ff")) +} + +func (s *pinSuite) TestPIN5(c *C) { + pin, err := ParsePIN("246813") + c.Assert(err, IsNil) + + c.Check(pin.String(), Equals, "246813") + c.Check(pin.Bytes(), DeepEquals, testutil.DecodeHexString(c, "0603c41d")) +} + +func (s *pinSuite) TestPINLongest(c *C) { + pin, err := ParsePIN("1234567812345678123456781234567812345678123456781234567812345678" + + "12345678123456781234567812345678123456781234567812345678123456781234567812345678" + + "12345678123456781234567812345678123456781234567812345678123456781234567812345678" + + "1234567812345678123456781234567") + c.Assert(err, IsNil) + + c.Check(pin.String(), Equals, "1234567812345678123456781234567812345678123456781234567812345678"+ + "12345678123456781234567812345678123456781234567812345678123456781234567812345678"+ + "12345678123456781234567812345678123456781234567812345678123456781234567812345678"+ + "1234567812345678123456781234567") + c.Check(pin.Bytes(), DeepEquals, testutil.DecodeHexString(c, "ff10d6ce8940392078ffd0aa1ced339ebd632df03586ebc7a964a198aa06dfecf4417552290933dd874c2e00f55ea5ba5c1d4bea13735a8c5fc9edfbdb473a2df4dda455f0c098d6c0d592a7cb42a5383e7b9a34b3d5b8ccde89851ecf645becf69d528a2af48c8b923187")) +} + +func (s *pinSuite) TestPINMax(c *C) { + pin, err := ParsePIN("9999999999999999999999999999999999999999999999999999999999999999" + + "99999999999999999999999999999999999999999999999999999999999999999999999999999999" + + "99999999999999999999999999999999999999999999999999999999999999999999999999999999" + + "9999999999999999999999999999999") + c.Assert(err, IsNil) + + c.Check(pin.String(), Equals, "9999999999999999999999999999999999999999999999999999999999999999"+ + "99999999999999999999999999999999999999999999999999999999999999999999999999999999"+ + "99999999999999999999999999999999999999999999999999999999999999999999999999999999"+ + "9999999999999999999999999999999") + c.Check(pin.Bytes(), DeepEquals, testutil.DecodeHexString(c, "ff8865899617fb18717e2fa67c7a658892d0e50a3297e8c7a2252cd6ccbb9b0606aebc361bb89d4493d7119d783e8b155bc8ce61877171a4630813ce9bb7f3fc15c32513152722c26b0c667fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff")) +} + +func (s *pinSuite) TestPINTooLong(c *C) { + _, err := ParsePIN("1234567812345678123456781234567812345678123456781234567812345678" + + "12345678123456781234567812345678123456781234567812345678123456781234567812345678" + + "12345678123456781234567812345678123456781234567812345678123456781234567812345678" + + "12345678123456781234567812345678") + c.Check(err, ErrorMatches, `invalid PIN: too long`) +} + +func (s *pinSuite) TestPINInvalidChars(c *C) { + _, err := ParsePIN("1234abc") + c.Check(err, ErrorMatches, `invalid PIN`) +}