From 1d0d689a986ca8d1f1951d4f2f40f4c0ba191eb1 Mon Sep 17 00:00:00 2001 From: Chris Coulson Date: Sat, 9 Mar 2024 03:02:11 +0000 Subject: [PATCH] [WIP] add PIN support to KeyData This adds PIN support to KeyData which is distinct from the existing passphrase support. Passphrases are used both for authentication with the hardware element (such as the TPM) and for additional encryption on the host CPU with a passphrase derived key, and are intended to use a memory hard key derivation. The intention here is that this configuration provides some additional protection in the event of a TPM compromise (eg, say a TPM manufacturer is coerced by a government agency to provide firmware that bypasses authentications), where sensitive data is able to be extracted without the usual authentication, because extracting the secret from the TPM will not be sufficient to obtain all of the key material necessary to unlock a device. PINs (in the literal sense) have a fairly low entropy - an 8 digit PIN only has an entropy of 26.5bits, so this additional encryption will provide little protection in the event of a TPM compromise - if sensitive data is obtained from the TPM, the 26.5bits of entropy won't provide a significant barrier to deriving the remaining key material necessary to unlock a device. We take advantage of this by implementing distinct PIN support that is only used for authentication. With this in mind, the memory hard key derivation does not provide a lot of benefit, so PINs only support PBKDF2, and it can be configured to run faster than the key derivation for passphrases. In that sense, PIN support is essentially just a faster and slightly weaker passphrase. As the PIN is a PIN in the literal sense, it is encoded as a length prefixed binary number before going through the key derivation. This only implements the support to KeyData for now - unlocking support will be added in another PR. --- argon2.go | 6 +- argon2_test.go | 38 ++++++++---- export_test.go | 12 ++-- kdf.go | 4 +- keydata.go | 156 +++++++++++++++++++++++++++++++++++++++++++++++- keydata_test.go | 2 +- pbkdf2.go | 14 ++--- pbkdf2_test.go | 34 +++++++++-- pin.go | 72 ++++++++++++++++++++++ pin_test.go | 101 +++++++++++++++++++++++++++++++ 10 files changed, 401 insertions(+), 38 deletions(-) create mode 100644 pin.go create mode 100644 pin_test.go 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 b61a0afa..8194850c 100644 --- a/pbkdf2_test.go +++ b/pbkdf2_test.go @@ -46,7 +46,29 @@ func (s *pbkdf2Suite) TestKDFParamsDefault(c *C) { 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) @@ -68,7 +90,7 @@ func (s *pbkdf2Suite) TestKDFParamsDefault48(c *C) { 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) @@ -90,7 +112,7 @@ func (s *pbkdf2Suite) TestKDFParamsDefault64(c *C) { 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) @@ -113,7 +135,7 @@ func (s *pbkdf2Suite) TestKDFParamsTargetDuration(c *C) { 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) @@ -125,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", @@ -148,7 +170,7 @@ func (s *pbkdf2Suite) TestKDFParamsCustomHash(c *C) { 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) 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`) +}