diff --git a/assets/src/main/java/bisq/asset/CryptonoteAddressValidator.java b/assets/src/main/java/bisq/asset/CryptonoteAddressValidator.java index 47a6883154..f9591fca1d 100644 --- a/assets/src/main/java/bisq/asset/CryptonoteAddressValidator.java +++ b/assets/src/main/java/bisq/asset/CryptonoteAddressValidator.java @@ -17,70 +17,248 @@ package bisq.asset; +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.LongBuffer; +import java.util.Map; + /** * {@link AddressValidator} for Base58-encoded Cryptonote addresses. * - * @author Chris Beams - * @since 0.7.0 + * @author Xiphon */ public class CryptonoteAddressValidator implements AddressValidator { - private final String prefix; - private final String subAddressPrefix; - private final String validCharactersRegex = "^[1-9A-HJ-NP-Za-km-z]+$"; + private final long[] validPefixes; + private final boolean validateChecksum; - public CryptonoteAddressValidator(String prefix, String subAddressPrefix) { - this.prefix = prefix; - this.subAddressPrefix = subAddressPrefix; + public CryptonoteAddressValidator(boolean validateChecksum, long... validPefixes) { + this.validPefixes = validPefixes; + this.validateChecksum = validateChecksum; + } + + public CryptonoteAddressValidator(long... validPefixes) { + this(true, validPefixes); } @Override public AddressValidationResult validate(String address) { - if (!address.matches(validCharactersRegex)) { - // Invalid characters + try { + long prefix = MoneroBase58.decodeAddress(address, this.validateChecksum); + for (long validPrefix : this.validPefixes) { + if (prefix == validPrefix) { + return AddressValidationResult.validAddress(); + } + } + return AddressValidationResult.invalidAddress(String.format("invalid address prefix %x", prefix)); + } catch (Exception e) { return AddressValidationResult.invalidStructure(); } - if (address.startsWith(prefix)) { - if (prefix.length() == 1 && address.length() == 94 + prefix.length()) { - // XMR-type Standard address - return AddressValidationResult.validAddress(); - } - else if (prefix.length() == 2 && address.length() == 95 + prefix.length()) { - //Aeon & Blur-type addresses - return AddressValidationResult.validAddress(); - } - else if (prefix.length() == 4 && address.length() == 94 + prefix.length()) { - // FourtyTwo-type address - return AddressValidationResult.validAddress(); - } - else { - //Non-supported prefix - return AddressValidationResult.invalidStructure(); - } - } - if (address.startsWith(subAddressPrefix)) { - if (subAddressPrefix.length() == 1 && address.length() == 94 + subAddressPrefix.length()) { - // XMR-type subaddress - return AddressValidationResult.validAddress(); - } - else if (subAddressPrefix.length() == 2 && address.length() == 95 + subAddressPrefix.length()) { - // Aeon, Mask & Blur-type subaddress - return AddressValidationResult.validAddress(); - } - if (subAddressPrefix.length() == 5 && address.length() == 96 + subAddressPrefix.length()) { - // FourtyTwo-type subaddress - return AddressValidationResult.validAddress(); - } - else { - // Non-supported subAddress - return AddressValidationResult.invalidStructure(); - } - } - else { - //Integrated? Invalid? Doesn't matter - return AddressValidationResult.invalidStructure(); - } - } + } } +class Keccak { + private static final int BLOCK_SIZE = 136; + private static final int LONGS_PER_BLOCK = BLOCK_SIZE / 8; + private static final int KECCAK_ROUNDS = 24; + private static final long[] KECCAKF_RNDC = { + 0x0000000000000001L, 0x0000000000008082L, 0x800000000000808aL, + 0x8000000080008000L, 0x000000000000808bL, 0x0000000080000001L, + 0x8000000080008081L, 0x8000000000008009L, 0x000000000000008aL, + 0x0000000000000088L, 0x0000000080008009L, 0x000000008000000aL, + 0x000000008000808bL, 0x800000000000008bL, 0x8000000000008089L, + 0x8000000000008003L, 0x8000000000008002L, 0x8000000000000080L, + 0x000000000000800aL, 0x800000008000000aL, 0x8000000080008081L, + 0x8000000000008080L, 0x0000000080000001L, 0x8000000080008008L + }; + private static final int[] KECCAKF_ROTC = { + 1, 3, 6, 10, 15, 21, 28, 36, 45, 55, 2, 14, + 27, 41, 56, 8, 25, 43, 62, 18, 39, 61, 20, 44 + }; + private static final int[] KECCAKF_PILN = { + 10, 7, 11, 17, 18, 3, 5, 16, 8, 21, 24, 4, + 15, 23, 19, 13, 12, 2, 20, 14, 22, 9, 6, 1 + }; + + private static long rotateLeft(long value, int shift) { + return (value << shift) | (value >>> (64 - shift)); + } + + private static void keccakf(long[] st, int rounds) { + long[] bc = new long[5]; + + for (int round = 0; round < rounds; ++round) { + for (int i = 0; i < 5; ++i) { + bc[i] = st[i] ^ st[i + 5] ^ st[i + 10] ^ st[i + 15] ^ st[i + 20]; + } + + for (int i = 0; i < 5; i++) { + long t = bc[(i + 4) % 5] ^ rotateLeft(bc[(i + 1) % 5], 1); + for (int j = 0; j < 25; j += 5) { + st[j + i] ^= t; + } + } + + long t = st[1]; + for (int i = 0; i < 24; ++i) { + int j = KECCAKF_PILN[i]; + bc[0] = st[j]; + st[j] = rotateLeft(t, KECCAKF_ROTC[i]); + t = bc[0]; + } + + for (int j = 0; j < 25; j += 5) { + for (int i = 0; i < 5; i++) { + bc[i] = st[j + i]; + } + for (int i = 0; i < 5; i++) { + st[j + i] ^= (~bc[(i + 1) % 5]) & bc[(i + 2) % 5]; + } + } + + st[0] ^= KECCAKF_RNDC[round]; + } + } + + public static ByteBuffer keccak1600(ByteBuffer input) { + input.order(ByteOrder.LITTLE_ENDIAN); + + int fullBlocks = input.remaining() / BLOCK_SIZE; + long[] st = new long[25]; + for (int block = 0; block < fullBlocks; ++block) { + for (int index = 0; index < LONGS_PER_BLOCK; ++index) { + st[index] ^= input.getLong(); + } + keccakf(st, KECCAK_ROUNDS); + } + + ByteBuffer lastBlock = ByteBuffer.allocate(144).order(ByteOrder.LITTLE_ENDIAN); + lastBlock.put(input); + lastBlock.put((byte)1); + int paddingOffset = BLOCK_SIZE - 1; + lastBlock.put(paddingOffset, (byte)(lastBlock.get(paddingOffset) | 0x80)); + lastBlock.rewind(); + + for (int index = 0; index < LONGS_PER_BLOCK; ++index) { + st[index] ^= lastBlock.getLong(); + } + + keccakf(st, KECCAK_ROUNDS); + + ByteBuffer result = ByteBuffer.allocate(32); + result.slice().order(ByteOrder.LITTLE_ENDIAN).asLongBuffer().put(st, 0, 4); + return result; + } +} + +class MoneroBase58 { + + private static final String ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; + private static final BigInteger ALPHABET_SIZE = BigInteger.valueOf(ALPHABET.length()); + private static final int FULL_DECODED_BLOCK_SIZE = 8; + private static final int FULL_ENCODED_BLOCK_SIZE = 11; + private static final BigInteger UINT64_MAX = new BigInteger("18446744073709551615"); + private static final Map DECODED_CHUNK_LENGTH = Map.of( 2, 1, + 3, 2, + 5, 3, + 6, 4, + 7, 5, + 9, 6, + 10, 7, + 11, 8); + + private static void decodeChunk(String input, + int inputOffset, + int inputLength, + byte[] decoded, + int decodedOffset, + int decodedLength) throws Exception { + + BigInteger result = BigInteger.ZERO; + + BigInteger order = BigInteger.ONE; + for (int index = inputOffset + inputLength; index != inputOffset; order = order.multiply(ALPHABET_SIZE)) { + char character = input.charAt(--index); + int digit = ALPHABET.indexOf(character); + if (digit == -1) { + throw new Exception("invalid character " + character); + } + result = result.add(order.multiply(BigInteger.valueOf(digit))); + if (result.compareTo(UINT64_MAX) > 0) { + throw new Exception("64-bit unsinged integer overflow " + result.toString()); + } + } + + BigInteger maxCapacity = BigInteger.ONE.shiftLeft(8 * decodedLength); + if (result.compareTo(maxCapacity) >= 0) { + throw new Exception("capacity overflow " + result.toString()); + } + + for (int index = decodedOffset + decodedLength; index != decodedOffset; result = result.shiftRight(8)) { + decoded[--index] = result.byteValue(); + } + } + + private static byte[] decode(String input) throws Exception { + if (input.length() == 0) { + return new byte[0]; + } + + int chunks = input.length() / FULL_ENCODED_BLOCK_SIZE; + int lastEncodedSize = input.length() % FULL_ENCODED_BLOCK_SIZE; + int lastChunkSize = lastEncodedSize > 0 ? DECODED_CHUNK_LENGTH.get(lastEncodedSize) : 0; + + byte[] result = new byte[chunks * FULL_DECODED_BLOCK_SIZE + lastChunkSize]; + int inputOffset = 0; + int resultOffset = 0; + for (int chunk = 0; chunk < chunks; ++chunk, + inputOffset += FULL_ENCODED_BLOCK_SIZE, + resultOffset += FULL_DECODED_BLOCK_SIZE) { + decodeChunk(input, inputOffset, FULL_ENCODED_BLOCK_SIZE, result, resultOffset, FULL_DECODED_BLOCK_SIZE); + } + if (lastChunkSize > 0) { + decodeChunk(input, inputOffset, lastEncodedSize, result, resultOffset, lastChunkSize); + } + + return result; + } + + private static long readVarInt(ByteBuffer buffer) { + long result = 0; + for (int shift = 0; ; shift += 7) { + byte current = buffer.get(); + result += (current & 0x7fL) << shift; + if ((current & 0x80L) == 0) { + break; + } + } + return result; + } + + public static long decodeAddress(String address, boolean validateChecksum) throws Exception { + byte[] decoded = decode(address); + + int checksumSize = 4; + if (decoded.length < checksumSize) { + throw new Exception("invalid length"); + } + + ByteBuffer decodedAddress = ByteBuffer.wrap(decoded, 0, decoded.length - checksumSize); + + long prefix = readVarInt(decodedAddress.slice()); + if (!validateChecksum) { + return prefix; + } + + ByteBuffer fastHash = Keccak.keccak1600(decodedAddress.slice()); + int checksum = fastHash.getInt(); + int expected = ByteBuffer.wrap(decoded, decoded.length - checksumSize, checksumSize).getInt(); + if (checksum != expected) { + throw new Exception(String.format("invalid checksum %08X, expected %08X", checksum, expected)); + } + + return prefix; + } +} diff --git a/assets/src/main/java/bisq/asset/coins/Aeon.java b/assets/src/main/java/bisq/asset/coins/Aeon.java index a1ded133e6..3a6b6408a4 100644 --- a/assets/src/main/java/bisq/asset/coins/Aeon.java +++ b/assets/src/main/java/bisq/asset/coins/Aeon.java @@ -23,6 +23,6 @@ import bisq.asset.CryptonoteAddressValidator; public class Aeon extends Coin { public Aeon() { - super("Aeon", "AEON", new CryptonoteAddressValidator("Wm", "Xn")); + super("Aeon", "AEON", new CryptonoteAddressValidator(0xB2, 0x06B8)); } } diff --git a/assets/src/main/java/bisq/asset/coins/Blur.java b/assets/src/main/java/bisq/asset/coins/Blur.java index 90eea56575..366ee453a0 100644 --- a/assets/src/main/java/bisq/asset/coins/Blur.java +++ b/assets/src/main/java/bisq/asset/coins/Blur.java @@ -25,6 +25,6 @@ import bisq.asset.CryptonoteAddressValidator; public class Blur extends Coin { public Blur() { - super("Blur", "BLUR", new CryptonoteAddressValidator("bL", "Ry")); + super("Blur", "BLUR", new CryptonoteAddressValidator(0x1e4d, 0x2195)); } } diff --git a/assets/src/main/java/bisq/asset/coins/Cash2.java b/assets/src/main/java/bisq/asset/coins/Cash2.java index f6225533c7..10c0cb0053 100644 --- a/assets/src/main/java/bisq/asset/coins/Cash2.java +++ b/assets/src/main/java/bisq/asset/coins/Cash2.java @@ -25,6 +25,6 @@ import bisq.asset.CryptonoteAddressValidator; public class Cash2 extends Coin { public Cash2() { - super("Cash2", "CASH2", new CryptonoteAddressValidator("2", "")); + super("Cash2", "CASH2", new CryptonoteAddressValidator(false, 0x6)); } } diff --git a/assets/src/main/java/bisq/asset/coins/FourtyTwo.java b/assets/src/main/java/bisq/asset/coins/FourtyTwo.java index fb21882df4..14f95d9d93 100644 --- a/assets/src/main/java/bisq/asset/coins/FourtyTwo.java +++ b/assets/src/main/java/bisq/asset/coins/FourtyTwo.java @@ -23,6 +23,6 @@ import bisq.asset.CryptonoteAddressValidator; public class FourtyTwo extends Coin { public FourtyTwo() { - super("FourtyTwo", "FRTY", new CryptonoteAddressValidator("foUr", "SNake")); + super("FourtyTwo", "FRTY", new CryptonoteAddressValidator(0x1cbd67, 0x13271817)); } } diff --git a/assets/src/main/java/bisq/asset/coins/Mask.java b/assets/src/main/java/bisq/asset/coins/Mask.java index 9921722215..a539a53f90 100644 --- a/assets/src/main/java/bisq/asset/coins/Mask.java +++ b/assets/src/main/java/bisq/asset/coins/Mask.java @@ -22,6 +22,6 @@ import bisq.asset.CryptonoteAddressValidator; public class Mask extends Coin { public Mask() { - super("Mask", "MASK", new CryptonoteAddressValidator("M", "bT")); + super("Mask", "MASK", new CryptonoteAddressValidator(123, 206)); } } diff --git a/assets/src/main/java/bisq/asset/coins/Monero.java b/assets/src/main/java/bisq/asset/coins/Monero.java index 898ff326ad..77c50fb803 100644 --- a/assets/src/main/java/bisq/asset/coins/Monero.java +++ b/assets/src/main/java/bisq/asset/coins/Monero.java @@ -25,6 +25,6 @@ import bisq.asset.CryptonoteAddressValidator; public class Monero extends Coin { public Monero() { - super("Monero", "XMR", new CryptonoteAddressValidator("4", "8")); + super("Monero", "XMR", new CryptonoteAddressValidator(18, 42)); } }