From f23f432f07db7084fba1ed5d44d40232c9d2fc16 Mon Sep 17 00:00:00 2001 From: Christian Basler Date: Sat, 9 May 2015 17:27:45 +0200 Subject: [PATCH] Decryption works now! (well, for v4 pubkeys at least) --- .../bitmessage/entity/BitmessageAddress.java | 101 +++++---- .../bitmessage/entity/ObjectMessage.java | 2 +- .../bitmessage/entity/payload/CryptoBox.java | 203 ++++++++++++++++++ .../entity/payload/GenericPayload.java | 2 +- .../bitmessage/entity/payload/GetPubkey.java | 3 +- .../dissem/bitmessage/entity/payload/Msg.java | 37 ++-- .../bitmessage/entity/payload/Pubkey.java | 2 +- .../entity/payload/UnencryptedMessage.java | 51 +++-- .../bitmessage/entity/payload/V3Pubkey.java | 15 +- .../bitmessage/entity/payload/V4Pubkey.java | 22 +- .../ch/dissem/bitmessage/factory/Factory.java | 5 +- .../bitmessage/factory/V3MessageFactory.java | 25 ++- .../ch/dissem/bitmessage/utils/Decode.java | 20 ++ .../ch/dissem/bitmessage/utils/Security.java | 100 ++++++--- .../entity/BitmessageAddressTest.java | 13 +- .../bitmessage/entity/SerializationTest.java | 8 +- .../test/resources/V1MsgStrangeData.payload | Bin 0 -> 54 bytes domain/src/test/resources/V4Pubkey.payload | Bin 396 -> 396 bytes 18 files changed, 467 insertions(+), 142 deletions(-) create mode 100644 domain/src/main/java/ch/dissem/bitmessage/entity/payload/CryptoBox.java create mode 100644 domain/src/test/resources/V1MsgStrangeData.payload 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 0e7228c..a366a56 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/entity/BitmessageAddress.java +++ b/domain/src/main/java/ch/dissem/bitmessage/entity/BitmessageAddress.java @@ -17,11 +17,9 @@ package ch.dissem.bitmessage.entity; import ch.dissem.bitmessage.entity.payload.Pubkey; +import ch.dissem.bitmessage.entity.payload.V4Pubkey; import ch.dissem.bitmessage.entity.valueobject.PrivateKey; -import ch.dissem.bitmessage.utils.AccessCounter; -import ch.dissem.bitmessage.utils.Base58; -import ch.dissem.bitmessage.utils.Encode; -import ch.dissem.bitmessage.utils.Security; +import ch.dissem.bitmessage.utils.*; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -36,9 +34,14 @@ import static ch.dissem.bitmessage.utils.Decode.varInt; * holding private keys. */ public class BitmessageAddress { - private long version; - private long stream; - private byte[] ripe; + private final long version; + private final long stream; + private final byte[] ripe; + private final byte[] tag; + /** + * Used for V4 address encryption. It's easier to just create it regardless of address version. + */ + private final byte[] privateEncryptionKey; private String address; @@ -47,45 +50,55 @@ public class BitmessageAddress { 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) { + private BitmessageAddress(long version, long stream, byte[] ripe) { try { - byte[] bytes = Base58.decode(address.substring(3)); - ByteArrayInputStream in = new ByteArrayInputStream(bytes); - AccessCounter counter = new AccessCounter(); - this.version = varInt(in, counter); - this.stream = varInt(in, counter); - this.ripe = bytes(in, bytes.length - counter.length() - 4); - testChecksum(bytes(in, 4), bytes); - this.address = generateAddress(); + this.version = version; + this.stream = stream; + this.ripe = ripe; + + ByteArrayOutputStream os = new ByteArrayOutputStream(); + Encode.varInt(version, os); + Encode.varInt(stream, os); + // for the tag, the checksum has to be created with 0x00 padding + byte[] checksum = Security.doubleSha512(os.toByteArray(), ripe); + this.tag = Arrays.copyOfRange(checksum, 32, 64); + this.privateEncryptionKey = Arrays.copyOfRange(checksum, 0, 32); + // but for the address and its checksum they need to be stripped + os.write(Bytes.stripLeadingZeros(ripe)); + checksum = Security.doubleSha512(os.toByteArray(), ripe); + os.write(checksum, 0, 4); + this.address = "BM-" + Base58.encode(os.toByteArray()); } 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"); - } + public BitmessageAddress(PrivateKey privateKey) { + this(privateKey.getPubkey().getVersion(), privateKey.getPubkey().getStream(), privateKey.getPubkey().getRipe()); + this.privateKey = privateKey; + this.pubkey = privateKey.getPubkey(); } - private String generateAddress() { + public BitmessageAddress(String address) { try { - ByteArrayOutputStream os = new ByteArrayOutputStream(); - Encode.varInt(version, os); - Encode.varInt(stream, os); - os.write(ripe); + this.address = address; + byte[] bytes = Base58.decode(address.substring(3)); + ByteArrayInputStream in = new ByteArrayInputStream(bytes); + AccessCounter counter = new AccessCounter(); + this.version = varInt(in, counter); + this.stream = varInt(in, counter); + this.ripe = Bytes.expand(bytes(in, bytes.length - counter.length() - 4), 20); - byte[] checksum = Security.doubleSha512(os.toByteArray()); - os.write(checksum, 0, 4); - return "BM-" + Base58.encode(os.toByteArray()); + // test checksum + byte[] checksum = Security.doubleSha512(bytes, bytes.length - 4); + byte[] expectedChecksum = bytes(in, 4); + for (int i = 0; i < 4; i++) { + if (expectedChecksum[i] != checksum[i]) + throw new IllegalArgumentException("Checksum of address failed"); + } + checksum = Security.doubleSha512(Arrays.copyOfRange(bytes, 0, counter.length()), ripe); + this.tag = Arrays.copyOfRange(checksum, 32, 64); + this.privateEncryptionKey = Arrays.copyOfRange(checksum, 0, 32); } catch (IOException e) { throw new RuntimeException(e); } @@ -104,8 +117,18 @@ public class BitmessageAddress { } public void setPubkey(Pubkey pubkey) { + if (pubkey instanceof V4Pubkey) { + try { + V4Pubkey v4 = (V4Pubkey) pubkey; + if (!Arrays.equals(tag, v4.getTag())) + throw new IllegalArgumentException("Pubkey has incompatible tag"); + v4.decrypt(privateEncryptionKey); + } catch (IOException e) { + throw new RuntimeException(e); + } + } if (!Arrays.equals(ripe, pubkey.getRipe())) - throw new IllegalArgumentException("Pubkey has incompatible RIPE"); + throw new IllegalArgumentException("Pubkey has incompatible ripe"); this.pubkey = pubkey; } @@ -133,4 +156,8 @@ public class BitmessageAddress { public byte[] getRipe() { return ripe; } + + public byte[] getTag() { + return tag; + } } 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 67cea53..7259bad 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/entity/ObjectMessage.java +++ b/domain/src/main/java/ch/dissem/bitmessage/entity/ObjectMessage.java @@ -106,7 +106,7 @@ public class ObjectMessage implements MessagePayload { } public boolean isSignatureValid() throws IOException { - Pubkey pubkey=null; // TODO + Pubkey pubkey = null; // TODO return Security.isSignatureValid(getBytesToSign(), payload.getSignature(), pubkey); } diff --git a/domain/src/main/java/ch/dissem/bitmessage/entity/payload/CryptoBox.java b/domain/src/main/java/ch/dissem/bitmessage/entity/payload/CryptoBox.java new file mode 100644 index 0000000..68a7cda --- /dev/null +++ b/domain/src/main/java/ch/dissem/bitmessage/entity/payload/CryptoBox.java @@ -0,0 +1,203 @@ +/* + * 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.payload; + +import ch.dissem.bitmessage.entity.Streamable; +import ch.dissem.bitmessage.utils.*; +import org.bouncycastle.crypto.BufferedBlockCipher; +import org.bouncycastle.crypto.CipherParameters; +import org.bouncycastle.crypto.InvalidCipherTextException; +import org.bouncycastle.crypto.engines.AESEngine; +import org.bouncycastle.crypto.modes.CBCBlockCipher; +import org.bouncycastle.crypto.paddings.PKCS7Padding; +import org.bouncycastle.crypto.paddings.PaddedBufferedBlockCipher; +import org.bouncycastle.crypto.params.KeyParameter; +import org.bouncycastle.crypto.params.ParametersWithIV; +import org.bouncycastle.jce.interfaces.ECPublicKey; +import org.bouncycastle.math.ec.ECPoint; + +import java.io.*; +import java.math.BigInteger; +import java.util.Arrays; + + +/** + * Created by chris on 09.04.15. + */ +public class CryptoBox implements Streamable { + private final byte[] initializationVector; + private final int curveType; + private final byte[] xComponent; + private final byte[] yComponent; + private final byte[] mac; + private byte[] encrypted; + + public CryptoBox(Streamable data, byte[] encryptionKey) { + curveType = 0x02CA; + + // 1. The destination public key is called K. + ECPublicKey K = Security.getPublicKey(encryptionKey); + // 2. Generate 16 random bytes using a secure random number generator. Call them IV. + initializationVector = Security.randomBytes(16); + + // 3. Generate a new random EC key pair with private key called r and public key called R. + // TODO + BigInteger r = null; + // 4. Do an EC point multiply with public key K and private key r. This gives you public key P. + ECPoint P = K.getQ().multiply(r).normalize(); + xComponent = Bytes.stripLeadingZeros(P.getXCoord().getEncoded()); + yComponent = Bytes.stripLeadingZeros(P.getYCoord().getEncoded()); + // 5. Use the X component of public key P and calculate the SHA512 hash H. + byte[] H = Security.sha512(xComponent); + // 6. The first 32 bytes of H are called key_e and the last 32 bytes are called key_m. + byte[] key_e = Arrays.copyOfRange(H, 0, 32); + byte[] key_m = Arrays.copyOfRange(H, H.length - 32, 32); + // 7. Pad the input text to a multiple of 16 bytes, in accordance to PKCS7. + // 8. Encrypt the data with AES-256-CBC, using IV as initialization vector, key_e as encryption key and the padded input text as payload. Call the output cipher text. + encrypted = null; // TODO + // 9. Calculate a 32 byte MAC with HMACSHA256, using key_m as salt and IV + R + cipher text as data. Call the output MAC. + mac = null; // TODO + + // The resulting data is: IV + R + cipher text + MAC + } + + private CryptoBox(Builder builder) { + initializationVector = builder.initializationVector; + curveType = builder.curveType; + xComponent = builder.xComponent; + yComponent = builder.yComponent; + encrypted = builder.encrypted; + mac = builder.mac; + } + + public static CryptoBox read(InputStream stream, int length) throws IOException { + AccessCounter counter = new AccessCounter(); + return new Builder() + .IV(Decode.bytes(stream, 16, counter)) + .curveType(Decode.uint16(stream, counter)) + .X(Decode.shortVarBytes(stream, counter)) + .Y(Decode.shortVarBytes(stream, counter)) + .encrypted(Decode.bytes(stream, length - counter.length() - 32)) + .MAC(Decode.bytes(stream, 32)) + .build(); + } + + /** + * @see https://bitmessage.org/wiki/Encryption#Decryption + */ + public InputStream decrypt(byte[] privateKey) { + // 1. The private key used to decrypt is called k. + BigInteger K = Security.keyToBigInt(privateKey); + // 2. Do an EC point multiply with private key k and public key R. This gives you public key P. + ECPublicKey R = Security.getPublicKey(xComponent, yComponent); + ECPoint P = R.getQ().multiply(K).normalize(); + // 3. Use the X component of public key P and calculate the SHA512 hash H. + byte[] sha512key = Security.sha512(Bytes.expand(P.getXCoord().toBigInteger().toByteArray(), 32)); + // 4. The first 32 bytes of H are called key_e and the last 32 bytes are called key_m. + byte[] key_e = Arrays.copyOfRange(sha512key, 0, 32); + byte[] key_m = Arrays.copyOfRange(sha512key, 32, 64); + + // 5. Calculate MAC' with HMACSHA256, using key_m as salt and IV + R + cipher text as data. + ByteArrayOutputStream macData = new ByteArrayOutputStream(); + try { + writeWithoutMAC(macData); + } catch (IOException e) { + throw new RuntimeException(e); + } + // 6. Compare MAC with MAC'. If not equal, decryption will fail. + if (!Arrays.equals(mac, Security.mac(key_m, macData.toByteArray()))) { + throw new RuntimeException("Invalid MAC while decrypting"); + } + + // 7. Decrypt the cipher text with AES-256-CBC, using IV as initialization vector, key_e as decryption key + // and the cipher text as payload. The output is the padded input text. + BufferedBlockCipher cipher = new PaddedBufferedBlockCipher(new CBCBlockCipher(new AESEngine()), new PKCS7Padding()); + + CipherParameters params = new ParametersWithIV(new KeyParameter(key_e), initializationVector); + + cipher.init(false, params); + + byte[] buffer = new byte[cipher.getOutputSize(encrypted.length)]; + int length = cipher.processBytes(encrypted, 0, encrypted.length, buffer, 0); + try { + length += cipher.doFinal(buffer, length); + } catch (InvalidCipherTextException e) { + throw new IllegalArgumentException(e); + } + return new ByteArrayInputStream(buffer, 0, length); + } + + private void writeWithoutMAC(OutputStream stream) throws IOException { + stream.write(initializationVector); + Encode.int16(curveType, stream); + Encode.int16(xComponent.length, stream); + stream.write(xComponent); + Encode.int16(yComponent.length, stream); + stream.write(yComponent); + stream.write(encrypted); + } + + @Override + public void write(OutputStream stream) throws IOException { + writeWithoutMAC(stream); + stream.write(mac); + } + + public static final class Builder { + private byte[] initializationVector; + private int curveType; + private byte[] xComponent; + private byte[] yComponent; + private byte[] encrypted; + private byte[] mac; + + public Builder IV(byte[] initializationVector) { + this.initializationVector = initializationVector; + return this; + } + + public Builder curveType(int curveType) { + if (curveType != 0x2CA) System.out.println("Unexpected curve type " + curveType); + this.curveType = curveType; + return this; + } + + public Builder X(byte[] xComponent) { + this.xComponent = xComponent; + return this; + } + + public Builder Y(byte[] yComponent) { + this.yComponent = yComponent; + return this; + } + + private Builder encrypted(byte[] encrypted) { + this.encrypted = encrypted; + return this; + } + + public Builder MAC(byte[] mac) { + this.mac = mac; + return this; + } + + public CryptoBox build() { + return new CryptoBox(this); + } + } +} diff --git a/domain/src/main/java/ch/dissem/bitmessage/entity/payload/GenericPayload.java b/domain/src/main/java/ch/dissem/bitmessage/entity/payload/GenericPayload.java index cd94c6f..30ba2be 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/entity/payload/GenericPayload.java +++ b/domain/src/main/java/ch/dissem/bitmessage/entity/payload/GenericPayload.java @@ -30,7 +30,7 @@ public class GenericPayload extends ObjectPayload { private long stream; private byte[] data; - private GenericPayload(long stream, byte[] data) { + public GenericPayload(long stream, byte[] data) { this.stream = stream; this.data = data; } diff --git a/domain/src/main/java/ch/dissem/bitmessage/entity/payload/GetPubkey.java b/domain/src/main/java/ch/dissem/bitmessage/entity/payload/GetPubkey.java index 514eae5..b044bce 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/entity/payload/GetPubkey.java +++ b/domain/src/main/java/ch/dissem/bitmessage/entity/payload/GetPubkey.java @@ -19,6 +19,7 @@ package ch.dissem.bitmessage.entity.payload; import ch.dissem.bitmessage.entity.BitmessageAddress; import ch.dissem.bitmessage.utils.Bytes; import ch.dissem.bitmessage.utils.Decode; +import ch.dissem.bitmessage.utils.Security; import java.io.IOException; import java.io.InputStream; @@ -37,7 +38,7 @@ public class GetPubkey extends ObjectPayload { if (address.getVersion() < 4) this.ripe = address.getRipe(); else - this.tag = ((V4Pubkey) address.getPubkey()).getTag(); + this.tag = address.getTag(); } private GetPubkey(long stream, long version, byte[] ripeOrTag) { diff --git a/domain/src/main/java/ch/dissem/bitmessage/entity/payload/Msg.java b/domain/src/main/java/ch/dissem/bitmessage/entity/payload/Msg.java index c0c1c6b..413d99b 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/entity/payload/Msg.java +++ b/domain/src/main/java/ch/dissem/bitmessage/entity/payload/Msg.java @@ -16,9 +16,6 @@ package ch.dissem.bitmessage.entity.payload; -import ch.dissem.bitmessage.utils.Decode; - -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -28,21 +25,22 @@ import java.io.OutputStream; */ public class Msg extends ObjectPayload { private long stream; - private byte[] encrypted; - private UnencryptedMessage unencrypted; + private CryptoBox encrypted; + private UnencryptedMessage decrypted; - private Msg(long stream, byte[] encrypted) { + private Msg(long stream, CryptoBox encrypted) { this.stream = stream; this.encrypted = encrypted; } - public Msg(UnencryptedMessage unencrypted) { + public Msg(UnencryptedMessage unencrypted, Pubkey publicKey) { this.stream = unencrypted.getStream(); - this.unencrypted = unencrypted; + this.decrypted = unencrypted; + this.encrypted = new CryptoBox(unencrypted, publicKey.getEncryptionKey()); } - public static Msg read(InputStream is, long stream, int length) throws IOException { - return new Msg(stream, Decode.bytes(is, length)); + public static Msg read(InputStream in, long stream, int length) throws IOException { + return new Msg(stream, CryptoBox.read(in, length)); } @Override @@ -57,33 +55,30 @@ public class Msg extends ObjectPayload { @Override public boolean isSigned() { - return unencrypted != null; + return decrypted != null; } @Override public void writeBytesToSign(OutputStream out) throws IOException { - unencrypted.write(out, false); + decrypted.write(out, false); } @Override public byte[] getSignature() { - return unencrypted.getSignature(); + return decrypted.getSignature(); } @Override public void setSignature(byte[] signature) { - unencrypted.setSignature(signature); + decrypted.setSignature(signature); } - public byte[] getEncrypted() { - if (encrypted == null) { - // TODO encrypt - } - return encrypted; + public void decrypt(byte[] privateKey) throws IOException { + decrypted = UnencryptedMessage.read(encrypted.decrypt(privateKey)); } @Override - public void write(OutputStream stream) throws IOException { - stream.write(getEncrypted()); + public void write(OutputStream out) throws IOException { + encrypted.write(out); } } 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 eb6aad0..abf7244 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 @@ -36,7 +36,7 @@ public abstract class Pubkey extends ObjectPayload { public abstract byte[] getEncryptionKey(); public byte[] getRipe() { - return Bytes.stripLeadingZeros(ripemd160(sha512(getSigningKey(), getEncryptionKey()))); + return ripemd160(sha512(getSigningKey(), getEncryptionKey())); } protected byte[] add0x04(byte[] key){ diff --git a/domain/src/main/java/ch/dissem/bitmessage/entity/payload/UnencryptedMessage.java b/domain/src/main/java/ch/dissem/bitmessage/entity/payload/UnencryptedMessage.java index 3cb47f1..ea13dd8 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/entity/payload/UnencryptedMessage.java +++ b/domain/src/main/java/ch/dissem/bitmessage/entity/payload/UnencryptedMessage.java @@ -16,15 +16,18 @@ package ch.dissem.bitmessage.entity.payload; +import ch.dissem.bitmessage.entity.Streamable; +import ch.dissem.bitmessage.utils.Decode; import ch.dissem.bitmessage.utils.Encode; import java.io.IOException; +import java.io.InputStream; import java.io.OutputStream; /** * The unencrypted message to be sent by 'msg' or 'broadcast'. */ -public class UnencryptedMessage { +public class UnencryptedMessage implements Streamable { private final long addressVersion; private final long stream; private final int behaviorBitfield; @@ -49,6 +52,21 @@ public class UnencryptedMessage { signature = builder.signature; } + public static UnencryptedMessage read(InputStream is) throws IOException { + return new Builder() + .addressVersion(Decode.varInt(is)) + .stream(Decode.varInt(is)) + .behaviorBitfield(Decode.int32(is)) + .publicSigningKey(Decode.bytes(is, 64)) + .publicEncryptionKey(Decode.bytes(is, 64)) + .nonceTrialsPerByte(Decode.varInt(is)) + .extraBytes(Decode.varInt(is)) + .encoding(Decode.varInt(is)) + .message(Decode.varBytes(is)) + .signature(Decode.varBytes(is)) + .build(); + } + public long getStream() { return stream; } @@ -61,23 +79,28 @@ public class UnencryptedMessage { this.signature = signature; } - public void write(OutputStream os, boolean includeSignature) throws IOException { - Encode.varInt(addressVersion, os); - Encode.varInt(stream, os); - Encode.int32(behaviorBitfield, os); - os.write(publicSigningKey); - os.write(publicEncryptionKey); - Encode.varInt(nonceTrialsPerByte, os); - Encode.varInt(extraBytes, os); - Encode.varInt(encoding, os); - Encode.varInt(message.length, os); - os.write(message); + public void write(OutputStream out, boolean includeSignature) throws IOException { + Encode.varInt(addressVersion, out); + Encode.varInt(stream, out); + Encode.int32(behaviorBitfield, out); + out.write(publicSigningKey); + out.write(publicEncryptionKey); + Encode.varInt(nonceTrialsPerByte, out); + Encode.varInt(extraBytes, out); + Encode.varInt(encoding, out); + Encode.varInt(message.length, out); + out.write(message); if (includeSignature) { - Encode.varInt(signature.length, os); - os.write(signature); + Encode.varInt(signature.length, out); + out.write(signature); } } + @Override + public void write(OutputStream out) throws IOException { + write(out, true); + } + public static final class Builder { private long addressVersion; private long stream; 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 1ce9340..10f320f 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 @@ -18,6 +18,7 @@ package ch.dissem.bitmessage.entity.payload; import ch.dissem.bitmessage.utils.Decode; import ch.dissem.bitmessage.utils.Encode; +import ch.dissem.bitmessage.utils.Security; import java.io.IOException; import java.io.InputStream; @@ -36,23 +37,21 @@ public class V3Pubkey extends V2Pubkey { behaviorBitfield = builder.behaviorBitfield; publicSigningKey = add0x04(builder.publicSigningKey); publicEncryptionKey = add0x04(builder.publicEncryptionKey); - nonceTrialsPerByte = builder.nonceTrialsPerByte; extraBytes = builder.extraBytes; signature = builder.signature; } public static V3Pubkey read(InputStream is, long stream) throws IOException { - V3Pubkey.Builder v3 = new V3Pubkey.Builder() + return new V3Pubkey.Builder() .stream(stream) - .behaviorBitfield((int) Decode.uint32(is)) + .behaviorBitfield(Decode.int32(is)) .publicSigningKey(Decode.bytes(is, 64)) .publicEncryptionKey(Decode.bytes(is, 64)) .nonceTrialsPerByte(Decode.varInt(is)) - .extraBytes(Decode.varInt(is)); - int sigLength = (int) Decode.varInt(is); - v3.signature(Decode.bytes(is, sigLength)); - return v3.build(); + .extraBytes(Decode.varInt(is)) + .signature(Decode.varBytes(is)) + .build(); } @Override @@ -92,7 +91,6 @@ public class V3Pubkey extends V2Pubkey { private int behaviorBitfield; private byte[] publicSigningKey; private byte[] publicEncryptionKey; - private long nonceTrialsPerByte; private long extraBytes; private byte[] signature; @@ -136,7 +134,6 @@ public class V3Pubkey extends V2Pubkey { } public V3Pubkey build() { - // TODO: check signature return new V3Pubkey(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 fef778a..5921089 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 @@ -31,30 +31,36 @@ import java.io.OutputStream; public class V4Pubkey extends Pubkey { private long stream; private byte[] tag; - private byte[] encrypted; + private CryptoBox encrypted; private V3Pubkey decrypted; - private V4Pubkey(long stream, byte[] tag, byte[] encrypted) { + private V4Pubkey(long stream, byte[] tag, CryptoBox encrypted) { this.stream = stream; this.tag = tag; this.encrypted = encrypted; } - public V4Pubkey(V3Pubkey decrypted) { + public V4Pubkey(byte[] tag, V3Pubkey decrypted) { this.stream = decrypted.stream; - // TODO: this.tag = new BitmessageAddress(this).doubleHash + this.tag = tag; this.decrypted = decrypted; - // TODO: this.encrypted + this.encrypted = new CryptoBox(decrypted, null); } - 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)); + public static V4Pubkey read(InputStream in, long stream, int length) throws IOException { + return new V4Pubkey(stream, + Decode.bytes(in, 32), + CryptoBox.read(in, length - 32)); + } + + public void decrypt(byte[] privateKey) throws IOException { + decrypted = V3Pubkey.read(encrypted.decrypt(privateKey), stream); } @Override public void write(OutputStream stream) throws IOException { stream.write(tag); - stream.write(encrypted); + encrypted.write(stream); } @Override 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 f6f8fd8..abee08d 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/factory/Factory.java +++ b/domain/src/main/java/ch/dissem/bitmessage/factory/Factory.java @@ -56,10 +56,10 @@ public class Factory { public static Pubkey createPubkey(long version, long stream, byte[] publicSigningKey, byte[] publicEncryptionKey, long nonceTrialsPerByte, long extraBytes, Pubkey.Feature... features) { - if (publicSigningKey.length != 64) + if (publicSigningKey.length != 64 && publicSigningKey.length != 65) throw new IllegalArgumentException("64 bytes signing key expected, but it was " + publicSigningKey.length + " bytes long."); - if (publicEncryptionKey.length != 64) + if (publicEncryptionKey.length != 64 && publicEncryptionKey.length != 65) throw new IllegalArgumentException("64 bytes encryption key expected, but it was " + publicEncryptionKey.length + " bytes long."); @@ -82,6 +82,7 @@ public class Factory { .build(); case 4: return new V4Pubkey( + null, // FIXME: calculate tag new V3Pubkey.Builder() .stream(stream) .publicSigningKey(publicSigningKey) 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 23f97e2..c720caa 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/factory/V3MessageFactory.java +++ b/domain/src/main/java/ch/dissem/bitmessage/factory/V3MessageFactory.java @@ -17,6 +17,7 @@ package ch.dissem.bitmessage.factory; import ch.dissem.bitmessage.entity.*; +import ch.dissem.bitmessage.entity.payload.GenericPayload; import ch.dissem.bitmessage.entity.payload.ObjectPayload; import ch.dissem.bitmessage.entity.valueobject.InventoryVector; import ch.dissem.bitmessage.entity.valueobject.NetworkAddress; @@ -79,22 +80,30 @@ class V3MessageFactory { } } - public static ObjectMessage readObject(InputStream stream, int length) throws IOException { + public static ObjectMessage readObject(InputStream in, int length) throws IOException { AccessCounter counter = new AccessCounter(); - byte nonce[] = Decode.bytes(stream, 8, counter); - long expiresTime = Decode.int64(stream, counter); - long objectType = Decode.uint32(stream, counter); - long version = Decode.varInt(stream, counter); - long streamNumber = Decode.varInt(stream, counter); + byte nonce[] = Decode.bytes(in, 8, counter); + long expiresTime = Decode.int64(in, counter); + long objectType = Decode.uint32(in, counter); + long version = Decode.varInt(in, counter); + long stream = Decode.varInt(in, counter); - ObjectPayload payload = Factory.getObjectPayload(objectType, version, streamNumber, stream, length - counter.length()); + byte[] data = Decode.bytes(in, length - counter.length()); + ObjectPayload payload; + try { + ByteArrayInputStream dataStream = new ByteArrayInputStream(data); + payload = Factory.getObjectPayload(objectType, version, stream, dataStream, data.length); + } catch (IOException e) { + LOG.trace("Could not parse object payload - using generic payload instead", e); + payload = new GenericPayload(stream, data); + } return new ObjectMessage.Builder() .nonce(nonce) .expiresTime(expiresTime) .objectType(objectType) .version(version) - .stream(streamNumber) + .stream(stream) .payload(payload) .build(); } diff --git a/domain/src/main/java/ch/dissem/bitmessage/utils/Decode.java b/domain/src/main/java/ch/dissem/bitmessage/utils/Decode.java index 4567d5a..b539aa9 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/utils/Decode.java +++ b/domain/src/main/java/ch/dissem/bitmessage/utils/Decode.java @@ -27,6 +27,21 @@ import static ch.dissem.bitmessage.utils.AccessCounter.inc; * https://bitmessage.org/wiki/Protocol_specification#Common_structures */ public class Decode { + public static byte[] shortVarBytes(InputStream stream, AccessCounter counter) throws IOException { + int length = uint16(stream, counter); + return bytes(stream, length, counter); + } + + public static byte[] varBytes(InputStream stream) throws IOException { + int length = (int) varInt(stream, null); + return bytes(stream, length, null); + } + + public static byte[] varBytes(InputStream stream, AccessCounter counter) throws IOException { + int length = (int) varInt(stream, counter); + return bytes(stream, length, counter); + } + public static byte[] bytes(InputStream stream, int count) throws IOException { return bytes(stream, count, null); } @@ -97,6 +112,11 @@ public class Decode { } public static int int32(InputStream stream) throws IOException { + return int32(stream, null); + } + + public static int int32(InputStream stream, AccessCounter counter) throws IOException { + inc(counter, 4); return ByteBuffer.wrap(bytes(stream, 4)).getInt(); } 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 2a64062..e00674a 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/utils/Security.java +++ b/domain/src/main/java/ch/dissem/bitmessage/utils/Security.java @@ -20,19 +20,23 @@ 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.x9.X9ECParameters; +import org.bouncycastle.crypto.ec.CustomNamedCurves; +import org.bouncycastle.crypto.params.ECDomainParameters; +import org.bouncycastle.jcajce.provider.asymmetric.util.EC5Util; import org.bouncycastle.jce.ECNamedCurveTable; -import org.bouncycastle.jce.ECPointUtil; +import org.bouncycastle.jce.interfaces.ECPublicKey; import org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.bouncycastle.jce.spec.ECNamedCurveSpec; -import org.bouncycastle.util.encoders.Hex; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; import java.io.IOException; import java.math.BigInteger; import java.security.*; -import java.security.interfaces.ECPublicKey; import java.security.spec.*; +import java.util.Arrays; /** * Provides some methods to help with hashing and encryption. @@ -41,7 +45,14 @@ 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 ECGenParameterSpec EC_PARAMETERS = new ECGenParameterSpec("secp256k1"); + private static final String EC_CURVE_NAME = "secp256k1"; + private static final X9ECParameters EC_CURVE_PARAMETERS = CustomNamedCurves.getByName(EC_CURVE_NAME); + private static final ECDomainParameters EC_DOMAIN_PARAMETERS = new ECDomainParameters( + EC_CURVE_PARAMETERS.getCurve(), + EC_CURVE_PARAMETERS.getG(), + EC_CURVE_PARAMETERS.getN(), + EC_CURVE_PARAMETERS.getH() + ); static { java.security.Security.addProvider(new BouncyCastleProvider()); @@ -138,50 +149,75 @@ public class Security { } } + public static byte[] mac(byte[] key_m, byte[] data) { + try { + Mac mac = Mac.getInstance("HmacSHA256", "BC"); + mac.init(new SecretKeySpec(key_m, "HmacSHA256")); + return mac.doFinal(data); + } catch (GeneralSecurityException e) { + throw new RuntimeException(e); + } + } + public static Pubkey createPubkey(long version, long stream, byte[] privateSigningKey, byte[] privateEncryptionKey, long nonceTrialsPerByte, long extraBytes, Pubkey.Feature... features) { -// ECPublicKeySpec pubKey = new ECPublicKeySpec( -// ECPointUtil.decodePoint(curve, Hex.decode("025b6dc53bc61a2548ffb0f671472de6c9521a9d2d2534e65abfcbd5fe0c70")), // Q -// EC_PARAMETERS); -// byte[] publicSigningKey = EC_PARAMETERS.getG().multiply(keyToBigInt(privateSigningKey)).getEncoded(false); -// byte[] publicEncryptionKey = EC_PARAMETERS.getG().multiply(keyToBigInt(privateEncryptionKey)).getEncoded(false); -// return Factory.createPubkey(version, stream, // publicSigningKey, publicEncryptionKey, -// Bytes.subArray(publicSigningKey, 1, publicSigningKey.length - 1), -// Bytes.subArray(publicEncryptionKey, 1, publicEncryptionKey.length - 1), -// nonceTrialsPerByte, extraBytes, features); - return null; + return Factory.createPubkey(version, stream, + createPublicKey(privateSigningKey), + createPublicKey(privateEncryptionKey), + nonceTrialsPerByte, extraBytes, features); } - private static byte[] createPublicKey(byte[] privateKey){ -// ECParameterSpec spec = new ECNamedCurveSpec(ECNamedCurveTable.getParameterSpec("prime239v1")); -// ECPrivateKeySpec priKey = new ECPrivateKeySpec( -// new BigInteger("876300101507107567501066130761671078357010671067781776716671676178726717"), // d -// spec); -// ECPublicKeySpec pubKey = new ECPublicKeySpec( -// ECPointUtil.decodePoint( -// spec.getCurve(), -// Hex.decode("025b6dc53bc61a2548ffb0f671472de6c9521a9d2d2534e65abfcbd5fe0c70")), // Q -// spec); - return null; + private static byte[] createPublicKey(byte[] privateKey) { + return EC_DOMAIN_PARAMETERS.getG().multiply(keyToBigInt(privateKey)).getEncoded(false); } - private static BigInteger keyToBigInt(byte[] key) { - return new BigInteger(1, key); + public static BigInteger keyToBigInt(byte[] privateKey) { + return new BigInteger(1, privateKey); + } + + private static ECPoint keyToPoint(byte[] publicKey) { + BigInteger x = new BigInteger(Arrays.copyOfRange(publicKey, 1, 33)); + BigInteger y = new BigInteger(Arrays.copyOfRange(publicKey, 33, 65)); + return new ECPoint(x, y); } public static boolean isSignatureValid(byte[] bytesToSign, byte[] signature, Pubkey pubkey) { -// ECPoint W = EC_CURVE.getCurve().decodePoint(pubkey.getSigningKey()); // TODO: probably this needs 0x04 added + ECPoint W = keyToPoint(pubkey.getSigningKey()); try { ECParameterSpec param = null; -// KeySpec keySpec = new ECPublicKeySpec(W,param);; -// PublicKey publicKey = KeyFactory.getInstance("ECDSA", "BC").generatePublic(keySpec); + KeySpec keySpec = new ECPublicKeySpec(W, param); + PublicKey publicKey = KeyFactory.getInstance("ECDSA", "BC").generatePublic(keySpec); Signature sig = Signature.getInstance("ECDSA", "BC"); -// sig.initVerify(publicKey); + sig.initVerify(publicKey); sig.update(bytesToSign); return sig.verify(signature); } catch (Exception e) { throw new RuntimeException(e); } } + + public static ECPublicKey getPublicKey(byte[] publicKey) { + if (publicKey[0] != 0x04) throw new IllegalArgumentException("Public key starting with 0x04 expected"); + return getPublicKey( + Arrays.copyOfRange(publicKey, 1, 33), + Arrays.copyOfRange(publicKey, 33, 65) + ); + } + + public static ECPublicKey getPublicKey(byte[] X, byte[] Y) { + try { + ECPoint w = new ECPoint(keyToBigInt(X), keyToBigInt(Y)); + EllipticCurve curve = EC5Util.convertCurve(EC_DOMAIN_PARAMETERS.getCurve(), EC_CURVE_PARAMETERS.getSeed()); + ECParameterSpec params = EC5Util.convertSpec(curve, ECNamedCurveTable.getParameterSpec(EC_CURVE_NAME)); + ECPublicKeySpec keySpec = new ECPublicKeySpec(w, params); + return (ECPublicKey) getKeyFactory().generatePublic(keySpec); + } catch (GeneralSecurityException e) { + throw new RuntimeException(e); + } + } + + private static KeyFactory getKeyFactory() throws NoSuchProviderException, NoSuchAlgorithmException { + return KeyFactory.getInstance("EC", "BC"); + } } diff --git a/domain/src/test/java/ch/dissem/bitmessage/entity/BitmessageAddressTest.java b/domain/src/test/java/ch/dissem/bitmessage/entity/BitmessageAddressTest.java index 1020121..243e613 100644 --- a/domain/src/test/java/ch/dissem/bitmessage/entity/BitmessageAddressTest.java +++ b/domain/src/test/java/ch/dissem/bitmessage/entity/BitmessageAddressTest.java @@ -18,6 +18,7 @@ package ch.dissem.bitmessage.entity; import ch.dissem.bitmessage.entity.payload.Pubkey; import ch.dissem.bitmessage.entity.payload.V3Pubkey; +import ch.dissem.bitmessage.entity.payload.V4Pubkey; import ch.dissem.bitmessage.entity.valueobject.PrivateKey; import ch.dissem.bitmessage.utils.*; import org.junit.Test; @@ -67,22 +68,22 @@ public class BitmessageAddressTest { @Test public void testV3PubkeyImport() throws IOException { BitmessageAddress address = new BitmessageAddress("BM-2D9Vc5rFxxR5vTi53T9gkLfemViHRMVLQZ"); - assertArrayEquals(Bytes.fromHex("7402be6e76c3cb87caa946d0c003a3d4d8e1d5"), address.getRipe()); + assertArrayEquals(Bytes.fromHex("007402be6e76c3cb87caa946d0c003a3d4d8e1d5"), address.getRipe()); ObjectMessage object = TestUtils.loadObjectMessage(3, "V3Pubkey.payload"); Pubkey pubkey = (Pubkey) object.getPayload(); address.setPubkey(pubkey); - assertArrayEquals(Bytes.fromHex("7402be6e76c3cb87caa946d0c003a3d4d8e1d5"), pubkey.getRipe()); + assertArrayEquals(Bytes.fromHex("007402be6e76c3cb87caa946d0c003a3d4d8e1d5"), pubkey.getRipe()); } @Test public void testV4PubkeyImport() throws IOException { // TODO -// ObjectMessage object = TestUtils.loadObjectMessage(3, "V4Pubkey.payload"); -// Pubkey pubkey = (Pubkey) object.getPayload(); -// BitmessageAddress address = new BitmessageAddress("BM-2D9Vc5rFxxR5vTi53T9gkLfemViHRMVLQZ"); -// address.setPubkey(pubkey); + ObjectMessage object = TestUtils.loadObjectMessage(4, "V4Pubkey.payload"); + V4Pubkey pubkey = (V4Pubkey) object.getPayload(); + BitmessageAddress address = new BitmessageAddress("BM-2cXxfcSetKnbHJX2Y85rSkaVpsdNUZ5q9h"); + address.setPubkey(pubkey); } @Test diff --git a/domain/src/test/java/ch/dissem/bitmessage/entity/SerializationTest.java b/domain/src/test/java/ch/dissem/bitmessage/entity/SerializationTest.java index baefe7b..1e941af 100644 --- a/domain/src/test/java/ch/dissem/bitmessage/entity/SerializationTest.java +++ b/domain/src/test/java/ch/dissem/bitmessage/entity/SerializationTest.java @@ -28,7 +28,6 @@ import java.io.InputStream; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; /** * Created by chris on 28.04.15. @@ -60,15 +59,22 @@ public class SerializationTest { public void ensureV1MsgIsDeserializedAndSerializedCorrectly() throws IOException { doTest("V1Msg.payload", 1, Msg.class); } + @Test public void ensureV4BroadcastIsDeserializedAndSerializedCorrectly() throws IOException { doTest("V4Broadcast.payload", 4, V4Broadcast.class); } + @Test public void ensureV5BroadcastIsDeserializedAndSerializedCorrectly() throws IOException { doTest("V5Broadcast.payload", 5, V5Broadcast.class); } + @Test + public void ensureUnknownDataIsDeserializedAndSerializedCorrectly() throws IOException { + doTest("V1MsgStrangeData.payload", 1, GenericPayload.class); + } + private void doTest(String resourceName, int version, Class expectedPayloadType) throws IOException { byte[] data = TestUtils.getBytes(resourceName); InputStream in = new ByteArrayInputStream(data); diff --git a/domain/src/test/resources/V1MsgStrangeData.payload b/domain/src/test/resources/V1MsgStrangeData.payload new file mode 100644 index 0000000000000000000000000000000000000000..91c83e54fb1b30276a1c76767559798fb61d76fd GIT binary patch literal 54 zcmZQz0D?x3Ob`|7_VW;sVq#>}exxcs>BTyp=5xolU7EdK?&UB2tz7p$O|27|vSMR} Gm=XZBL=<-b literal 0 HcmV?d00001 diff --git a/domain/src/test/resources/V4Pubkey.payload b/domain/src/test/resources/V4Pubkey.payload index b9b1c59b003d22a27d3331dffc8a608ad7444995..a5e4c5c40944e8ffc38b369e40840c06a416c5aa 100644 GIT binary patch literal 396 zcmV;70dxKU00000xAyk{00010UeGiE000350Z5r#d*j`5Pg%2ba1!PSq98oPb(^t% z;KgzK_V$mQZm9inp(ffPq;aqrDrn3?C<4j=Akdap1po|b4j_f0notM>d(L9Ml{C2@ z+T1h%bp#E0>i{5sJWl0^@?*BHMYKflgKudScHd!TX)VzSU6-cwk+pgTmYfiz1uQCj zSs4!l9plyrnujt2%tbA6X)Hz_q)G?EPXO!Uy0#j8MQVhpFP4*Qu>!6gP@)wYpVarh zgpUg62HESXiAQQMO_}jTWWG}erMJvUwXEpY1uLsPa(u$}|D#H;5*xal`le2&=b|6X zXDuFEZ4@AwqBs+8&pZ<&P7N~hBU_TcS#YQ9yYuq}#O&r(M5b62y}r$6<(Ba$=f(7# zTcA@q?ut~DeC+hIc@%8p@Hu+Tckgr{-rc17p<4cAWEv#WU`aSrVyLq*B74o@+$o~b q!`TVqn-`}!5^p-`%#4j-0g7a3xT-Zo<53UgE9%*VnnKWkIVoM&N4W9; literal 396 zcmV;70dxKU00004pf9@s00010MHmYJ000350nqGwzNfY!9w64R$joS$S5%3q5zG+lUG@d+W#B?8I-AmE7M`4N#f_>l5sBGqyTKB8TGRakg9 z{PNyG)E0p#TL2(2Bc#SVza5G}4(Qc1boXpiS1KmQ0u>Y9m(kj~dz#2#IRo_+of09u zM}mU0(-->0q6&Wzu^mhK5O~d~?-}~qaS;1T@P^>uT->>%=hNd0q;kp3jfo z4Rr)2gw4(EA}u_w;Zs;DlTRM4@IEdI0j{@Htc6m|5sM7<_`ZPbu=B%aeH!OFZLGvp q0esO1m&p6{H$wn}ivBFpR<0sS)&!?lDlgF