From daa9a9911be40832f71124be338d400c7004c2a6 Mon Sep 17 00:00:00 2001 From: Christian Basler Date: Sat, 11 Apr 2015 16:16:41 +0200 Subject: [PATCH] Added some POW code, it probably doesn't work yet (takes very long and uses lots of battery, but I didn't get a result yet) --- domain/build.gradle | 1 + .../bitmessage/entity/ObjectMessage.java | 41 ++++-- .../bitmessage/entity/payload/V2Pubkey.java | 4 +- .../ch/dissem/bitmessage/factory/Factory.java | 4 +- .../bitmessage/factory/V3MessageFactory.java | 2 +- .../ch/dissem/bitmessage/utils/Bytes.java | 51 +++++++ .../ch/dissem/bitmessage/utils/Encode.java | 27 ++++ .../ch/dissem/bitmessage/utils/Security.java | 133 +++++++++++++++--- .../ch/dissem/bitmessage/utils/BytesTest.java | 53 +++++++ .../dissem/bitmessage/utils/SecurityTest.java | 90 ++++++++++++ 10 files changed, 371 insertions(+), 35 deletions(-) create mode 100644 domain/src/main/java/ch/dissem/bitmessage/utils/Bytes.java create mode 100644 domain/src/test/java/ch/dissem/bitmessage/utils/BytesTest.java create mode 100644 domain/src/test/java/ch/dissem/bitmessage/utils/SecurityTest.java diff --git a/domain/build.gradle b/domain/build.gradle index f216ccc..8a74085 100644 --- a/domain/build.gradle +++ b/domain/build.gradle @@ -9,5 +9,6 @@ repositories { dependencies { compile 'org.slf4j:slf4j-api:1.7.12' + compile 'org.bouncycastle:bcprov-jdk15on:1.52' testCompile group: 'junit', name: 'junit', version: '4.11' } \ No newline at end of file diff --git a/domain/src/main/java/ch/dissem/bitmessage/entity/ObjectMessage.java b/domain/src/main/java/ch/dissem/bitmessage/entity/ObjectMessage.java index 23fcd16..efe708d 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/entity/ObjectMessage.java +++ b/domain/src/main/java/ch/dissem/bitmessage/entity/ObjectMessage.java @@ -20,6 +20,7 @@ import ch.dissem.bitmessage.entity.payload.ObjectPayload; import ch.dissem.bitmessage.entity.valueobject.InventoryVector; import ch.dissem.bitmessage.utils.Encode; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; @@ -27,7 +28,7 @@ import java.io.OutputStream; * The 'object' command sends an object that is shared throughout the network. */ public class ObjectMessage implements MessagePayload { - private long nonce; + private byte[] nonce; private long expiresTime; private long objectType; /** @@ -37,6 +38,7 @@ public class ObjectMessage implements MessagePayload { private long streamNumber; private ObjectPayload payload; + private byte[] payloadBytes; private ObjectMessage(Builder builder) { nonce = builder.nonce; @@ -52,6 +54,14 @@ public class ObjectMessage implements MessagePayload { return Command.OBJECT; } + public byte[] getNonce() { + return nonce; + } + + public long getExpiresTime() { + return expiresTime; + } + public ObjectPayload getPayload() { return payload; } @@ -63,16 +73,29 @@ public class ObjectMessage implements MessagePayload { @Override public void write(OutputStream stream) throws IOException { - Encode.int64(nonce, stream); - Encode.int64(expiresTime, stream); - Encode.int32(objectType, stream); - Encode.varInt(version, stream); - Encode.varInt(streamNumber, stream); - payload.write(stream); + stream.write(nonce); + stream.write(getPayloadBytes()); + } + + public byte[] getPayloadBytes() throws IOException { + if (payloadBytes == null) { + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + Encode.int64(expiresTime, stream); + Encode.int32(objectType, stream); + Encode.varInt(version, stream); + Encode.varInt(streamNumber, stream); + payload.write(stream); + payloadBytes = stream.toByteArray(); + } + return payloadBytes; + } + + public void setNonce(byte[] nonce) { + this.nonce = nonce; } public static final class Builder { - private long nonce; + private byte[] nonce; private long expiresTime; private long objectType; private long version; @@ -82,7 +105,7 @@ public class ObjectMessage implements MessagePayload { public Builder() { } - public Builder nonce(long nonce) { + public Builder nonce(byte[] nonce) { this.nonce = nonce; return this; } diff --git a/domain/src/main/java/ch/dissem/bitmessage/entity/payload/V2Pubkey.java b/domain/src/main/java/ch/dissem/bitmessage/entity/payload/V2Pubkey.java index 8cc242a..f2e3200 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/entity/payload/V2Pubkey.java +++ b/domain/src/main/java/ch/dissem/bitmessage/entity/payload/V2Pubkey.java @@ -27,8 +27,8 @@ import java.io.OutputStream; public class V2Pubkey implements Pubkey { protected long stream; protected int behaviorBitfield; - protected byte[] publicSigningKey; - protected byte[] publicEncryptionKey; + protected byte[] publicSigningKey; // 64 Bytes + protected byte[] publicEncryptionKey; // 64 Bytes protected V2Pubkey() { } diff --git a/domain/src/main/java/ch/dissem/bitmessage/factory/Factory.java b/domain/src/main/java/ch/dissem/bitmessage/factory/Factory.java index 2d7379b..0212ca3 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/factory/Factory.java +++ b/domain/src/main/java/ch/dissem/bitmessage/factory/Factory.java @@ -63,14 +63,14 @@ public class Factory { case 2: return new V2Pubkey.Builder() .streamNumber(streamNumber) - .behaviorBitfield(Decode.int64(stream)) + .behaviorBitfield((int) Decode.int64(stream)) .publicSigningKey(Decode.bytes(stream, 64)) .publicEncryptionKey(Decode.bytes(stream, 64)) .build(); case 3: V3Pubkey.Builder v3 = new V3Pubkey.Builder() .streamNumber(streamNumber) - .behaviorBitfield(Decode.int64(stream)) + .behaviorBitfield((int) Decode.int64(stream)) .publicSigningKey(Decode.bytes(stream, 64)) .publicEncryptionKey(Decode.bytes(stream, 64)) .nonceTrialsPerByte(Decode.varInt(stream)) diff --git a/domain/src/main/java/ch/dissem/bitmessage/factory/V3MessageFactory.java b/domain/src/main/java/ch/dissem/bitmessage/factory/V3MessageFactory.java index 6c3a9f6..bb6143f 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/factory/V3MessageFactory.java +++ b/domain/src/main/java/ch/dissem/bitmessage/factory/V3MessageFactory.java @@ -76,7 +76,7 @@ class V3MessageFactory { } private ObjectMessage parseObject(InputStream stream, int length) throws IOException { - long nonce = Decode.int64(stream); + byte nonce[] = Decode.bytes(stream, 8); long expiresTime = Decode.int64(stream); long objectType = Decode.uint32(stream); long version = Decode.varInt(stream); diff --git a/domain/src/main/java/ch/dissem/bitmessage/utils/Bytes.java b/domain/src/main/java/ch/dissem/bitmessage/utils/Bytes.java new file mode 100644 index 0000000..9ccd50a --- /dev/null +++ b/domain/src/main/java/ch/dissem/bitmessage/utils/Bytes.java @@ -0,0 +1,51 @@ +/* + * Copyright 2015 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.bitmessage.utils; + +/** + * A helper class for working with byte arrays interpreted as unsigned big endian integers. + */ +public class Bytes { + public static void inc(byte[] nonce) { + for (int i = nonce.length - 1; i >= 0; i--) { + nonce[i]++; + if (nonce[i] != 0) break; + } + } + + public static boolean lt(byte[] a, byte[] b) { + byte[] max = (a.length > b.length ? a : b); + byte[] min = (max == a ? b : a); + int diff = max.length - min.length; + + for (int i = 0; i < max.length - min.length; i++) { + if (max[i] != 0) return a != max; + } + for (int i = diff; i < max.length; i++) { + if (max[i] != min[i - diff]) { + return lt(max[i], min[i - diff]) == (a == max); + } + } + return false; + } + + private static boolean lt(byte a, byte b) { + if (a < 0) return b < 0 && a < b; + if (b < 0) return a >= 0 || a < b; + return a < b; + } +} diff --git a/domain/src/main/java/ch/dissem/bitmessage/utils/Encode.java b/domain/src/main/java/ch/dissem/bitmessage/utils/Encode.java index 1e925c5..d98d2b9 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/utils/Encode.java +++ b/domain/src/main/java/ch/dissem/bitmessage/utils/Encode.java @@ -16,9 +16,13 @@ package ch.dissem.bitmessage.utils; +import ch.dissem.bitmessage.entity.Streamable; + +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; import java.nio.ByteBuffer; +import java.util.Arrays; /** * This class handles encoding simple types from byte stream, according to @@ -76,4 +80,27 @@ public class Encode { varInt(bytes.length, stream); stream.write(bytes); } + + /** + * Returns an array of bytes representing the given streamable object. + */ + public static byte[] bytes(Streamable streamable) throws IOException { + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + streamable.write(stream); + return stream.toByteArray(); + } + + /** + * Returns the bytes of the given streamable object, 0-padded such that the final + * length is x*padding. + */ + public static byte[] bytes(Streamable streamable, int padding) throws IOException { + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + streamable.write(stream); + int offset = padding - stream.size() % padding; + int length = stream.size() + offset; + byte[] result = new byte[length]; + stream.write(result, offset, stream.size()); + return result; + } } diff --git a/domain/src/main/java/ch/dissem/bitmessage/utils/Security.java b/domain/src/main/java/ch/dissem/bitmessage/utils/Security.java index 90612ff..fdb7d41 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/utils/Security.java +++ b/domain/src/main/java/ch/dissem/bitmessage/utils/Security.java @@ -16,37 +16,128 @@ package ch.dissem.bitmessage.utils; +import ch.dissem.bitmessage.entity.ObjectMessage; +import org.bouncycastle.jce.provider.BouncyCastleProvider; + +import java.io.IOException; +import java.math.BigInteger; +import java.security.GeneralSecurityException; import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; + +import static ch.dissem.bitmessage.utils.Bytes.inc; /** * Provides some methods to help with hashing and encryption. */ public class Security { - public static byte[] sha512(byte[] data) { + private static final SecureRandom RANDOM = new SecureRandom(); + private static final BigInteger TWO = BigInteger.valueOf(2); + + static { + java.security.Security.addProvider(new BouncyCastleProvider()); + } + + public static byte[] sha512(byte[]... data) { + return hash("SHA-512", data); + } + + public static byte[] doubleSha512(byte[]... data) { + MessageDigest mda = md("SHA-512"); + for (byte[] d : data) { + mda.update(d); + } + return mda.digest(mda.digest()); + } + + public static byte[] ripemd160(byte[]... data) { + return hash("RIPEMD160", data); + } + + public static byte[] sha1(byte[]... data) { + return hash("SHA-1", data); + } + + public static byte[] randomBytes(int length) { + byte[] result = new byte[length]; + RANDOM.nextBytes(result); + return result; + } + + public static void doProofOfWork(ObjectMessage object, long nonceTrialsPerByte, long extraBytes) throws IOException { + // payload = embeddedTime + encodedObjectVersion + encodedStreamNumber + encrypted + byte[] payload = object.getPayloadBytes(); + // payloadLength = the length of payload, in bytes, + 8 (to account for the nonce which we will append later) + // TTL = the number of seconds in between now and the object expiresTime. + // initialHash = hash(payload) + byte[] initialHash = getInitialHash(object); + + byte[] target = getProofOfWorkTarget(object, nonceTrialsPerByte, extraBytes); + // start with trialValue = 99999999999999999999 + byte[] trialValue; + // also start with nonce = 0 where nonce is 8 bytes in length and can be hashed as if it is a string. + byte[] nonce = new byte[8]; + MessageDigest mda = md("SHA-512"); + do { + inc(nonce); + mda.update(nonce); + mda.update(initialHash); + trialValue = bytes(mda.digest(mda.digest()), 8); + } while (Bytes.lt(target, trialValue)); + object.setNonce(nonce); + } + + /** + * @param object + * @param nonceTrialsPerByte + * @param extraBytes + * @throws IOException if proof of work doesn't check out + */ + public static void checkProofOfWork(ObjectMessage object, long nonceTrialsPerByte, long extraBytes) throws IOException { + // nonce = the first 8 bytes of payload + byte[] nonce = object.getNonce(); + byte[] initialHash = getInitialHash(object); + // resultHash = hash(hash( nonce || initialHash )) + byte[] resultHash = Security.doubleSha512(nonce, initialHash); + // POWValue = the first eight bytes of resultHash converted to an integer + byte[] powValue = bytes(resultHash, 8); + + if (Bytes.lt(getProofOfWorkTarget(object, nonceTrialsPerByte, extraBytes), powValue)) { + throw new IOException("Insufficient proof of work"); + } + } + + private static byte[] getInitialHash(ObjectMessage object) throws IOException { + return Security.sha512(object.getPayloadBytes()); + } + + private static byte[] getProofOfWorkTarget(ObjectMessage object, long nonceTrialsPerByte, long extraBytes) throws IOException { + BigInteger TTL = BigInteger.valueOf(object.getExpiresTime() - (System.currentTimeMillis() / 1000)); + BigInteger numerator = TWO.pow(64); + BigInteger powLength = BigInteger.valueOf(object.getPayloadBytes().length + extraBytes); + BigInteger denominator = BigInteger.valueOf(nonceTrialsPerByte).multiply(powLength.add(powLength.multiply(TTL).divide(BigInteger.valueOf(2).pow(16)))); + return numerator.divide(denominator).toByteArray(); + } + + private static byte[] hash(String algorithm, byte[]... data) { + MessageDigest mda = md(algorithm); + for (byte[] d : data) { + mda.update(d); + } + return mda.digest(); + } + + private static MessageDigest md(String algorithm) { try { - MessageDigest mda = MessageDigest.getInstance("SHA-512"); - return mda.digest(data); - } catch (NoSuchAlgorithmException e) { + return MessageDigest.getInstance(algorithm, "BC"); + } catch (GeneralSecurityException e) { throw new RuntimeException(e); } } - public static byte[] doubleSha512(byte[] data) { - try { - MessageDigest mda = MessageDigest.getInstance("SHA-512"); - return mda.digest(mda.digest(data)); - } catch (NoSuchAlgorithmException e) { - throw new RuntimeException(e); - } - } - - public static byte[] ripemd160(byte[] data) { - try { - MessageDigest mda = MessageDigest.getInstance("RIPEMD-160"); - return mda.digest(data); - } catch (NoSuchAlgorithmException e) { - throw new RuntimeException(e); - } + private static byte[] bytes(byte[] data, int count) { + byte[] result = new byte[count]; + System.arraycopy(data, 0, result, 0, count); + return result; } } diff --git a/domain/src/test/java/ch/dissem/bitmessage/utils/BytesTest.java b/domain/src/test/java/ch/dissem/bitmessage/utils/BytesTest.java new file mode 100644 index 0000000..862158f --- /dev/null +++ b/domain/src/test/java/ch/dissem/bitmessage/utils/BytesTest.java @@ -0,0 +1,53 @@ +/* + * Copyright 2015 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.bitmessage.utils; + +import org.junit.Test; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.math.BigInteger; +import java.util.Random; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; + +/** + * Created by chris on 10.04.15. + */ +public class BytesTest { + @Test + public void testIncrement() throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Encode.int16(256, out); + + byte[] bytes = {0, -1}; + Bytes.inc(bytes); + assertArrayEquals(out.toByteArray(), bytes); + } + + @Test + public void testLowerThan() { + Random rnd = new Random(); + for (int i = 0; i < 1000; i++) { + BigInteger a = BigInteger.valueOf(rnd.nextLong()).pow((rnd.nextInt(5) + 1)).abs(); + BigInteger b = BigInteger.valueOf(rnd.nextLong()).pow((rnd.nextInt(5) + 1)).abs(); + System.out.println("a = " + a.toString(16) + "\tb = " + b.toString(16)); + assertEquals(a.compareTo(b) == -1, Bytes.lt(a.toByteArray(), b.toByteArray())); + } + } +} diff --git a/domain/src/test/java/ch/dissem/bitmessage/utils/SecurityTest.java b/domain/src/test/java/ch/dissem/bitmessage/utils/SecurityTest.java new file mode 100644 index 0000000..79862bc --- /dev/null +++ b/domain/src/test/java/ch/dissem/bitmessage/utils/SecurityTest.java @@ -0,0 +1,90 @@ +/* + * Copyright 2015 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.bitmessage.utils; + +import ch.dissem.bitmessage.entity.ObjectMessage; +import ch.dissem.bitmessage.entity.payload.GenericPayload; +import org.junit.Test; + +import javax.xml.bind.DatatypeConverter; +import java.io.IOException; +import java.util.Calendar; +import java.util.GregorianCalendar; + +import static org.junit.Assert.assertArrayEquals; + +/** + * Created by chris on 10.04.15. + */ +public class SecurityTest { + public static final byte[] TEST_VALUE = "teststring".getBytes(); + public static final byte[] TEST_SHA1 = DatatypeConverter.parseHexBinary("" + + "b8473b86d4c2072ca9b08bd28e373e8253e865c4"); + public static final byte[] TEST_SHA512 = DatatypeConverter.parseHexBinary("" + + "6253b39071e5df8b5098f59202d414c37a17d6a38a875ef5f8c7d89b0212b028" + + "692d3d2090ce03ae1de66c862fa8a561e57ed9eb7935ce627344f742c0931d72"); + public static final byte[] TEST_RIPEMD160 = DatatypeConverter.parseHexBinary("" + + "cd566972b5e50104011a92b59fa8e0b1234851ae"); + + @Test + public void testRipemd160() { + assertArrayEquals(TEST_RIPEMD160, Security.ripemd160(TEST_VALUE)); + } + + @Test + public void testSha1() { + assertArrayEquals(TEST_SHA1, Security.sha1(TEST_VALUE)); + } + + @Test + public void testSha512() { + assertArrayEquals(TEST_SHA512, Security.sha512(TEST_VALUE)); + } + + @Test + public void testChaining() { + assertArrayEquals(TEST_SHA512, Security.sha512("test".getBytes(), "string".getBytes())); + } + + @Test + public void testDoubleHash() { + assertArrayEquals(Security.sha512(TEST_SHA512), Security.doubleSha512(TEST_VALUE)); + } + + @Test(expected = IOException.class) + public void testProofOfWorkFails() throws IOException { + ObjectMessage objectMessage = new ObjectMessage.Builder() + .nonce(new byte[8]) + .expiresTime(300 + (System.currentTimeMillis() / 1000)) // 5 minutes + .payload(new GenericPayload(1, new byte[0])) + .build(); + Security.checkProofOfWork(objectMessage, 1000, 1000); + } + + @Test + public void testDoProofOfWork() throws IOException { + Calendar expires = new GregorianCalendar(); + expires.add(1, Calendar.HOUR); + ObjectMessage objectMessage = new ObjectMessage.Builder() + .nonce(new byte[8]) + .expiresTime(expires.getTimeInMillis() / 1000) + .payload(new GenericPayload(1, new byte[0])) + .build(); + Security.doProofOfWork(objectMessage, 1000, 1000); + Security.checkProofOfWork(objectMessage, 1000, 1000); + } +}