Decryption works now!
(well, for v4 pubkeys at least)
This commit is contained in:
parent
d0250444d5
commit
f23f432f07
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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 <a href='https://bitmessage.org/wiki/Encryption#Decryption'>https://bitmessage.org/wiki/Encryption#Decryption</a>
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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){
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
BIN
domain/src/test/resources/V1MsgStrangeData.payload
Normal file
BIN
domain/src/test/resources/V1MsgStrangeData.payload
Normal file
Binary file not shown.
Binary file not shown.
Loading…
Reference in New Issue
Block a user