From 415fda3d640cb3326a52b30027f7d7648a09e635 Mon Sep 17 00:00:00 2001 From: droideparanoico Date: Tue, 10 Aug 2021 16:50:35 +0200 Subject: [PATCH] Refactor serialization and added message validation --- src/Main.java | 28 ++++---- src/model/Block.java | 41 +++++------ src/model/BlockChain.java | 136 ++++++++++++++++++++++------------- src/model/ChatClient.java | 54 -------------- src/model/Message.java | 9 +-- src/util/ChatClient.java | 53 ++++++++++++++ src/util/FileManagement.java | 44 +++++------- src/util/HashFunction.java | 6 ++ src/util/Security.java | 15 +++- 9 files changed, 215 insertions(+), 171 deletions(-) delete mode 100644 src/model/ChatClient.java create mode 100644 src/util/ChatClient.java diff --git a/src/Main.java b/src/Main.java index 29ff8a6..ea143ef 100644 --- a/src/Main.java +++ b/src/Main.java @@ -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); + } } - diff --git a/src/model/Block.java b/src/model/Block.java index 1baf3e0..b90370b 100644 --- a/src/model/Block.java +++ b/src/model/Block.java @@ -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 chatMessages + final String previousBlockHash, + final String blockHash, + final int minerId, + final int id, + final int magicNumber, + final float generationSecs, + final List 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 getMessages() { diff --git a/src/model/BlockChain.java b/src/model/BlockChain.java index 0da3ea9..fa35715 100644 --- a/src/model/BlockChain.java +++ b/src/model/BlockChain.java @@ -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 blockList = new LinkedList<>(); - private final List chatMessages = new ArrayList<>(); + private final List 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 previousHashes = blockList.stream() - .map(Block::getPreviousBlockHash) - .collect(Collectors.toList()); + .map(Block::getPreviousBlockHash) + .collect(Collectors.toList()); final List 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; diff --git a/src/model/ChatClient.java b/src/model/ChatClient.java deleted file mode 100644 index 2043dee..0000000 --- a/src/model/ChatClient.java +++ /dev/null @@ -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(); - } - -} \ No newline at end of file diff --git a/src/model/Message.java b/src/model/Message.java index 2854ce4..ffe7b78 100644 --- a/src/model/Message.java +++ b/src/model/Message.java @@ -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; } diff --git a/src/util/ChatClient.java b/src/util/ChatClient.java new file mode 100644 index 0000000..ed71354 --- /dev/null +++ b/src/util/ChatClient.java @@ -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(); + } + +} diff --git a/src/util/FileManagement.java b/src/util/FileManagement.java index b3cb60e..0ec215a 100644 --- a/src/util/FileManagement.java +++ b/src/util/FileManagement.java @@ -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 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 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 { diff --git a/src/util/HashFunction.java b/src/util/HashFunction.java index b070d04..2bc0b50 100644 --- a/src/util/HashFunction.java +++ b/src/util/HashFunction.java @@ -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); } } + } diff --git a/src/util/Security.java b/src/util/Security.java index 5a0ce79..2579d6a 100644 --- a/src/util/Security.java +++ b/src/util/Security.java @@ -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");