From 06af9b8ee21b7e38d90d8d0fc343a308acebfc0d Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Tue, 20 Jul 2021 11:16:15 +0200 Subject: [PATCH] v1.6.2 to fix byte padding bug in short source fingerprint integers --- README.md | 2 +- build.gradle | 2 +- .../hummingbird/registry/CryptoAccount.java | 3 +- .../hummingbird/registry/CryptoHDKey.java | 2 +- .../hummingbird/registry/CryptoKeypath.java | 2 +- .../hummingbird/registry/RegistryItem.java | 32 +++++++++++++++++ .../hummingbird/registry/CryptoHDKeyTest.java | 34 +++++++++++++++++++ 7 files changed, 72 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 54b0710..52b27c4 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Hummingbird requires a minimum of Java 8. Hummingbird is hosted in Maven Central and can be added as a dependency with the following: ``` -implementation('com.sparrowwallet:hummingbird:1.6.1') +implementation('com.sparrowwallet:hummingbird:1.6.2') ``` ## Usage diff --git a/build.gradle b/build.gradle index ba40dff..37b6b5c 100644 --- a/build.gradle +++ b/build.gradle @@ -16,7 +16,7 @@ apply plugin: 'com.bmuschko.nexus' archivesBaseName = 'hummingbird' group 'com.sparrowwallet' -version '1.6.1' +version '1.6.2' repositories { mavenCentral() diff --git a/src/main/java/com/sparrowwallet/hummingbird/registry/CryptoAccount.java b/src/main/java/com/sparrowwallet/hummingbird/registry/CryptoAccount.java index d26a5a0..2d81568 100644 --- a/src/main/java/com/sparrowwallet/hummingbird/registry/CryptoAccount.java +++ b/src/main/java/com/sparrowwallet/hummingbird/registry/CryptoAccount.java @@ -47,12 +47,13 @@ public class CryptoAccount extends RegistryItem { Map cryptoAccountMap = (Map)cbor; UnsignedInteger uintMasterFingerprint = (UnsignedInteger)cryptoAccountMap.get(new UnsignedInteger(MASTER_FINGERPRINT_KEY)); + byte[] masterFingerprint = bigIntegerToBytes(uintMasterFingerprint.getValue(), 4); Array outputDescriptors = (Array)cryptoAccountMap.get(new UnsignedInteger(OUTPUT_DESCRIPTORS_KEY)); List cryptoOutputs = new ArrayList<>(outputDescriptors.getDataItems().size()); for(DataItem item : outputDescriptors.getDataItems()) { cryptoOutputs.add(CryptoOutput.fromCbor(item)); } - return new CryptoAccount(uintMasterFingerprint.getValue().toByteArray(), cryptoOutputs); + return new CryptoAccount(masterFingerprint, cryptoOutputs); } } diff --git a/src/main/java/com/sparrowwallet/hummingbird/registry/CryptoHDKey.java b/src/main/java/com/sparrowwallet/hummingbird/registry/CryptoHDKey.java index 3c3b368..c90fbe8 100644 --- a/src/main/java/com/sparrowwallet/hummingbird/registry/CryptoHDKey.java +++ b/src/main/java/com/sparrowwallet/hummingbird/registry/CryptoHDKey.java @@ -176,7 +176,7 @@ public class CryptoHDKey extends RegistryItem { } else if(intKey == CHILDREN_KEY) { children = CryptoKeypath.fromCbor(map.get(uintKey)); } else if(intKey == PARENT_FINGERPRINT_KEY) { - parentFingerprint = ((UnsignedInteger)map.get(uintKey)).getValue().toByteArray(); + parentFingerprint = bigIntegerToBytes(((UnsignedInteger)map.get(uintKey)).getValue(), 4); } else if(intKey == NAME_KEY) { name = ((UnicodeString)map.get(uintKey)).getString(); } else if(intKey == NOTE_KEY) { diff --git a/src/main/java/com/sparrowwallet/hummingbird/registry/CryptoKeypath.java b/src/main/java/com/sparrowwallet/hummingbird/registry/CryptoKeypath.java index bcc3536..64bf237 100644 --- a/src/main/java/com/sparrowwallet/hummingbird/registry/CryptoKeypath.java +++ b/src/main/java/com/sparrowwallet/hummingbird/registry/CryptoKeypath.java @@ -99,7 +99,7 @@ public class CryptoKeypath extends RegistryItem { } } } else if(intKey == SOURCE_FINGERPRINT_KEY) { - sourceFingerprint = ((UnsignedInteger)map.get(key)).getValue().toByteArray(); + sourceFingerprint = bigIntegerToBytes(((UnsignedInteger)map.get(key)).getValue(), 4); } else if(intKey == DEPTH_KEY) { depth = ((UnsignedInteger)map.get(key)).getValue().intValue(); } diff --git a/src/main/java/com/sparrowwallet/hummingbird/registry/RegistryItem.java b/src/main/java/com/sparrowwallet/hummingbird/registry/RegistryItem.java index c8592df..440e7df 100644 --- a/src/main/java/com/sparrowwallet/hummingbird/registry/RegistryItem.java +++ b/src/main/java/com/sparrowwallet/hummingbird/registry/RegistryItem.java @@ -5,6 +5,8 @@ import co.nstant.in.cbor.CborException; import com.sparrowwallet.hummingbird.UR; import java.io.ByteArrayOutputStream; +import java.math.BigInteger; +import java.util.Arrays; public abstract class RegistryItem implements CborSerializable { public abstract RegistryType getRegistryType(); @@ -19,4 +21,34 @@ public abstract class RegistryItem implements CborSerializable { throw new IllegalArgumentException(e); } } + + /** + *

