From 80c464ffcb235f1894776ac79e952679313a6420 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 | 8 +-- kdf.go | 4 +- keydata.go | 156 +++++++++++++++++++++++++++++++++++++++++++++++- keydata_test.go | 2 +- pbkdf2.go | 6 +- pbkdf2_test.go | 19 ++++-- pin.go | 63 +++++++++++++++++++ pin_test.go | 101 +++++++++++++++++++++++++++++++ 10 files changed, 371 insertions(+), 32 deletions(-) create mode 100644 pin.go create mode 100644 pin_test.go diff --git a/argon2.go b/argon2.go index dc5f4429..0ddf801c 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 016957c7..01ef4a3a 100644 --- a/argon2_test.go +++ b/argon2_test.go @@ -75,7 +75,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) @@ -87,10 +87,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) @@ -105,7 +119,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) @@ -120,7 +134,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) @@ -135,7 +149,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) @@ -153,7 +167,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) @@ -172,7 +186,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) @@ -190,7 +204,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) @@ -209,7 +223,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) @@ -228,7 +242,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) @@ -243,7 +257,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`) } @@ -251,7 +265,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 0a5e6a6f..1030bb76 100644 --- a/export_test.go +++ b/export_test.go @@ -43,12 +43,12 @@ type ( ProtectedKeys = protectedKeys ) -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 33554f48..38cb909c 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 @@ -302,6 +313,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 @@ -336,6 +352,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. @@ -394,7 +411,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 @@ -467,6 +486,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 { @@ -502,6 +551,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 { @@ -594,6 +663,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 } @@ -683,6 +754,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). @@ -705,6 +799,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) @@ -771,7 +882,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) } @@ -799,6 +910,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 03ba13a7..54ea1559 100644 --- a/keydata_test.go +++ b/keydata_test.go @@ -435,7 +435,7 @@ func (s *keyDataTestBase) checkKeyDataJSONDecodedAuthModePassphrase(c *C, j map[ kdfOpts = &def } - kdfParams, err := kdfOpts.KdfParams(0) + kdfParams, err := kdfOpts.KdfParams(2*time.Second, 0) c.Assert(err, IsNil) s.checkKeyDataJSONCommon(c, j, &creationParams.KeyParams, nmodels) diff --git a/pbkdf2.go b/pbkdf2.go index 72aa5bb4..026b4933 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,7 +84,7 @@ func (o *PBKDF2Options) kdfParams(keyLen uint32) (*kdfParams, error) { return params, nil default: - targetDuration := 2 * time.Second // the default target duration is 2s. + targetDuration := defaultTargetDuration hashAlg := defaultHashAlg if o.TargetDuration != 0 { @@ -102,6 +102,6 @@ func (o *PBKDF2Options) kdfParams(keyLen uint32) (*kdfParams, error) { o = &PBKDF2Options{ ForceIterations: uint32(iterations), HashAlg: hashAlg} - return o.kdfParams(keyLen) + return o.kdfParams(defaultTargetDuration, keyLen) } } diff --git a/pbkdf2_test.go b/pbkdf2_test.go index 7ec1479c..1ec7aca6 100644 --- a/pbkdf2_test.go +++ b/pbkdf2_test.go @@ -67,7 +67,14 @@ 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) + s.checkParams(c, &opts, 32, params) +} + +func (s *pbkdf2Suite) TestKDFParamsDefaultWithDifferentTargetDuration(c *C) { + var opts PBKDF2Options + params, err := opts.KdfParams(200*time.Millisecond, 32) c.Assert(err, IsNil) c.Check(params.Type, Equals, "pbkdf2") c.Check(params.Hash, Equals, HashAlg(crypto.SHA256)) @@ -84,7 +91,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.Hash, Equals, HashAlg(crypto.SHA384)) @@ -101,7 +108,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.Hash, Equals, HashAlg(crypto.SHA512)) @@ -119,7 +126,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.Hash, Equals, HashAlg(crypto.SHA256)) @@ -130,7 +137,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", @@ -149,7 +156,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.Hash, Equals, HashAlg(crypto.SHA512)) diff --git a/pin.go b/pin.go new file mode 100644 index 00000000..e4a16e0b --- /dev/null +++ b/pin.go @@ -0,0 +1,63 @@ +// -*- 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" +) + +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. +} + +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 +} + +func (p PIN) String() string { + return fmt.Sprintf("%0*s", p.length, p.value.String()) +} + +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`) +}