From e9b2ca2eacf759f994dd940c0f069c046c1978f7 Mon Sep 17 00:00:00 2001 From: Michele Balistreri Date: Tue, 2 Aug 2022 12:18:52 +0200 Subject: [PATCH] add support for metadata --- README.md | 8 +- apdu/utils.go | 52 ++++++--- apdu/utils_test.go | 4 +- command_set.go | 22 +++- commands.go | 25 ++++ derivationpath/decoder.go | 4 +- derivationpath/encoder.go | 6 +- globalplatform/commands_test.go | 2 +- secure_channel.go | 32 +++-- types/metadata.go | 201 ++++++++++++++++++++++++++++++++ types/metadata_test.go | 20 ++++ 11 files changed, 335 insertions(+), 41 deletions(-) create mode 100644 types/metadata.go create mode 100644 types/metadata_test.go diff --git a/README.md b/README.md index 5da0bf1..c8e3436 100644 --- a/README.md +++ b/README.md @@ -13,17 +13,17 @@ If you only need a tool to initialize your card, check out [keycard-cli](https:/ - [x] PAIR - [x] UNPAIR - [x] GET STATUS -- [ ] SET NDEF +- [x] STORE DATA +- [x] GET DATA - [x] VERIFY PIN - [x] CHANGE PIN -- [ ] UNBLOCK PIN -- [ ] LOAD KEY +- [x] UNBLOCK PIN +- [x] LOAD KEY - [x] DERIVE KEY - [x] GENERATE MNEMONIC - [x] REMOVE KEY - [x] GENERATE KEY - [x] INIT -- [ ] DUPLICATE KEY - [x] SIGN - [ ] SET PINLESS PATH - [x] EXPORT KEY diff --git a/apdu/utils.go b/apdu/utils.go index f0a9394..5bd4c7a 100644 --- a/apdu/utils.go +++ b/apdu/utils.go @@ -50,7 +50,7 @@ func findTag(raw []byte, occurrence int, tags ...Tag) ([]byte, error) { ) for { - tag, buf, err = parseTag(buf) + tag, err = parseTag(buf) switch { case err == io.EOF: return []byte{}, &ErrTagNotFound{target} @@ -58,7 +58,7 @@ func findTag(raw []byte, occurrence int, tags ...Tag) ([]byte, error) { return nil, err } - length, buf, err = parseLength(buf) + length, err = ParseLength(buf) if err != nil { return nil, err } @@ -87,59 +87,83 @@ func findTag(raw []byte, occurrence int, tags ...Tag) ([]byte, error) { } } -func parseLength(buf *bytes.Buffer) (uint32, *bytes.Buffer, error) { +func ParseLength(buf *bytes.Buffer) (uint32, error) { length, err := buf.ReadByte() if err != nil { - return 0, nil, err + return 0, err } if length == 0x80 { - return 0, nil, ErrUnsupportedLenth80 + return 0, ErrUnsupportedLenth80 } if length > 0x80 { lengthSize := length - 0x80 if lengthSize > 3 { - return 0, nil, ErrLengthTooBig + return 0, ErrLengthTooBig } data := make([]byte, lengthSize) _, err = buf.Read(data) if err != nil { - return 0, nil, err + return 0, err } num := make([]byte, 4) copy(num[4-lengthSize:], data) - return binary.BigEndian.Uint32(num), buf, nil + return binary.BigEndian.Uint32(num), nil } - return uint32(length), buf, nil + return uint32(length), nil } -func parseTag(buf *bytes.Buffer) (Tag, *bytes.Buffer, error) { +func WriteLength(buf *bytes.Buffer, length uint32) { + if length < 0x80 { + buf.WriteByte(byte(length)) + } else if length < 0x100 { + buf.WriteByte(0x81) + buf.WriteByte(byte(length)) + } else if length < 0x10000 { + buf.WriteByte(0x82) + buf.WriteByte(byte(length >> 8)) + buf.WriteByte(byte(length)) + } else if length < 0x1000000 { + buf.WriteByte(0x83) + buf.WriteByte(byte(length >> 16)) + buf.WriteByte(byte(length >> 8)) + buf.WriteByte(byte(length)) + } else { + buf.WriteByte(0x84) + buf.WriteByte(byte(length >> 24)) + buf.WriteByte(byte(length >> 16)) + buf.WriteByte(byte(length >> 8)) + buf.WriteByte(byte(length)) + } +} + +func parseTag(buf *bytes.Buffer) (Tag, error) { tag := make(Tag, 0) b, err := buf.ReadByte() if err != nil { - return nil, nil, err + return nil, err } tag = append(tag, b) if b&0x1F != 0x1F { - return tag, buf, nil + return tag, nil } for { b, err = buf.ReadByte() if err != nil { - return nil, nil, err + return nil, err } tag = append(tag, b) if b&0x80 != 0x80 { - return tag, buf, nil + return tag, nil } } } diff --git a/apdu/utils_test.go b/apdu/utils_test.go index fc7360a..d70e565 100644 --- a/apdu/utils_test.go +++ b/apdu/utils_test.go @@ -89,7 +89,7 @@ func TestParseLength(t *testing.T) { for _, s := range scenarios { buf := bytes.NewBuffer(s.data) - length, _, err := parseLength(buf) + length, err := ParseLength(buf) if s.err == nil { assert.NoError(t, err) assert.Equal(t, s.expectedLength, length) @@ -128,7 +128,7 @@ func TestParseTag(t *testing.T) { for _, s := range scenarios { buf := bytes.NewBuffer(s.rawTag) - tag, _, err := parseTag(buf) + tag, err := parseTag(buf) require.Nil(t, err) assert.Equal(t, s.expectedTag, tag) } diff --git a/command_set.go b/command_set.go index 176a1d0..70308aa 100644 --- a/command_set.go +++ b/command_set.go @@ -301,15 +301,15 @@ func (cs *CommandSet) DeriveKey(path string) error { func (cs *CommandSet) ExportKey(derive bool, makeCurrent bool, onlyPublic bool, path string) ([]byte, []byte, error) { var p1 uint8 - if derive == false { + if !derive { p1 = P1ExportKeyCurrent - } else if makeCurrent == false { + } else if !makeCurrent { p1 = P1ExportKeyDerive } else { p1 = P1ExportKeyDeriveAndMakeCurrent } var p2 uint8 - if onlyPublic == true { + if onlyPublic { p2 = P2ExportKeyPublicOnly } else { p2 = P2ExportKeyPrivateAndPublic @@ -391,6 +391,22 @@ func (cs *CommandSet) LoadSeed(seed []byte) ([]byte, error) { return resp.Data, nil } +func (cs *CommandSet) GetData(typ uint8) ([]byte, error) { + cmd := NewCommandGetData(typ) + resp, err := cs.sc.Send(cmd) + if err = cs.checkOK(resp, err); err != nil { + return nil, err + } + + return resp.Data, nil +} + +func (cs *CommandSet) StoreData(typ uint8, data []byte) error { + cmd := NewCommandStoreData(typ, data) + resp, err := cs.sc.Send(cmd) + return cs.checkOK(resp, err) +} + func (cs *CommandSet) mutualAuthenticate() error { data := make([]byte, 32) if _, err := rand.Read(data); err != nil { diff --git a/commands.go b/commands.go index 1373716..8ea1c73 100644 --- a/commands.go +++ b/commands.go @@ -26,8 +26,10 @@ const ( InsExportKey = 0xC2 InsSign = 0xC0 InsSetPinlessPath = 0xC1 + InsGetData = 0xCA InsLoadKey = 0xD0 InsGenerateMnemonic = 0xD2 + InsStoreData = 0xE2 P1PairingFirstStep = 0x00 P1PairingFinalStep = 0x01 @@ -43,6 +45,9 @@ const ( P1SignDerive = 0x01 P1SignDeriveAndMakeCurrent = 0x02 P1SignPinless = 0x03 + P1StoreDataPublic = 0x00 + P1StoreDataNDEF = 0x01 + P1StoreDataCash = 0x02 P1ExportKeyCurrent = 0x00 P1ExportKeyDerive = 0x01 P1ExportKeyDeriveAndMakeCurrent = 0x02 @@ -333,6 +338,26 @@ func NewCommandSign(data []byte, p1 uint8, pathStr string) (*apdu.Command, error ), nil } +func NewCommandGetData(typ uint8) *apdu.Command { + return apdu.NewCommand( + globalplatform.ClaGp, + InsGetData, + typ, + 0, + []byte{}, + ) +} + +func NewCommandStoreData(typ uint8, data []byte) *apdu.Command { + return apdu.NewCommand( + globalplatform.ClaGp, + InsStoreData, + typ, + 0, + data, + ) +} + // Internal function. Get the type of starting point for the derivation path. // Used for both DeriveKey and ExportKey func derivationP1FromStartingPoint(s derivationpath.StartingPoint) (uint8, error) { diff --git a/derivationpath/decoder.go b/derivationpath/decoder.go index f269da2..f07151f 100644 --- a/derivationpath/decoder.go +++ b/derivationpath/decoder.go @@ -73,8 +73,6 @@ func (d *decoder) parse() (StartingPoint, []uint32, error) { return d.start, d.path, err } } - - return d.start, d.path, nil } func (d *decoder) readByte() (byte, error) { @@ -168,7 +166,7 @@ func (d *decoder) parseSeparator() error { return d.saveSegment() } - return fmt.Errorf("expected %s, got %s", string(tokenSeparator), string(b)) + return fmt.Errorf("expected %c, got %c", tokenSeparator, b) } func (d *decoder) parseSegment() error { diff --git a/derivationpath/encoder.go b/derivationpath/encoder.go index c1f5b02..c009733 100644 --- a/derivationpath/encoder.go +++ b/derivationpath/encoder.go @@ -8,20 +8,20 @@ import ( ) func Encode(rawPath []uint32) string { - segments := []string{string(tokenMaster)} + segments := []string{string(rune(tokenMaster))} for _, i := range rawPath { suffix := "" if i >= hardenedStart { i = i - hardenedStart - suffix = string(tokenHardened) + suffix = string(rune(tokenHardened)) } segments = append(segments, fmt.Sprintf("%d%s", i, suffix)) } - return strings.Join(segments, string(tokenSeparator)) + return strings.Join(segments, string(rune(tokenSeparator))) } func EncodeFromBytes(data []byte) (string, error) { diff --git a/globalplatform/commands_test.go b/globalplatform/commands_test.go index 2c416ac..1ca0db8 100644 --- a/globalplatform/commands_test.go +++ b/globalplatform/commands_test.go @@ -56,7 +56,7 @@ func TestNewCommandExternalAuthenticate(t *testing.T) { func TestNewCommandDelete(t *testing.T) { aid := hexutils.HexToBytes("0102030405") - cmd := NewCommandDelete(aid) + cmd := NewCommandDelete(aid, P2DeleteObject) assert.Equal(t, uint8(0x80), cmd.Cla) assert.Equal(t, uint8(0xE4), cmd.Ins) assert.Equal(t, uint8(0x00), cmd.P1) diff --git a/secure_channel.go b/secure_channel.go index 277b3a7..d886b18 100644 --- a/secure_channel.go +++ b/secure_channel.go @@ -96,19 +96,29 @@ func (sc *SecureChannel) Send(cmd *apdu.Command) (*apdu.Response, error) { return nil, apdu.NewErrBadResponse(resp.Sw, "unexpected sw in secure channel") } - rmeta := []byte{byte(len(resp.Data)), 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} - rmac := resp.Data[:len(sc.iv)] - rdata := resp.Data[len(sc.iv):] - plainData, err := crypto.DecryptData(rdata, sc.encKey, sc.iv) - if err = sc.updateIV(rmeta, rdata); err != nil { - return nil, err - } + var plainData []byte - if !bytes.Equal(sc.iv, rmac) { - return nil, ErrInvalidResponseMAC - } + if sc.open { + rmeta := []byte{byte(len(resp.Data)), 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} + rmac := resp.Data[:len(sc.iv)] + rdata := resp.Data[len(sc.iv):] - logger.Debug("apdu response decrypted", "hex", hexutils.BytesToHexWithSpaces(plainData)) + if plainData, err = crypto.DecryptData(rdata, sc.encKey, sc.iv); err != nil { + return nil, err + } + + if err = sc.updateIV(rmeta, rdata); err != nil { + return nil, err + } + + if !bytes.Equal(sc.iv, rmac) { + return nil, ErrInvalidResponseMAC + } + + logger.Debug("apdu response decrypted", "hex", hexutils.BytesToHexWithSpaces(plainData)) + } else { + plainData = resp.Data + } return apdu.ParseResponse(plainData) } diff --git a/types/metadata.go b/types/metadata.go new file mode 100644 index 0000000..5e6880e --- /dev/null +++ b/types/metadata.go @@ -0,0 +1,201 @@ +package types + +import ( + "bytes" + "container/list" + "errors" + "io" + + "github.com/status-im/keycard-go/apdu" +) + +type Metadata struct { + name string + paths *list.List +} + +func EmptyMetadata() *Metadata { + return &Metadata{"", list.New()} +} + +func NewMetadata(name string, paths []uint32) (*Metadata, error) { + m := EmptyMetadata() + + if err := m.SetName(name); err != nil { + return nil, err + } + + for i := 0; i < len(paths); i++ { + m.AddPath(paths[i]) + } + + return m, nil +} + +func ParseMetadata(data []byte) (*Metadata, error) { + buf := bytes.NewBuffer(data) + header, err := buf.ReadByte() + + if err != nil { + return nil, err + } + + version := header >> 5 + + if version != 1 { + return nil, errors.New("invalid version") + } + + namelen := int(header & 0x1f) + cardName := string(buf.Next(namelen)) + + list := list.New() + + for { + start, err := apdu.ParseLength(buf) + + if err == io.EOF { + break + } else if err != nil { + return nil, err + } + + count, err := apdu.ParseLength(buf) + + if err != nil { + return nil, err + } + + for i := start; i <= (start + count); i++ { + insertOrderedNoDups(list, i) + } + } + + return &Metadata{cardName, list}, nil +} + +func insertOrderedNoDups(list *list.List, num uint32) { + le := list.Back() + + for le != nil { + val := le.Value.(uint32) + + if num > val { + break + } else if num == val { + return + } + + le = le.Prev() + } + + if le == nil { + list.PushFront(num) + } else { + list.InsertAfter(num, le) + } +} + +func (m *Metadata) Name() string { + return m.name +} + +func (m *Metadata) SetName(name string) error { + if len(name) > 20 { + return errors.New("name longer than 20 chars") + } + + m.name = name + return nil +} + +func (m *Metadata) Paths() []uint32 { + listlen := m.paths.Len() + paths := make([]uint32, listlen) + e := m.paths.Front() + + for i := 0; i < listlen; i++ { + paths[i] = e.Value.(uint32) + e = e.Next() + } + + return paths +} + +func (m *Metadata) AddPath(path uint32) { + insertOrderedNoDups(m.paths, path) +} + +func (m *Metadata) RemovePath(path uint32) { + for le := m.paths.Front(); le != nil; le = le.Next() { + if path == le.Value.(uint32) { + m.paths.Remove(le) + return + } + } +} + +func (m *Metadata) Serialize() []byte { + buf := new(bytes.Buffer) + buf.WriteByte(0x20 | byte(len(m.name))) + buf.WriteString(m.name) + + le := m.paths.Front() + + if le == nil { + return buf.Bytes() + } + + start := le.Value.(uint32) + len := uint32(0) + + for le = le.Next(); le != nil; le = le.Next() { + w := le.Value.(uint32) + + if w == (start + len + 1) { + len++ + } else { + apdu.WriteLength(buf, start) + apdu.WriteLength(buf, len) + start = w + len = 0 + } + } + + apdu.WriteLength(buf, start) + apdu.WriteLength(buf, len) + + return buf.Bytes() +} + +/* + public byte[] toByteArray() { + ByteArrayOutputStream os = new ByteArrayOutputStream(); + byte[] name = this.cardName.getBytes(Charset.forName("US-ASCII")); + os.write(0x20 | name.length); + os.write(name, 0, name.length); + + if (wallets.isEmpty()) { + return os.toByteArray(); + } + + long start = wallets.first(); + int len = 0; + + for (Long w : wallets.tailSet(start + 1)) { + if (w == (start + len + 1)) { + len++; + } else { + TinyBERTLV.writeNum(os, (int) start); + TinyBERTLV.writeNum(os, len); + len = 0; + start = w; + } + } + + TinyBERTLV.writeNum(os, (int) start); + TinyBERTLV.writeNum(os, len); + + return os.toByteArray(); + } +*/ diff --git a/types/metadata_test.go b/types/metadata_test.go new file mode 100644 index 0000000..2219da9 --- /dev/null +++ b/types/metadata_test.go @@ -0,0 +1,20 @@ +package types + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestParseMetadata(t *testing.T) { + m, err := ParseMetadata([]byte{0x23, 0x31, 0x32, 0x33, 0x00, 0x00, 0x04, 0x03, 0x82, 0x7a, 0x28, 0x01}) + assert.NoError(t, err) + assert.Equal(t, "123", m.Name()) + assert.Equal(t, []uint32{0x00, 0x04, 0x05, 0x06, 0x07, 0x7a28, 0x7a29}, m.Paths()) +} + +func TestSerialize(t *testing.T) { + m, err := NewMetadata("123", []uint32{0x00, 0x04, 0x05, 0x06, 0x07, 0x7a28, 0x7a29}) + assert.NoError(t, err) + assert.Equal(t, []byte{0x23, 0x31, 0x32, 0x33, 0x00, 0x00, 0x04, 0x03, 0x82, 0x7a, 0x28, 0x01}, m.Serialize()) +}