diff --git a/src/main/java/com/sparrowwallet/sparrow/io/keycard/TinyBERTLV.java b/src/main/java/com/sparrowwallet/sparrow/io/keycard/TinyBERTLV.java index a952dbcc..56c2e7c8 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/keycard/TinyBERTLV.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/keycard/TinyBERTLV.java @@ -15,16 +15,39 @@ public class TinyBERTLV { private byte[] buffer; private int pos; + /** + * Reads the length at the given offset, on one to four bytes. The returned length is guaranteed to be present in + * the given buffer. + * + * @param buf the buffer to read from + * @param off the offset of the length + * @return the length, and the offset of the body following it + * @throws IllegalArgumentException if the length is malformed, or declares more bytes than the buffer holds + */ public static int[] readNum(byte[] buf, int off) { + if(off >= buf.length) { + throw new IllegalArgumentException("Truncated TLV: no length byte at offset " + off); + } + int len = buf[off++] & 0xff; int lenlen = 0; if((len & 0x80) == 0x80) { lenlen = len & 0x7f; + if(lenlen < 1 || lenlen > 4 || (off + lenlen) > buf.length) { + throw new IllegalArgumentException("Truncated TLV: length header of " + lenlen + " bytes is unsupported or exceeds the " + (buf.length - off) + " bytes remaining"); + } + len = readVal(buf, off, lenlen); } - return new int[]{len, off + lenlen}; + off += lenlen; + + if(len < 0 || len > (buf.length - off)) { + throw new IllegalArgumentException("Truncated TLV: declared length of " + Integer.toUnsignedString(len) + " exceeds the " + (buf.length - off) + " bytes remaining"); + } + + return new int[]{len, off}; } public static int readVal(byte[] val, int off, int len) { @@ -76,7 +99,7 @@ public class TinyBERTLV { * * @param tag the tag to enter * @return the length of the TLV - * @throws IllegalArgumentException if the next tag does not match the given one + * @throws IllegalArgumentException if the next tag does not match the given one, or the TLV extends past the end of the buffer */ public int enterConstructed(int tag) throws IllegalArgumentException { checkTag(tag, readTag()); @@ -88,7 +111,7 @@ public class TinyBERTLV { * * @param tag the tag to read * @return the body of the TLV - * @throws IllegalArgumentException if the next tag does not match the given one + * @throws IllegalArgumentException if the next tag does not match the given one, or the TLV extends past the end of the buffer */ public byte[] readPrimitive(int tag) throws IllegalArgumentException { checkTag(tag, readTag()); @@ -148,9 +171,10 @@ public class TinyBERTLV { } /** - * Reads the next tag. The current implementation only reads length on one and two bytes. Can be extended if needed. + * Reads the next length. The current implementation reads lengths on one to four bytes. Can be extended if needed. * - * @return the tag + * @return the length + * @throws IllegalArgumentException if the length is malformed, or declares more bytes than the buffer holds */ public int readLength() { int[] len = TinyBERTLV.readNum(buffer, pos); diff --git a/src/test/java/com/sparrowwallet/sparrow/io/keycard/TinyBERTLVTest.java b/src/test/java/com/sparrowwallet/sparrow/io/keycard/TinyBERTLVTest.java new file mode 100644 index 00000000..bb512d44 --- /dev/null +++ b/src/test/java/com/sparrowwallet/sparrow/io/keycard/TinyBERTLVTest.java @@ -0,0 +1,131 @@ +package com.sparrowwallet.sparrow.io.keycard; + +import com.sparrowwallet.drongo.Utils; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayOutputStream; + +public class TinyBERTLVTest { + @Test + public void testFourByteLengthRejected() { + //tag 0x80 declaring a ~2GiB body from six bytes of card response + TinyBERTLV tlv = new TinyBERTLV(Utils.hexToBytes("80847fffffff")); + Assertions.assertThrows(IllegalArgumentException.class, () -> tlv.readPrimitive(ApplicationInfo.TLV_PUB_KEY)); + } + + @Test + public void testNegativeLengthRejected() { + //a four byte length with the sign bit set + TinyBERTLV tlv = new TinyBERTLV(Utils.hexToBytes("8084ffffffff")); + Assertions.assertThrows(IllegalArgumentException.class, () -> tlv.readPrimitive(ApplicationInfo.TLV_PUB_KEY)); + } + + @Test + public void testShortBodyRejectedNotZeroPadded() { + //five bytes declared, two supplied - previously returned three zero bytes of padding + TinyBERTLV tlv = new TinyBERTLV(Utils.hexToBytes("80050102")); + Assertions.assertThrows(IllegalArgumentException.class, () -> tlv.readPrimitive(ApplicationInfo.TLV_PUB_KEY)); + } + + @Test + public void testLongFormShortBodyRejectedNotZeroPadded() { + //a long form length of one million, four bytes supplied - previously returned a megabyte of zero padding + TinyBERTLV tlv = new TinyBERTLV(Utils.hexToBytes("8084000f424001020304")); + Assertions.assertThrows(IllegalArgumentException.class, () -> tlv.readPrimitive(ApplicationInfo.TLV_PUB_KEY)); + } + + @Test + public void testMissingLengthByteRejected() { + TinyBERTLV tlv = new TinyBERTLV(Utils.hexToBytes("80")); + Assertions.assertThrows(IllegalArgumentException.class, () -> tlv.readPrimitive(ApplicationInfo.TLV_PUB_KEY)); + } + + @Test + public void testTruncatedLengthHeaderRejected() { + //0x84 promises four length bytes, only one follows + TinyBERTLV tlv = new TinyBERTLV(Utils.hexToBytes("808400")); + Assertions.assertThrows(IllegalArgumentException.class, () -> tlv.readPrimitive(ApplicationInfo.TLV_PUB_KEY)); + } + + @Test + public void testIndefiniteLengthRejected() { + TinyBERTLV tlv = new TinyBERTLV(Utils.hexToBytes("8080")); + Assertions.assertThrows(IllegalArgumentException.class, () -> tlv.readPrimitive(ApplicationInfo.TLV_PUB_KEY)); + } + + @Test + public void testOversizedConstructedLengthRejected() { + TinyBERTLV tlv = new TinyBERTLV(Utils.hexToBytes("a4847fffffff")); + Assertions.assertThrows(IllegalArgumentException.class, () -> tlv.enterConstructed(ApplicationInfo.TLV_APPLICATION_INFO_TEMPLATE)); + } + + @Test + public void testShortFormLengthStillParses() { + TinyBERTLV tlv = new TinyBERTLV(Utils.hexToBytes("80020102")); + Assertions.assertArrayEquals(Utils.hexToBytes("0102"), tlv.readPrimitive(ApplicationInfo.TLV_PUB_KEY)); + } + + @Test + public void testLongFormLengthStillParses() { + //a 200 byte body, which requires the 0x81 length form + byte[] body = new byte[200]; + for(int i = 0; i < body.length; i++) { + body[i] = (byte)i; + } + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + baos.write(ApplicationInfo.TLV_PUB_KEY); + TinyBERTLV.writeNum(baos, body.length); + baos.writeBytes(body); + + TinyBERTLV tlv = new TinyBERTLV(baos.toByteArray()); + Assertions.assertArrayEquals(body, tlv.readPrimitive(ApplicationInfo.TLV_PUB_KEY)); + } + + @Test + public void testUninitializedCardSelectResponseStillParses() { + byte[] pubKey = new byte[65]; + pubKey[0] = 0x04; + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + writePrimitive(baos, ApplicationInfo.TLV_PUB_KEY, pubKey); + + ApplicationInfo applicationInfo = new ApplicationInfo(baos.toByteArray()); + Assertions.assertFalse(applicationInfo.isInitializedCard()); + Assertions.assertArrayEquals(pubKey, applicationInfo.getSecureChannelPubKey()); + Assertions.assertTrue(applicationInfo.hasSecureChannelCapability()); + } + + @Test + public void testInitializedCardSelectResponseStillParses() { + byte[] instanceUID = new byte[16]; + byte[] pubKey = new byte[65]; + pubKey[0] = 0x04; + byte[] keyUID = new byte[32]; + + ByteArrayOutputStream template = new ByteArrayOutputStream(); + writePrimitive(template, ApplicationInfo.TLV_UID, instanceUID); + writePrimitive(template, ApplicationInfo.TLV_PUB_KEY, pubKey); + writePrimitive(template, TinyBERTLV.TLV_INT, new byte[] {0x03, 0x01}); + writePrimitive(template, TinyBERTLV.TLV_INT, new byte[] {0x05}); + writePrimitive(template, ApplicationInfo.TLV_KEY_UID, keyUID); + writePrimitive(template, ApplicationInfo.TLV_CAPABILITIES, new byte[] {ApplicationInfo.CAPABILITIES_ALL}); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + writePrimitive(baos, ApplicationInfo.TLV_APPLICATION_INFO_TEMPLATE, template.toByteArray()); + + ApplicationInfo applicationInfo = new ApplicationInfo(baos.toByteArray()); + Assertions.assertTrue(applicationInfo.isInitializedCard()); + Assertions.assertEquals("3.1", applicationInfo.getAppVersionString()); + Assertions.assertEquals(5, applicationInfo.getFreePairingSlots()); + Assertions.assertArrayEquals(keyUID, applicationInfo.getKeyUID()); + Assertions.assertEquals(ApplicationInfo.CAPABILITIES_ALL, applicationInfo.getCapabilities()); + } + + private void writePrimitive(ByteArrayOutputStream baos, byte tag, byte[] body) { + baos.write(tag); + TinyBERTLV.writeNum(baos, body.length); + baos.writeBytes(body); + } +}