Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement correct and complete Monero and Cryptonote address validator #2422

Merged
merged 1 commit into from
Feb 15, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
280 changes: 229 additions & 51 deletions assets/src/main/java/bisq/asset/CryptonoteAddressValidator.java
Original file line number Diff line number Diff line change
Expand Up @@ -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(boolean validateChecksum, long... validPefixes) {
this.validPefixes = validPefixes;
this.validateChecksum = validateChecksum;
}

public CryptonoteAddressValidator(String prefix, String subAddressPrefix) {
this.prefix = prefix;
this.subAddressPrefix = subAddressPrefix;
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<Integer, Integer> 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;
}
}
2 changes: 1 addition & 1 deletion assets/src/main/java/bisq/asset/coins/Aeon.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,6 @@
public class Aeon extends Coin {

public Aeon() {
super("Aeon", "AEON", new CryptonoteAddressValidator("Wm", "Xn"));
super("Aeon", "AEON", new CryptonoteAddressValidator(0xB2, 0x06B8));
}
}
2 changes: 1 addition & 1 deletion assets/src/main/java/bisq/asset/coins/Blur.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,6 @@
public class Blur extends Coin {

public Blur() {
super("Blur", "BLUR", new CryptonoteAddressValidator("bL", "Ry"));
super("Blur", "BLUR", new CryptonoteAddressValidator(0x1e4d, 0x2195));
}
}
2 changes: 1 addition & 1 deletion assets/src/main/java/bisq/asset/coins/Cash2.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,6 @@
public class Cash2 extends Coin {

public Cash2() {
super("Cash2", "CASH2", new CryptonoteAddressValidator("2", ""));
super("Cash2", "CASH2", new CryptonoteAddressValidator(false, 0x6));
}
}
2 changes: 1 addition & 1 deletion assets/src/main/java/bisq/asset/coins/FourtyTwo.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,6 @@
public class FourtyTwo extends Coin {

public FourtyTwo() {
super("FourtyTwo", "FRTY", new CryptonoteAddressValidator("foUr", "SNake"));
super("FourtyTwo", "FRTY", new CryptonoteAddressValidator(0x1cbd67, 0x13271817));
}
}
2 changes: 1 addition & 1 deletion assets/src/main/java/bisq/asset/coins/Mask.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,6 @@

public class Mask extends Coin {
public Mask() {
super("Mask", "MASK", new CryptonoteAddressValidator("M", "bT"));
super("Mask", "MASK", new CryptonoteAddressValidator(123, 206));
}
}
2 changes: 1 addition & 1 deletion assets/src/main/java/bisq/asset/coins/Monero.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,6 @@
public class Monero extends Coin {

public Monero() {
super("Monero", "XMR", new CryptonoteAddressValidator("4", "8"));
super("Monero", "XMR", new CryptonoteAddressValidator(18, 42));
}
}