Refactor serialization and added message validation

This commit is contained in:
2021-08-10 16:50:35 +02:00
parent 0abd4c4fec
commit 415fda3d64
9 changed files with 215 additions and 171 deletions

View File

@@ -1,18 +1,20 @@
import java.io.File;
import java.io.IOException;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;
import model.BlockChain;
import model.ChatClient;
import util.ChatClient;
import model.Miner;
import util.FileManagement;
import util.Security;
public final class Main {
public static void main(final String[] args) throws InterruptedException {
public static void main(final String[] args) throws InterruptedException, IOException {
final var nThreads = Runtime.getRuntime().availableProcessors();
final var blockChain = new BlockChain();
blockChain.load();
final var blockChain = BlockChain.getInstance();
// Cryptographic keys management
final File publicKey = new File(Security.PUBLIC_KEY);
@@ -24,17 +26,15 @@ public final class Main {
final var chatExecutor = Executors.newScheduledThreadPool(nThreads);
// Mocks 3 chat clients who send messages to the blockchain
IntStream.range(0, 3)
.mapToObj(chatClientId -> new ChatClient(chatClientId, blockChain))
.forEach(e -> chatExecutor.scheduleAtFixedRate(e, 0, 100, TimeUnit.MILLISECONDS));
// Mocks a chat client who send messages to the blockchain
chatExecutor.scheduleAtFixedRate(new ChatClient(blockChain), 0, 200, TimeUnit.MILLISECONDS);
final var minerExecutor = Executors.newFixedThreadPool(nThreads);
// Creation of 10 miners
IntStream.range(0, 10)
.mapToObj(minerId -> new Miner(minerId, blockChain))
.forEach(minerExecutor::submit);
// Creation of 5 miners
IntStream.range(0, 5)
.mapToObj(minerId -> new Miner(minerId, blockChain))
.forEach(minerExecutor::submit);
minerExecutor.shutdown();
@@ -48,8 +48,8 @@ public final class Main {
chatExecutor.shutdownNow();
}
blockChain.save();
FileManagement.saveBlockChain(blockChain);
}
}

View File

@@ -8,6 +8,7 @@ import java.util.stream.Collectors;
public class Block implements Serializable {
private static final long serialVersionUID = 1L;
private final String previousBlockHash;
private final String blockHash;
private final int minerId;
@@ -18,13 +19,13 @@ public class Block implements Serializable {
private final long timeStamp;
public Block(
final String previousBlockHash,
final String blockHash,
final int minerId,
final int id,
final int magicNumber,
final float generationSecs,
final List<Message> chatMessages
final String previousBlockHash,
final String blockHash,
final int minerId,
final int id,
final int magicNumber,
final float generationSecs,
final List<Message> blockMessages
) {
this.previousBlockHash = previousBlockHash;
this.blockHash = blockHash;
@@ -32,7 +33,7 @@ public class Block implements Serializable {
this.id = id;
this.magicNumber = magicNumber;
this.generationSecs = generationSecs;
this.chatMessages = chatMessages;
this.chatMessages = blockMessages;
this.timeStamp = new Date().getTime();
}
@@ -46,21 +47,21 @@ public class Block implements Serializable {
public String toString() {
return "Block: " + "\n"
+ "Created by miner #" + this.minerId + "\n"
+ "Id: " + this.id + "\n"
+ "Timestamp: " + this.timeStamp + "\n"
+ "Magic number: " + this.magicNumber + "\n"
+ "Hash of the previous block: " + "\n" + this.previousBlockHash + "\n"
+ "Hash of the block: \n" + this.blockHash + "\n"
+ "Block data: \n"
+ messagesToString()
+ "Block was generating for " + this.generationSecs + " seconds";
+ "Created by miner #" + this.minerId + "\n"
+ "Id: " + this.id + "\n"
+ "Timestamp: " + this.timeStamp + "\n"
+ "Magic number: " + this.magicNumber + "\n"
+ "Hash of the previous block: " + "\n" + this.previousBlockHash + "\n"
+ "Hash of the block: \n" + this.blockHash + "\n"
+ "Block data: \n"
+ messagesToString()
+ "Block was generating for " + this.generationSecs + " seconds";
}
public String messagesToString() {
return chatMessages.stream()
.map(m -> m.getText().concat("\n"))
.collect(Collectors.joining());
return chatMessages.isEmpty() ? "Empty block\n" : chatMessages.stream()
.map(m -> String.valueOf(m.getId()).concat(" - ").concat(m.getText()).concat("\n"))
.collect(Collectors.joining());
}
public List<Message> getMessages() {

View File

@@ -1,5 +1,12 @@
package model;
import java.io.IOException;
import java.io.Serializable;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SignatureException;
import java.security.spec.InvalidKeySpecException;
import java.util.concurrent.atomic.AtomicInteger;
import util.FileManagement;
import util.HashFunction;
import util.Security;
@@ -9,37 +16,59 @@ import java.util.stream.Collectors;
import static java.lang.String.valueOf;
public class BlockChain {
public class BlockChain implements Serializable {
private static final long serialVersionUID = 1L;
private static final float LOWER_LIMIT_SECS = 0.1F;
private static final float UPPER_LIMIT_SECS = 0.5F;
private final Random random = new Random();
private final List<Block> blockList = new LinkedList<>();
private final List<Message> chatMessages = new ArrayList<>();
private final List<Message> incomingChatMessages = new ArrayList<>();
private final AtomicInteger lastMessageId = new AtomicInteger(1);
private int hashZeroes;
private int magicNumber;
private float generationSecs = 0;
public static BlockChain getInstance() {
try {
final BlockChain blockChain = (BlockChain) FileManagement.loadBlockChain();
if (!blockChain.validateBlockchain()) {
System.out.println("Blockchain not valid! Creating new one");
return new BlockChain();
} else {
return blockChain;
}
} catch (final ClassNotFoundException | IOException e) {
return new BlockChain();
}
}
public synchronized void addBlock(final int minerId) {
final int nextId = blockList.size();
final String previousBlockHash = (nextId > 0) ? blockList.get(nextId - 1).getBlockHash() : "0";
final String blockHash = calculateBlockHash(minerId);
incomingChatMessages.sort(Comparator.comparingInt(Message::getId));
final var block = new Block(
previousBlockHash,
blockHash,
minerId,
nextId,
magicNumber,
generationSecs,
chatMessages
previousBlockHash,
blockHash,
minerId,
nextId,
magicNumber,
generationSecs,
incomingChatMessages
);
blockList.add(block);
System.out.println(block);
this.chatMessages.clear();
incomingChatMessages.clear();
if (generationSecs < 1) {
if (generationSecs < LOWER_LIMIT_SECS) {
hashZeroes += 1;
System.out.println("N was increased to " + hashZeroes +"\n");
} else if (generationSecs > 10) {
} else if (generationSecs > UPPER_LIMIT_SECS) {
hashZeroes -= 1;
System.out.println("N was decreased by 1\n");
} else {
@@ -48,48 +77,58 @@ public class BlockChain {
}
public void acceptMessage(Message message) {
if (!blockList.isEmpty() || message.getId() > getLastMessageId()) {
chatMessages.add(message);
}
public int getNextMessageId() {
return lastMessageId.getAndIncrement();
}
public int getLastMessageId() {
return blockList.stream()
.map(Block::getMessages)
.flatMap(Collection::stream)
.mapToInt(Message::getId)
.max().orElse(0);
public void acceptText(final int nextMessageId, final String name, final String text)
throws NoSuchAlgorithmException, SignatureException, InvalidKeyException, IOException, InvalidKeySpecException {
incomingChatMessages.add(new Message(
nextMessageId,
name,
text,
Security.getPrivate(),
Security.getPublic()
));
}
public void load() {
FileManagement.loadBlockchain(blockList);
}
public void save() {
FileManagement.saveBlockchain(blockList);
}
public String toString() {
if (!validateBlockchain()) {
System.out.println("Blockchain invalid!");
}
return blockList.stream().map(Block::toString).collect(Collectors.joining("\n"));
}
private boolean validateBlockchain() {
public boolean validateBlockchain() {
if (blockList.isEmpty()) {
return true;
}
//TODO implement message validation
// Check message security
if (!blockList.stream()
.map(Block::getMessages)
.flatMap(Collection::stream)
.allMatch(Security::messageIsValid)) {
return false;
}
// Check messages Id ordering
if (!blockList.stream()
.map(Block::getMessages)
.flatMap(Collection::stream)
.map(Message::getId)
.sorted()
.collect(Collectors.toList())
.equals(blockList.stream()
.map(Block::getMessages)
.flatMap(Collection::stream)
.map(Message::getId)
.collect(Collectors.toList())
)
) {
return false;
}
// Check block hashes ordering
final List<String> previousHashes = blockList.stream()
.map(Block::getPreviousBlockHash)
.collect(Collectors.toList());
.map(Block::getPreviousBlockHash)
.collect(Collectors.toList());
final List<String> hashes = blockList.stream()
.map(Block::getBlockHash)
.collect(Collectors.toList());
.map(Block::getBlockHash)
.collect(Collectors.toList());
for (var index = 1; index < hashes.size(); index++) {
if (!previousHashes.get(index).equals(hashes.get(index - 1))) {
@@ -100,20 +139,15 @@ public class BlockChain {
return true;
}
private boolean messageIsValid(Message msg) {
//TODO correct signature issues
return Security.verifySignature(msg.getId() + msg.getText(), msg.getSignature(), msg.getPublicKey());
}
private String calculateBlockHash(final int minerId) {
final long start = System.currentTimeMillis();
var hash = "";
do {
hash = HashFunction.applySha256(minerId
+ valueOf(new Date().getTime())
+ calculateMagicNumber()
+ valueOf(new Date().getTime())
+ calculateMagicNumber()
);
} while (!hash.matches("(?s)0{" + hashZeroes + "}([^0].*)?"));
} while (!hash.startsWith("0".repeat(hashZeroes)));
final long end = System.currentTimeMillis();
generationSecs = (end - start) / 1000F;
return hash;

View File

@@ -1,54 +0,0 @@
package model;
import util.Security;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SignatureException;
import java.security.spec.InvalidKeySpecException;
import java.util.Random;
public class ChatClient implements Runnable {
private final int clientId;
private final BlockChain blockChain;
private static final Random random = new Random();
public ChatClient(final int chatClientId, final BlockChain blockChain) {
this.clientId = chatClientId;
this.blockChain = blockChain;
}
@Override
public void run() {
try {
blockChain.acceptMessage(createMessage());
} catch (NoSuchAlgorithmException | SignatureException | InvalidKeyException | IOException | InvalidKeySpecException e) {
e.printStackTrace();
}
}
private Message createMessage()
throws NoSuchAlgorithmException, SignatureException, InvalidKeyException, IOException, InvalidKeySpecException {
return new Message(
blockChain.getLastMessageId() + 1,
clientId,
generateRandomAlphabeticString(),
Security.getPrivate(),
Security.getPublic()
);
}
private String generateRandomAlphabeticString() {
final int leftLimit = 97; // letter 'a'
final int rightLimit = 122; // letter 'z'
final int targetStringLength = 10;
return random.ints(leftLimit, rightLimit + 1)
.limit(targetStringLength)
.collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append)
.toString();
}
}

View File

@@ -5,18 +5,19 @@ import util.Security;
import java.io.Serializable;
import java.security.*;
public class Message implements Serializable {
public class Message implements Serializable {
private static final long serialVersionUID = 1L;
private final int id;
private final String text;
private final byte[] signature;
private final PublicKey publicKey;
public Message(final int id, final int clientId, final String text, final PrivateKey privateKey, final PublicKey publicKey)
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
public Message(final int id, final String name, final String text, final PrivateKey privateKey, final PublicKey publicKey)
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
this.id = id;
this.text = "Client " + clientId + " says: " + text;
this.text = "Chatter " + name + " says: " + text;
this.signature = Security.sign(text, privateKey);
this.publicKey = publicKey;
}

53
src/util/ChatClient.java Normal file
View File

@@ -0,0 +1,53 @@
package util;
import model.BlockChain;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SignatureException;
import java.security.spec.InvalidKeySpecException;
import java.util.Random;
public class ChatClient implements Runnable {
private static final Random random = new Random();
private final BlockChain blockChain;
public ChatClient(final BlockChain blockChain) {
this.blockChain = blockChain;
}
@Override
public void run() {
try {
blockChain.acceptText(blockChain.getNextMessageId(), generateRandomName(), generateRandomText());
} catch (final NoSuchAlgorithmException | SignatureException | InvalidKeyException | IOException | InvalidKeySpecException e) {
e.printStackTrace();
}
}
private String generateRandomName() {
final int leftLimit = 97; // letter 'a'
final int rightLimit = 122; // letter 'z'
final int targetStringLength = 5;
return random.ints(leftLimit, rightLimit + 1)
.limit(targetStringLength)
.collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append)
.toString();
}
private String generateRandomText() {
final int leftLimit = 97; // letter 'a'
final int rightLimit = 122; // letter 'z'
final int targetStringLength = 20;
return random.ints(leftLimit, rightLimit + 1)
.limit(targetStringLength)
.collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append)
.toString();
}
}

View File

@@ -1,43 +1,37 @@
package util;
import model.Block;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.List;
public final class FileManagement {
private static final String BLOCKCHAIN = "blockChain.txt";
public static void saveBlockchain(final List<Block> blockList) {
final var blockChainFile = new File(BLOCKCHAIN);
try (final var fileOut = new FileOutputStream(blockChainFile);
final var objectOut = new ObjectOutputStream(fileOut)) {
for (final var block : blockList) {
objectOut.writeObject(block);
}
} catch (final IOException e) {
e.printStackTrace();
}
private FileManagement() {
throw new IllegalStateException("FileManagement class");
}
public static void loadBlockchain(final List<Block> blockList) {
final var blockChainFile = new File(BLOCKCHAIN);
if (blockChainFile.exists()) {
try (final var fileIn = new FileInputStream(blockChainFile);
final var objectIn = new ObjectInputStream(fileIn)) {
while (fileIn.available() > 0) {
final var object = objectIn.readObject();
blockList.add((Block) object);
}
} catch (final IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
public static void saveBlockChain(final Object obj) throws IOException {
final FileOutputStream fos = new FileOutputStream(BLOCKCHAIN);
final BufferedOutputStream bos = new BufferedOutputStream(fos);
final ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(obj);
oos.close();
}
public static Object loadBlockChain() throws IOException, ClassNotFoundException {
final FileInputStream fis = new FileInputStream(BLOCKCHAIN);
final BufferedInputStream bis = new BufferedInputStream(fis);
final ObjectInputStream ois = new ObjectInputStream(bis);
final Object obj = ois.readObject();
ois.close();
return obj;
}
public static void saveKey(final String path, final byte[] key) throws IOException {

View File

@@ -4,6 +4,11 @@ import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
public final class HashFunction {
private HashFunction() {
throw new IllegalStateException("HashFunction class");
}
/* Applies Sha256 to a string and returns a hash. */
public static String applySha256(final String input) throws RuntimeException {
try {
@@ -22,4 +27,5 @@ public final class HashFunction {
throw new RuntimeException(e);
}
}
}

View File

@@ -7,14 +7,19 @@ import java.security.*;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import model.Message;
public final class Security {
public static final String PRIVATE_KEY = "KeyPair/privateKey";
public static final String PUBLIC_KEY = "KeyPair/publicKey";
private Security() {
throw new IllegalStateException("Security class");
}
public static PublicKey getPublic()
throws NoSuchAlgorithmException, IOException, InvalidKeySpecException {
throws NoSuchAlgorithmException, IOException, InvalidKeySpecException {
final byte[] keyBytes = Files.readAllBytes(new File(PUBLIC_KEY).toPath());
final X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes);
final KeyFactory kf = KeyFactory.getInstance("RSA");
@@ -22,7 +27,7 @@ public final class Security {
}
public static PrivateKey getPrivate()
throws NoSuchAlgorithmException, IOException, InvalidKeySpecException {
throws NoSuchAlgorithmException, IOException, InvalidKeySpecException {
final byte[] keyBytes = Files.readAllBytes(new File(PRIVATE_KEY).toPath());
final PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes);
final KeyFactory kf = KeyFactory.getInstance("RSA");
@@ -30,13 +35,17 @@ public final class Security {
}
public static byte[] sign(final String data, final PrivateKey privateKey)
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
final Signature rsa = Signature.getInstance("SHA1withRSA");
rsa.initSign(privateKey);
rsa.update(data.getBytes());
return rsa.sign();
}
public static boolean messageIsValid(final Message msg) {
return Security.verifySignature(msg.getId() + msg.getText(), msg.getSignature(), msg.getPublicKey());
}
public static boolean verifySignature(final String data, final byte[] signature, final PublicKey publicKey) {
try {
final Signature verifier = Signature.getInstance("SHA1withRSA");