diff --git a/domain/src/main/java/ch/dissem/bitmessage/Context.java b/domain/src/main/java/ch/dissem/bitmessage/Context.java index 2474779..02cad5f 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/Context.java +++ b/domain/src/main/java/ch/dissem/bitmessage/Context.java @@ -16,7 +16,7 @@ package ch.dissem.bitmessage; -import ch.dissem.bitmessage.ports.AddressRepository; +import ch.dissem.bitmessage.ports.NodeRegistry; import ch.dissem.bitmessage.ports.Inventory; import ch.dissem.bitmessage.ports.NetworkHandler; @@ -32,7 +32,7 @@ public class Context { private static Context instance; private Inventory inventory; - private AddressRepository addressRepo; + private NodeRegistry addressRepo; private NetworkHandler networkHandler; private Collection streams = new TreeSet<>(); @@ -42,7 +42,7 @@ public class Context { private long networkNonceTrialsPerByte = 1000; private long networkExtraBytes = 1000; - private Context(Inventory inventory, AddressRepository addressRepo, + private Context(Inventory inventory, NodeRegistry addressRepo, NetworkHandler networkHandler, int port) { this.inventory = inventory; this.addressRepo = addressRepo; @@ -50,8 +50,8 @@ public class Context { this.port = port; } - public static void init(Inventory inventory, AddressRepository addressRepository, NetworkHandler networkHandler, int port) { - instance = new Context(inventory, addressRepository, networkHandler, port); + public static void init(Inventory inventory, NodeRegistry nodeRegistry, NetworkHandler networkHandler, int port) { + instance = new Context(inventory, nodeRegistry, networkHandler, port); } public static Context getInstance() { @@ -62,7 +62,7 @@ public class Context { return inventory; } - public AddressRepository getAddressRepository() { + public NodeRegistry getAddressRepository() { return addressRepo; } diff --git a/domain/src/main/java/ch/dissem/bitmessage/entity/BitmessageAddress.java b/domain/src/main/java/ch/dissem/bitmessage/entity/BitmessageAddress.java index 73adc67..9eb5955 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/entity/BitmessageAddress.java +++ b/domain/src/main/java/ch/dissem/bitmessage/entity/BitmessageAddress.java @@ -17,55 +17,115 @@ package ch.dissem.bitmessage.entity; import ch.dissem.bitmessage.entity.payload.Pubkey; -import ch.dissem.bitmessage.utils.Base58; -import ch.dissem.bitmessage.utils.Encode; -import ch.dissem.bitmessage.utils.Security; +import ch.dissem.bitmessage.entity.valueobject.PrivateKey; +import ch.dissem.bitmessage.utils.*; +import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; - -import static ch.dissem.bitmessage.utils.Security.ripemd160; -import static ch.dissem.bitmessage.utils.Security.sha512; +import java.util.Arrays; /** * A Bitmessage address. Can be a user's private address, an address string without public keys or a recipient's address * holding private keys. */ -public abstract class BitmessageAddress { +public class BitmessageAddress { private long version; - private long streamNumber; + private long stream; + private byte[] ripe; + private String address; + + private PrivateKey privateKey; private Pubkey pubkey; - public BitmessageAddress(Pubkey pubkey) { - this.pubkey = pubkey; + private String alias; + + public BitmessageAddress(PrivateKey privateKey) { + this.privateKey = privateKey; + this.pubkey = privateKey.getPubkey(); + this.ripe = pubkey.getRipe(); + this.address = generateAddress(); } public BitmessageAddress(String address) { - Base58.decode(address.substring(3)); - } - - @Override - public String toString() { try { - byte[] combinedKeys = new byte[pubkey.getSigningKey().length + pubkey.getEncryptionKey().length]; - System.arraycopy(pubkey.getSigningKey(), 0, combinedKeys, 0, pubkey.getSigningKey().length); - System.arraycopy(pubkey.getEncryptionKey(), 0, combinedKeys, pubkey.getSigningKey().length, pubkey.getEncryptionKey().length); - - byte[] hash = ripemd160(sha512(combinedKeys)); - - ByteArrayOutputStream stream = new ByteArrayOutputStream(); - Encode.varInt(version, stream); - Encode.varInt(streamNumber, stream); - stream.write(hash); - - byte[] checksum = Security.doubleSha512(stream.toByteArray()); - for (int i = 0; i < 4; i++) { - stream.write(checksum[i]); - } - return "BM-" + Base58.encode(stream.toByteArray()); + byte[] bytes = Base58.decode(address.substring(3)); + ByteArrayInputStream in = new ByteArrayInputStream(bytes); + AccessCounter counter = new AccessCounter(); + this.version = Decode.varInt(in, counter); + this.stream = Decode.varInt(in, counter); + this.ripe = Decode.bytes(in, bytes.length - counter.length() - 4); + testChecksum(Decode.bytes(in, 4), bytes); + this.address = generateAddress(); } catch (IOException e) { throw new RuntimeException(e); } } + + private void testChecksum(byte[] expected, byte[] address) { + byte[] checksum = Security.doubleSha512(address, address.length - 4); + for (int i = 0; i < 4; i++) { + if (expected[i] != checksum[i]) throw new IllegalArgumentException("Checksum of address failed"); + } + } + + private String generateAddress() { + try { + ByteArrayOutputStream os = new ByteArrayOutputStream(); + Encode.varInt(version, os); + Encode.varInt(stream, os); + os.write(ripe); + + byte[] checksum = Security.doubleSha512(os.toByteArray()); + for (int i = 0; i < 4; i++) { + os.write(checksum[i]); + } + return "BM-" + Base58.encode(os.toByteArray()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public long getStream() { + return stream; + } + + public long getVersion() { + return version; + } + + public Pubkey getPubkey() { + return pubkey; + } + + public void setPubkey(Pubkey pubkey) { + if (!Arrays.equals(ripe, pubkey.getRipe())) throw new IllegalArgumentException("Pubkey has incompatible RIPE"); + this.pubkey = pubkey; + } + + public PrivateKey getPrivateKey() { + return privateKey; + } + + public void setAlias(String alias) { + this.alias = alias; + } + + public String getAddress() { + return address; + } + + public String getAlias() { + return alias; + } + + @Override + public String toString() { + return alias != null ? alias : address; + } + + public byte[] getRipe() { + return ripe; + } } diff --git a/domain/src/main/java/ch/dissem/bitmessage/entity/payload/Pubkey.java b/domain/src/main/java/ch/dissem/bitmessage/entity/payload/Pubkey.java index 5bfedde..828b563 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/entity/payload/Pubkey.java +++ b/domain/src/main/java/ch/dissem/bitmessage/entity/payload/Pubkey.java @@ -16,24 +16,63 @@ package ch.dissem.bitmessage.entity.payload; +import java.util.ArrayList; + +import static ch.dissem.bitmessage.utils.Security.ripemd160; +import static ch.dissem.bitmessage.utils.Security.sha512; + /** * Public keys for signing and encryption, the answer to a 'getpubkey' request. */ -public interface Pubkey extends ObjectPayload { - // bits 0 through 29 are yet undefined +public abstract class Pubkey implements ObjectPayload { + public final static long LATEST_VERSION = 4; + + public abstract long getVersion(); + + public abstract byte[] getSigningKey(); + + public abstract byte[] getEncryptionKey(); + + public byte[] getRipe() { + return ripemd160(sha512(getSigningKey(), getEncryptionKey())); + } + /** - * Receiving node expects that the RIPE hash encoded in their address preceedes the encrypted message data of msg - * messages bound for them. + * Bits 0 through 29 are yet undefined */ - int FEATURE_INCLUDE_DESTINATION = 30; - /** - * If true, the receiving node does send acknowledgements (rather than dropping them). - */ - int FEATURE_DOES_ACK = 31; + public enum Feature { + /** + * Receiving node expects that the RIPE hash encoded in their address preceedes the encrypted message data of msg + * messages bound for them. + */ + INCLUDE_DESTINATION(1 << 30), + /** + * If true, the receiving node does send acknowledgements (rather than dropping them). + */ + DOES_ACK(1 << 31); - long getVersion(); + private int bit; - byte[] getSigningKey(); + Feature(int bit) { + this.bit = bit; + } - byte[] getEncryptionKey(); + public static int bitfield(Feature... features) { + int bits = 0; + for (Feature feature : features) { + bits |= feature.bit; + } + return bits; + } + + public static Feature[] features(int bitfield){ + ArrayList features = new ArrayList<>(Feature.values().length); + for (Feature feature:Feature.values()){ + if ((bitfield & feature.bit) != 0){ + features.add(feature); + } + } + return features.toArray(new Feature[features.size()]); + } + } } 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 205224b..6675e96 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 @@ -26,7 +26,7 @@ import java.io.OutputStream; /** * A version 2 public key. */ -public class V2Pubkey implements Pubkey { +public class V2Pubkey extends Pubkey { protected long stream; protected int behaviorBitfield; protected byte[] publicSigningKey; // 64 Bytes @@ -44,7 +44,7 @@ public class V2Pubkey implements Pubkey { public static V2Pubkey read(InputStream is, long stream) throws IOException { return new V2Pubkey.Builder() - .streamNumber(stream) + .stream(stream) .behaviorBitfield((int) Decode.uint32(is)) .publicSigningKey(Decode.bytes(is, 64)) .publicEncryptionKey(Decode.bytes(is, 64)) @@ -87,7 +87,7 @@ public class V2Pubkey implements Pubkey { public Builder() { } - public Builder streamNumber(long streamNumber) { + public Builder stream(long streamNumber) { this.streamNumber = streamNumber; return this; } diff --git a/domain/src/main/java/ch/dissem/bitmessage/entity/payload/V3Pubkey.java b/domain/src/main/java/ch/dissem/bitmessage/entity/payload/V3Pubkey.java index 053b51f..829b5e5 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/entity/payload/V3Pubkey.java +++ b/domain/src/main/java/ch/dissem/bitmessage/entity/payload/V3Pubkey.java @@ -44,7 +44,7 @@ public class V3Pubkey extends V2Pubkey { public static V3Pubkey read(InputStream is, long stream) throws IOException { V3Pubkey.Builder v3 = new V3Pubkey.Builder() - .streamNumber(stream) + .stream(stream) .behaviorBitfield((int) Decode.uint32(is)) .publicSigningKey(Decode.bytes(is, 64)) .publicEncryptionKey(Decode.bytes(is, 64)) @@ -82,7 +82,7 @@ public class V3Pubkey extends V2Pubkey { public Builder() { } - public Builder streamNumber(long streamNumber) { + public Builder stream(long streamNumber) { this.streamNumber = streamNumber; return this; } diff --git a/domain/src/main/java/ch/dissem/bitmessage/entity/payload/V4Pubkey.java b/domain/src/main/java/ch/dissem/bitmessage/entity/payload/V4Pubkey.java index 4ad5738..54e6644 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/entity/payload/V4Pubkey.java +++ b/domain/src/main/java/ch/dissem/bitmessage/entity/payload/V4Pubkey.java @@ -28,7 +28,7 @@ import java.io.OutputStream; * use that pubkey. This prevents people from gathering pubkeys sent around the network and using the data from them * to create messages to be used in spam or in flooding attacks. */ -public class V4Pubkey implements Pubkey { +public class V4Pubkey extends Pubkey { private long stream; private byte[] tag; private byte[] encrypted; @@ -47,7 +47,7 @@ public class V4Pubkey implements Pubkey { // TODO: this.encrypted } - public static ObjectPayload read(InputStream stream, long streamNumber, int length) throws IOException { + public static V4Pubkey read(InputStream stream, long streamNumber, int length) throws IOException { return new V4Pubkey(streamNumber, Decode.bytes(stream, 32), Decode.bytes(stream, length - 32)); } diff --git a/domain/src/main/java/ch/dissem/bitmessage/entity/valueobject/PrivateKey.java b/domain/src/main/java/ch/dissem/bitmessage/entity/valueobject/PrivateKey.java new file mode 100644 index 0000000..644382e --- /dev/null +++ b/domain/src/main/java/ch/dissem/bitmessage/entity/valueobject/PrivateKey.java @@ -0,0 +1,92 @@ +/* + * 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.entity.valueobject; + +import ch.dissem.bitmessage.entity.Streamable; +import ch.dissem.bitmessage.entity.payload.Pubkey; +import ch.dissem.bitmessage.factory.Factory; +import ch.dissem.bitmessage.utils.Bytes; +import ch.dissem.bitmessage.utils.Decode; +import ch.dissem.bitmessage.utils.Encode; +import ch.dissem.bitmessage.utils.Security; + +import java.io.*; + +/** + * Created by chris on 18.04.15. + */ +public class PrivateKey implements Streamable { + private final byte[] privateSigningKey; // 32 bytes + private final byte[] privateEncryptionKey; // 32 bytes + + private final Pubkey pubkey; + + public PrivateKey(long nonceTrialsPerByte, long extraBytes, Pubkey.Feature... features) { + this.privateSigningKey = Security.randomBytes(64); + this.privateEncryptionKey = Security.randomBytes(64); + this.pubkey = Security.createPubkey(Pubkey.LATEST_VERSION, privateSigningKey, privateEncryptionKey, + nonceTrialsPerByte, extraBytes, features); + } + + private PrivateKey(byte[] privateSigningKey, byte[] privateEncryptionKey, Pubkey pubkey) { + this.privateSigningKey = privateSigningKey; + this.privateEncryptionKey = privateEncryptionKey; + this.pubkey = pubkey; + } + + public PrivateKey(long version, String passphrase, long nonceTrialsPerByte, long extraBytes, Pubkey.Feature... features) { + try { + // FIXME: this is most definitely wrong + this.privateSigningKey = Bytes.truncate(Security.sha512(passphrase.getBytes("UTF-8"), new byte[]{0}), 32); + this.privateEncryptionKey = Bytes.truncate(Security.sha512(passphrase.getBytes("UTF-8"), new byte[]{1}), 32); + this.pubkey = Security.createPubkey(version, privateSigningKey, privateEncryptionKey, + nonceTrialsPerByte, extraBytes, features); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } + + public static PrivateKey read(InputStream is) throws IOException { + int version = (int) Decode.varInt(is); + long stream = Decode.varInt(is); + int len = (int) Decode.varInt(is); + Pubkey pubkey = Factory.readPubkey(version, stream, is, len); + len = (int) Decode.varInt(is); + byte[] signingKey = Decode.bytes(is, len); + len = (int) Decode.varInt(is); + byte[] encryptionKey = Decode.bytes(is, len); + return new PrivateKey(signingKey, encryptionKey, pubkey); + } + + public Pubkey getPubkey() { + return pubkey; + } + + @Override + public void write(OutputStream os) throws IOException { + Encode.varInt(pubkey.getVersion(), os); + Encode.varInt(pubkey.getStream(), os); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + pubkey.write(baos); + Encode.varInt(baos.size(), os); + os.write(baos.toByteArray()); + Encode.varInt(privateSigningKey.length, os); + os.write(privateSigningKey); + Encode.varInt(privateEncryptionKey.length, os); + os.write(privateEncryptionKey); + } +} 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 a40ae2c..e19ce83 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/factory/Factory.java +++ b/domain/src/main/java/ch/dissem/bitmessage/factory/Factory.java @@ -16,10 +16,11 @@ package ch.dissem.bitmessage.factory; +import ch.dissem.bitmessage.entity.BitmessageAddress; import ch.dissem.bitmessage.entity.NetworkMessage; import ch.dissem.bitmessage.entity.ObjectMessage; import ch.dissem.bitmessage.entity.payload.*; -import ch.dissem.bitmessage.utils.Decode; +import ch.dissem.bitmessage.entity.valueobject.PrivateKey; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -53,17 +54,60 @@ public class Factory { } } + public static Pubkey createPubkey(long version, byte[] publicSigningKey, byte[] publicEncryptionKey, + long nonceTrialsPerByte, long extraBytes, Pubkey.Feature... features) { + if (publicSigningKey.length != 64) + throw new IllegalArgumentException("64 bytes signing key expected, but it was " + + publicSigningKey.length + " bytes long."); + if (publicEncryptionKey.length != 64) + throw new IllegalArgumentException("64 bytes encryption key expected, but it was " + + publicEncryptionKey.length + " bytes long."); + + switch ((int) version) { + case 2: + return new V2Pubkey.Builder() + .publicSigningKey(publicSigningKey) + .publicEncryptionKey(publicEncryptionKey) + .behaviorBitfield(Pubkey.Feature.bitfield(features)) + .build(); + case 3: + return new V3Pubkey.Builder() + .publicSigningKey(publicSigningKey) + .publicEncryptionKey(publicEncryptionKey) + .behaviorBitfield(Pubkey.Feature.bitfield(features)) + .nonceTrialsPerByte(nonceTrialsPerByte) + .extraBytes(extraBytes) + .build(); + case 4: + return new V4Pubkey( + new V3Pubkey.Builder() + .publicSigningKey(publicSigningKey) + .publicEncryptionKey(publicEncryptionKey) + .behaviorBitfield(Pubkey.Feature.bitfield(features)) + .nonceTrialsPerByte(nonceTrialsPerByte) + .extraBytes(extraBytes) + .build() + ); + default: + throw new IllegalArgumentException("Unexpected pubkey version " + version); + } + } + + public static BitmessageAddress generatePrivateAddress(Pubkey.Feature... features) { + return new BitmessageAddress(new PrivateKey(1000, 1000, features)); + } + static ObjectPayload getObjectPayload(long objectType, long version, long streamNumber, InputStream stream, int length) throws IOException { if (objectType < 4) { switch ((int) objectType) { case 0: - return parseGetPubkey((int) version, streamNumber, stream, length); + return parseGetPubkey(version, streamNumber, stream, length); case 1: - return parsePubkey((int) version, streamNumber, stream, length); + return parsePubkey(version, streamNumber, stream, length); case 2: - return parseMsg((int) version, streamNumber, stream, length); + return parseMsg(version, streamNumber, stream, length); case 3: - return parseBroadcast((int) version, streamNumber, stream, length); + return parseBroadcast(version, streamNumber, stream, length); default: LOG.error("This should not happen, someone broke something in the code!"); } @@ -73,29 +117,34 @@ public class Factory { return GenericPayload.read(stream, streamNumber, length); } - private static ObjectPayload parseGetPubkey(int version, long streamNumber, InputStream stream, int length) throws IOException { + private static ObjectPayload parseGetPubkey(long version, long streamNumber, InputStream stream, int length) throws IOException { return GetPubkey.read(stream, streamNumber, length); } - private static ObjectPayload parsePubkey(int version, long streamNumber, InputStream stream, int length) throws IOException { - switch (version) { + public static Pubkey readPubkey(long version, long stream, InputStream is, int length) throws IOException { + switch ((int) version) { case 2: - return V2Pubkey.read(stream, streamNumber); + return V2Pubkey.read(is, stream); case 3: - return V3Pubkey.read(stream, streamNumber); + return V3Pubkey.read(is, stream); case 4: - return V4Pubkey.read(stream, streamNumber, length); + return V4Pubkey.read(is, stream, length); } LOG.debug("Unexpected pubkey version " + version + ", handling as generic payload object"); - return GenericPayload.read(stream, streamNumber, length); + return null; } - private static ObjectPayload parseMsg(int version, long streamNumber, InputStream stream, int length) throws IOException { + private static ObjectPayload parsePubkey(long version, long streamNumber, InputStream stream, int length) throws IOException { + Pubkey pubkey = readPubkey(version, streamNumber, stream, length); + return pubkey != null ? pubkey : GenericPayload.read(stream, streamNumber, length); + } + + private static ObjectPayload parseMsg(long version, long streamNumber, InputStream stream, int length) throws IOException { return Msg.read(stream, streamNumber, length); } - private static ObjectPayload parseBroadcast(int version, long streamNumber, InputStream stream, int length) throws IOException { - switch (version) { + private static ObjectPayload parseBroadcast(long version, long streamNumber, InputStream stream, int length) throws IOException { + switch ((int) version) { case 4: return V4Broadcast.read(stream, streamNumber, length); case 5: diff --git a/domain/src/main/java/ch/dissem/bitmessage/ports/AddressRepository.java b/domain/src/main/java/ch/dissem/bitmessage/ports/AddressRepository.java index 6469c8d..90edb4b 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/ports/AddressRepository.java +++ b/domain/src/main/java/ch/dissem/bitmessage/ports/AddressRepository.java @@ -16,15 +16,25 @@ package ch.dissem.bitmessage.ports; -import ch.dissem.bitmessage.entity.valueobject.NetworkAddress; +import ch.dissem.bitmessage.entity.BitmessageAddress; import java.util.List; /** - * Stores and provides known peers. + * Created by chris on 23.04.15. */ public interface AddressRepository { - List getKnownAddresses(int limit, long... streams); + /** + * Returns all Bitmessage addresses that belong to this user, i.e. have a private key. + */ + List findIdentities(); - void offerAddresses(List addresses); + /** + * Returns all Bitmessage addresses that have no private key. + */ + List findContacts(); + + void save(BitmessageAddress address); + + void remove(BitmessageAddress address); } diff --git a/domain/src/main/java/ch/dissem/bitmessage/ports/NodeRegistry.java b/domain/src/main/java/ch/dissem/bitmessage/ports/NodeRegistry.java new file mode 100644 index 0000000..a1d33ad --- /dev/null +++ b/domain/src/main/java/ch/dissem/bitmessage/ports/NodeRegistry.java @@ -0,0 +1,30 @@ +/* + * 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.ports; + +import ch.dissem.bitmessage.entity.valueobject.NetworkAddress; + +import java.util.List; + +/** + * Stores and provides known peers. + */ +public interface NodeRegistry { + List getKnownAddresses(int limit, long... streams); + + void offerAddresses(List addresses); +} diff --git a/domain/src/main/java/ch/dissem/bitmessage/utils/Bytes.java b/domain/src/main/java/ch/dissem/bitmessage/utils/Bytes.java index 0da0eb6..7a466c2 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/utils/Bytes.java +++ b/domain/src/main/java/ch/dissem/bitmessage/utils/Bytes.java @@ -78,9 +78,25 @@ public class Bytes { return result; } + /** + * Returns a new byte array containing the first size bytes of the given array. + */ public static byte[] truncate(byte[] source, int size) { byte[] result = new byte[size]; System.arraycopy(source, 0, result, 0, size); return result; } + + public static byte[] subArray(byte[] source, int offset, int length) { + byte[] result = new byte[length]; + System.arraycopy(source, offset, result, 0, length); + return result; + } + + public static byte[] concatenate(byte first, byte[] bytes) { + byte[] result = new byte[bytes.length + 1]; + result[0] = first; + System.arraycopy(bytes, 0, result, 1, bytes.length); + 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 94a9af9..80ae000 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/utils/Security.java +++ b/domain/src/main/java/ch/dissem/bitmessage/utils/Security.java @@ -17,7 +17,12 @@ package ch.dissem.bitmessage.utils; import ch.dissem.bitmessage.entity.ObjectMessage; +import ch.dissem.bitmessage.entity.payload.Pubkey; +import ch.dissem.bitmessage.factory.Factory; import ch.dissem.bitmessage.ports.ProofOfWorkEngine; +import org.bouncycastle.asn1.sec.SECNamedCurves; +import org.bouncycastle.asn1.x9.X9ECParameters; +import org.bouncycastle.crypto.params.ECDomainParameters; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -35,6 +40,8 @@ public class Security { public static final Logger LOG = LoggerFactory.getLogger(Security.class); private static final SecureRandom RANDOM = new SecureRandom(); private static final BigInteger TWO = BigInteger.valueOf(2); + private static final X9ECParameters EC_CURVE = SECNamedCurves.getByName("secp256k1"); + private static final ECDomainParameters EC_PARAMETERS = new ECDomainParameters(EC_CURVE.getCurve(), EC_CURVE.getG(), EC_CURVE.getN(), EC_CURVE.getH()); static { java.security.Security.addProvider(new BouncyCastleProvider()); @@ -52,6 +59,12 @@ public class Security { return mda.digest(mda.digest()); } + public static byte[] doubleSha512(byte[] data, int length) { + MessageDigest mda = md("SHA-512"); + mda.update(data, 0, length); + return mda.digest(mda.digest()); + } + public static byte[] ripemd160(byte[]... data) { return hash("RIPEMD160", data); } @@ -115,4 +128,16 @@ public class Security { throw new RuntimeException(e); } } + + public static Pubkey createPubkey(long version, byte[] privateSigningKey, byte[] privateEncryptionKey, + long nonceTrialsPerByte, long extraBytes, Pubkey.Feature... features) { + byte[] publicSigningKey = EC_PARAMETERS.getG().multiply(keyToBigInt(privateSigningKey)).getEncoded(false); + byte[] publicEncryptionKey = EC_PARAMETERS.getG().multiply(keyToBigInt(privateEncryptionKey)).getEncoded(false); + return Factory.createPubkey(Bytes.subArray(publicSigningKey, 1, 64), Bytes.subArray(publicEncryptionKey, 1, 64), + nonceTrialsPerByte, extraBytes, features); + } + + private static BigInteger keyToBigInt(byte[] key) { + return new BigInteger(Bytes.concatenate((byte) 0x00, key)); + } } diff --git a/domain/src/test/java/ch/dissem/bitmessage/entity/BitmessageAddressTest.java b/domain/src/test/java/ch/dissem/bitmessage/entity/BitmessageAddressTest.java new file mode 100644 index 0000000..d49b84e --- /dev/null +++ b/domain/src/test/java/ch/dissem/bitmessage/entity/BitmessageAddressTest.java @@ -0,0 +1,48 @@ +/* + * 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.entity; + +import ch.dissem.bitmessage.entity.valueobject.PrivateKey; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +public class BitmessageAddressTest { + @Test + public void ensureAddressStaysSame() { + String address = "BM-2D9Vc5rFxxR5vTi53T9gkLfemViHRMVLQZ"; + assertEquals(address, new BitmessageAddress(address).toString()); + } + + @Test + public void ensureStreamAndVersionAreParsed() { + BitmessageAddress address = new BitmessageAddress("BM-2D9Vc5rFxxR5vTi53T9gkLfemViHRMVLQZ"); + assertEquals(1, address.getStream()); + assertEquals(3, address.getVersion()); + + address = new BitmessageAddress("BM-87hJ99tPAXxtetvnje7Z491YSvbEtBJVc5e"); + assertEquals(1, address.getStream()); + assertEquals(4, address.getVersion()); + } + + @Test + public void testCreateAddress() { + BitmessageAddress address = new BitmessageAddress(new PrivateKey(0, 0)); + assertNotNull(address.getPubkey()); + } +} diff --git a/inventory/src/main/java/ch/dissem/bitmessage/inventory/DatabaseRepository.java b/inventory/src/main/java/ch/dissem/bitmessage/inventory/DatabaseRepository.java index d355d8f..4c05d42 100644 --- a/inventory/src/main/java/ch/dissem/bitmessage/inventory/DatabaseRepository.java +++ b/inventory/src/main/java/ch/dissem/bitmessage/inventory/DatabaseRepository.java @@ -17,17 +17,19 @@ package ch.dissem.bitmessage.inventory; import ch.dissem.bitmessage.entity.ObjectMessage; +import ch.dissem.bitmessage.entity.Streamable; import ch.dissem.bitmessage.entity.valueobject.InventoryVector; import ch.dissem.bitmessage.entity.valueobject.NetworkAddress; import ch.dissem.bitmessage.factory.Factory; -import ch.dissem.bitmessage.ports.AddressRepository; import ch.dissem.bitmessage.ports.Inventory; +import ch.dissem.bitmessage.ports.NodeRegistry; import org.flywaydb.core.Flyway; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.sql.*; import java.util.LinkedList; import java.util.List; @@ -38,7 +40,7 @@ import static ch.dissem.bitmessage.utils.UnixTime.now; /** * Stores everything in a database */ -public class DatabaseRepository implements Inventory, AddressRepository { +public class DatabaseRepository implements Inventory, NodeRegistry { private static final Logger LOG = LoggerFactory.getLogger(DatabaseRepository.class); private static final String DB_URL = "jdbc:h2:~/jabit"; @@ -170,6 +172,13 @@ public class DatabaseRepository implements Inventory, AddressRepository { } } + protected void writeBlob(PreparedStatement ps, int parameterIndex, Streamable data) throws SQLException, IOException { + ByteArrayOutputStream os = new ByteArrayOutputStream(); + data.write(os); + ByteArrayInputStream is = new ByteArrayInputStream(os.toByteArray()); + ps.setBlob(parameterIndex, is); + } + @Override public void cleanup() { try { @@ -180,7 +189,7 @@ public class DatabaseRepository implements Inventory, AddressRepository { } } - private Connection getConnection() { + protected Connection getConnection() { try { return DriverManager.getConnection(DB_URL, DB_USER, DB_PWD); } catch (SQLException e) { diff --git a/inventory/src/main/java/ch/dissem/bitmessage/inventory/JdbcAddressRepository.java b/inventory/src/main/java/ch/dissem/bitmessage/inventory/JdbcAddressRepository.java new file mode 100644 index 0000000..7921030 --- /dev/null +++ b/inventory/src/main/java/ch/dissem/bitmessage/inventory/JdbcAddressRepository.java @@ -0,0 +1,101 @@ +/* + * 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.inventory; + +import ch.dissem.bitmessage.entity.BitmessageAddress; +import ch.dissem.bitmessage.entity.payload.Pubkey; +import ch.dissem.bitmessage.entity.valueobject.PrivateKey; +import ch.dissem.bitmessage.factory.Factory; +import ch.dissem.bitmessage.ports.AddressRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.sql.*; +import java.util.LinkedList; +import java.util.List; + +/** + * Created by chris on 23.04.15. + */ +public class JdbcAddressRepository extends DatabaseRepository implements AddressRepository { + private static final Logger LOG = LoggerFactory.getLogger(DatabaseRepository.class); + + @Override + public List findIdentities() { + return find("private_signing_key IS NOT NULL"); + } + + @Override + public List findContacts() { + return find("private_signing_key IS NULL"); + } + + private List find(String where) { + List result = new LinkedList<>(); + try { + Statement stmt = getConnection().createStatement(); + ResultSet rs = stmt.executeQuery("SELECT address, alias, public_key, private_key FROM Address WHERE " + where); + while (rs.next()) { + BitmessageAddress address; + Blob privateKeyBlob = rs.getBlob("private_key"); + if (privateKeyBlob != null) { + PrivateKey privateKey = PrivateKey.read(privateKeyBlob.getBinaryStream()); + address = new BitmessageAddress(privateKey); + } else { + address = new BitmessageAddress(rs.getString("address")); + Blob publicKeyBlob = rs.getBlob("public_key"); + if (publicKeyBlob != null) { + Pubkey pubkey = Factory.readPubkey(address.getVersion(), address.getStream(), + publicKeyBlob.getBinaryStream(), (int)publicKeyBlob.length()); + address.setPubkey(pubkey); + } + } + address.setAlias(rs.getString("alias")); + + result.add(address); + } + } catch (IOException | SQLException e) { + LOG.error(e.getMessage(), e); + } + return result; + } + + @Override + public void save(BitmessageAddress address) { + try { + PreparedStatement ps = getConnection().prepareStatement( + "INSERT INTO Address (address, alias, public_key, private_key) VALUES (?, ?, ?, ?, ?)"); + ps.setString(1, address.getAddress()); + ps.setString(2, address.getAlias()); + writeBlob(ps, 3, address.getPubkey()); + writeBlob(ps, 4, address.getPrivateKey()); + } catch (IOException | SQLException e) { + LOG.error(e.getMessage(), e); + } + } + + @Override + public void remove(BitmessageAddress address) { + try { + Statement stmt = getConnection().createStatement(); + stmt.executeUpdate("DELETE FROM Address WHERE address = '" + address.getAddress() + "'"); + } catch (SQLException e) { + LOG.error(e.getMessage(), e); + } + } +} diff --git a/inventory/src/main/java/ch/dissem/bitmessage/inventory/SimpleAddressRepository.java b/inventory/src/main/java/ch/dissem/bitmessage/inventory/SimpleNodeRegistry.java similarity index 90% rename from inventory/src/main/java/ch/dissem/bitmessage/inventory/SimpleAddressRepository.java rename to inventory/src/main/java/ch/dissem/bitmessage/inventory/SimpleNodeRegistry.java index 3fb19de..39a6fa4 100644 --- a/inventory/src/main/java/ch/dissem/bitmessage/inventory/SimpleAddressRepository.java +++ b/inventory/src/main/java/ch/dissem/bitmessage/inventory/SimpleNodeRegistry.java @@ -17,7 +17,7 @@ package ch.dissem.bitmessage.inventory; import ch.dissem.bitmessage.entity.valueobject.NetworkAddress; -import ch.dissem.bitmessage.ports.AddressRepository; +import ch.dissem.bitmessage.ports.NodeRegistry; import java.util.Collections; import java.util.List; @@ -25,7 +25,7 @@ import java.util.List; /** * Created by chris on 06.04.15. */ -public class SimpleAddressRepository implements AddressRepository { +public class SimpleNodeRegistry implements NodeRegistry { @Override public List getKnownAddresses(int limit, long... streams) { return Collections.singletonList(new NetworkAddress.Builder().ipv4(127, 0, 0, 1).port(8444).build()); diff --git a/inventory/src/main/resources/db/migration/V1.2__Create_address_table.sql b/inventory/src/main/resources/db/migration/V1.2__Create_address_table.sql new file mode 100644 index 0000000..bd1cb70 --- /dev/null +++ b/inventory/src/main/resources/db/migration/V1.2__Create_address_table.sql @@ -0,0 +1,6 @@ +CREATE TABLE Address ( + address VARCHAR(40) NOT NULL PRIMARY KEY, + alias VARCHAR(255), + public_key BLOB, + private_key BLOB +); \ No newline at end of file