+ * The regular {@link BigInteger#toByteArray()} includes the sign bit of the number and + * might result in an extra byte addition. This method removes this extra byte. + *

+ * @param b the integer to format into a byte array + * @param numBytes the desired size of the resulting byte array + * @return numBytes byte long array. + */ + protected static byte[] bigIntegerToBytes(BigInteger b, int numBytes) { + if(b.signum() < 0) { + throw new IllegalArgumentException("b must be positive or zero"); + } + if(numBytes <= 0) { + throw new IllegalArgumentException("numBytes must be positive"); + } + byte[] src = b.toByteArray(); + byte[] dest = new byte[numBytes]; + boolean isFirstByteOnlyForSign = src[0] == 0; + int length = isFirstByteOnlyForSign ? src.length - 1 : src.length; + if(length > numBytes) { + throw new IllegalArgumentException("The given number does not fit in " + numBytes); + } + int srcPos = isFirstByteOnlyForSign ? 1 : 0; + int destPos = numBytes - length; + System.arraycopy(src, srcPos, dest, destPos, length); + Arrays.fill(src, (byte)0); + return dest; + } } diff --git a/src/test/java/com/sparrowwallet/hummingbird/registry/CryptoHDKeyTest.java b/src/test/java/com/sparrowwallet/hummingbird/registry/CryptoHDKeyTest.java index 83b35a2..59b49a8 100644 --- a/src/test/java/com/sparrowwallet/hummingbird/registry/CryptoHDKeyTest.java +++ b/src/test/java/com/sparrowwallet/hummingbird/registry/CryptoHDKeyTest.java @@ -61,4 +61,38 @@ public class CryptoHDKeyTest { String ur = "ur:crypto-hdkey/otaxhdclaxpsfswtmnsknejlceuogoqdaelbmhwnptlrecwpeehhfnpsfzbauecatleotsheptaahdcxvsbbhlrpdivdmelovygscttbstjpnllpasmtcaecmyvswpwftssffxrkcabsmdcxamtaaddyoeadlaaocylpgrstlfiewtseje"; Assert.assertEquals(ur, cryptoHDKey.toUR().toString()); } + + @Test + public void testZeroMasterFingerprint() throws CborException { + String hex = "a303582103ac3df08ec59f6f1cdc55b3007f90f1a98435ec345c3cac400ede1dd533d75fa9045820e8145db627e79188e14c1fd6c772998509961d358fe8ecf3d7cc43bb1d0f952006d90130a201800200"; + byte[] data = TestUtils.hexToBytes(hex); + List items = CborDecoder.decode(data); + CryptoHDKey cryptoHDKey = CryptoHDKey.fromCbor(items.get(0)); + Assert.assertFalse(cryptoHDKey.isMaster()); + Assert.assertFalse(cryptoHDKey.isPrivateKey()); + Assert.assertEquals("03ac3df08ec59f6f1cdc55b3007f90f1a98435ec345c3cac400ede1dd533d75fa9", TestUtils.bytesToHex(cryptoHDKey.getKey())); + Assert.assertEquals("e8145db627e79188e14c1fd6c772998509961d358fe8ecf3d7cc43bb1d0f9520", TestUtils.bytesToHex(cryptoHDKey.getChainCode())); + Assert.assertNull(cryptoHDKey.getOrigin().getPath()); + Assert.assertEquals("00000000", TestUtils.bytesToHex(cryptoHDKey.getOrigin().getSourceFingerprint())); + Assert.assertEquals(hex.toLowerCase(), TestUtils.encode(cryptoHDKey.toCbor())); + String ur = "ur:crypto-hdkey/otaxhdclaxpsfswtmnsknejlceuogoqdaelbmhwnptlrecwpeehhfnpsfzbauecatleotsheptaahdcxvsbbhlrpdivdmelovygscttbstjpnllpasmtcaecmyvswpwftssffxrkcabsmdcxamtaaddyoeadlaaoaebkoxdive"; + Assert.assertEquals(ur, cryptoHDKey.toUR().toString()); + } + + @Test + public void testShortMasterFingerprint() throws CborException { + String hex = "A303582103AC3DF08EC59F6F1CDC55B3007F90F1A98435EC345C3CAC400EDE1DD533D75FA9045820E8145DB627E79188E14C1FD6C772998509961D358FE8ECF3D7CC43BB1D0F952006D90130A201800218FF"; + byte[] data = TestUtils.hexToBytes(hex); + List items = CborDecoder.decode(data); + CryptoHDKey cryptoHDKey = CryptoHDKey.fromCbor(items.get(0)); + Assert.assertFalse(cryptoHDKey.isMaster()); + Assert.assertFalse(cryptoHDKey.isPrivateKey()); + Assert.assertEquals("03ac3df08ec59f6f1cdc55b3007f90f1a98435ec345c3cac400ede1dd533d75fa9", TestUtils.bytesToHex(cryptoHDKey.getKey())); + Assert.assertEquals("e8145db627e79188e14c1fd6c772998509961d358fe8ecf3d7cc43bb1d0f9520", TestUtils.bytesToHex(cryptoHDKey.getChainCode())); + Assert.assertNull(cryptoHDKey.getOrigin().getPath()); + Assert.assertEquals("000000ff", TestUtils.bytesToHex(cryptoHDKey.getOrigin().getSourceFingerprint())); + Assert.assertEquals(hex.toLowerCase(), TestUtils.encode(cryptoHDKey.toCbor())); + String ur = "ur:crypto-hdkey/otaxhdclaxpsfswtmnsknejlceuogoqdaelbmhwnptlrecwpeehhfnpsfzbauecatleotsheptaahdcxvsbbhlrpdivdmelovygscttbstjpnllpasmtcaecmyvswpwftssffxrkcabsmdcxamtaaddyoeadlaaocszmtnkocyct"; + Assert.assertEquals(ur, cryptoHDKey.toUR().toString()); + } }