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

View File

@@ -8,6 +8,7 @@ import java.util.stream.Collectors;
public class Block implements Serializable { public class Block implements Serializable {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
private final String previousBlockHash; private final String previousBlockHash;
private final String blockHash; private final String blockHash;
private final int minerId; private final int minerId;
@@ -24,7 +25,7 @@ public class Block implements Serializable {
final int id, final int id,
final int magicNumber, final int magicNumber,
final float generationSecs, final float generationSecs,
final List<Message> chatMessages final List<Message> blockMessages
) { ) {
this.previousBlockHash = previousBlockHash; this.previousBlockHash = previousBlockHash;
this.blockHash = blockHash; this.blockHash = blockHash;
@@ -32,7 +33,7 @@ public class Block implements Serializable {
this.id = id; this.id = id;
this.magicNumber = magicNumber; this.magicNumber = magicNumber;
this.generationSecs = generationSecs; this.generationSecs = generationSecs;
this.chatMessages = chatMessages; this.chatMessages = blockMessages;
this.timeStamp = new Date().getTime(); this.timeStamp = new Date().getTime();
} }
@@ -58,8 +59,8 @@ public class Block implements Serializable {
} }
public String messagesToString() { public String messagesToString() {
return chatMessages.stream() return chatMessages.isEmpty() ? "Empty block\n" : chatMessages.stream()
.map(m -> m.getText().concat("\n")) .map(m -> String.valueOf(m.getId()).concat(" - ").concat(m.getText()).concat("\n"))
.collect(Collectors.joining()); .collect(Collectors.joining());
} }

View File

@@ -1,5 +1,12 @@
package model; 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.FileManagement;
import util.HashFunction; import util.HashFunction;
import util.Security; import util.Security;
@@ -9,20 +16,42 @@ import java.util.stream.Collectors;
import static java.lang.String.valueOf; 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 Random random = new Random();
private final List<Block> blockList = new LinkedList<>(); 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 hashZeroes;
private int magicNumber; private int magicNumber;
private float generationSecs = 0; 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) { public synchronized void addBlock(final int minerId) {
final int nextId = blockList.size(); final int nextId = blockList.size();
final String previousBlockHash = (nextId > 0) ? blockList.get(nextId - 1).getBlockHash() : "0"; final String previousBlockHash = (nextId > 0) ? blockList.get(nextId - 1).getBlockHash() : "0";
final String blockHash = calculateBlockHash(minerId); final String blockHash = calculateBlockHash(minerId);
incomingChatMessages.sort(Comparator.comparingInt(Message::getId));
final var block = new Block( final var block = new Block(
previousBlockHash, previousBlockHash,
blockHash, blockHash,
@@ -30,16 +59,16 @@ public class BlockChain {
nextId, nextId,
magicNumber, magicNumber,
generationSecs, generationSecs,
chatMessages incomingChatMessages
); );
blockList.add(block); blockList.add(block);
System.out.println(block); System.out.println(block);
this.chatMessages.clear(); incomingChatMessages.clear();
if (generationSecs < 1) { if (generationSecs < LOWER_LIMIT_SECS) {
hashZeroes += 1; hashZeroes += 1;
System.out.println("N was increased to " + hashZeroes +"\n"); System.out.println("N was increased to " + hashZeroes +"\n");
} else if (generationSecs > 10) { } else if (generationSecs > UPPER_LIMIT_SECS) {
hashZeroes -= 1; hashZeroes -= 1;
System.out.println("N was decreased by 1\n"); System.out.println("N was decreased by 1\n");
} else { } else {
@@ -48,42 +77,52 @@ public class BlockChain {
} }
public void acceptMessage(Message message) { public int getNextMessageId() {
if (!blockList.isEmpty() || message.getId() > getLastMessageId()) { return lastMessageId.getAndIncrement();
chatMessages.add(message);
}
} }
public int getLastMessageId() { public void acceptText(final int nextMessageId, final String name, final String text)
return blockList.stream() throws NoSuchAlgorithmException, SignatureException, InvalidKeyException, IOException, InvalidKeySpecException {
.map(Block::getMessages) incomingChatMessages.add(new Message(
.flatMap(Collection::stream) nextMessageId,
.mapToInt(Message::getId) name,
.max().orElse(0); text,
Security.getPrivate(),
Security.getPublic()
));
} }
public void load() { public boolean validateBlockchain() {
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() {
if (blockList.isEmpty()) { if (blockList.isEmpty()) {
return true; 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() final List<String> previousHashes = blockList.stream()
.map(Block::getPreviousBlockHash) .map(Block::getPreviousBlockHash)
.collect(Collectors.toList()); .collect(Collectors.toList());
@@ -100,11 +139,6 @@ public class BlockChain {
return true; 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) { private String calculateBlockHash(final int minerId) {
final long start = System.currentTimeMillis(); final long start = System.currentTimeMillis();
var hash = ""; var hash = "";
@@ -113,7 +147,7 @@ public class BlockChain {
+ valueOf(new Date().getTime()) + valueOf(new Date().getTime())
+ calculateMagicNumber() + calculateMagicNumber()
); );
} while (!hash.matches("(?s)0{" + hashZeroes + "}([^0].*)?")); } while (!hash.startsWith("0".repeat(hashZeroes)));
final long end = System.currentTimeMillis(); final long end = System.currentTimeMillis();
generationSecs = (end - start) / 1000F; generationSecs = (end - start) / 1000F;
return hash; 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

@@ -8,15 +8,16 @@ import java.security.*;
public class Message implements Serializable { public class Message implements Serializable {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
private final int id; private final int id;
private final String text; private final String text;
private final byte[] signature; private final byte[] signature;
private final PublicKey publicKey; private final PublicKey publicKey;
public Message(final int id, final int clientId, final String text, final PrivateKey privateKey, final PublicKey publicKey) public Message(final int id, final String name, final String text, final PrivateKey privateKey, final PublicKey publicKey)
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException { throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
this.id = id; this.id = id;
this.text = "Client " + clientId + " says: " + text; this.text = "Chatter " + name + " says: " + text;
this.signature = Security.sign(text, privateKey); this.signature = Security.sign(text, privateKey);
this.publicKey = publicKey; 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; package util;
import model.Block; import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.ObjectInputStream; import java.io.ObjectInputStream;
import java.io.ObjectOutputStream; import java.io.ObjectOutputStream;
import java.util.List;
public final class FileManagement { public final class FileManagement {
private static final String BLOCKCHAIN = "blockChain.txt"; private static final String BLOCKCHAIN = "blockChain.txt";
public static void saveBlockchain(final List<Block> blockList) { private FileManagement() {
final var blockChainFile = new File(BLOCKCHAIN); throw new IllegalStateException("FileManagement class");
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();
}
} }
public static void loadBlockchain(final List<Block> blockList) { public static void saveBlockChain(final Object obj) throws IOException {
final var blockChainFile = new File(BLOCKCHAIN); final FileOutputStream fos = new FileOutputStream(BLOCKCHAIN);
if (blockChainFile.exists()) { final BufferedOutputStream bos = new BufferedOutputStream(fos);
try (final var fileIn = new FileInputStream(blockChainFile); final ObjectOutputStream oos = new ObjectOutputStream(bos);
final var objectIn = new ObjectInputStream(fileIn)) { oos.writeObject(obj);
while (fileIn.available() > 0) { oos.close();
final var object = objectIn.readObject();
blockList.add((Block) object);
}
} catch (final IOException | ClassNotFoundException e) {
e.printStackTrace();
}
} }
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 { 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; import java.security.MessageDigest;
public final class HashFunction { public final class HashFunction {
private HashFunction() {
throw new IllegalStateException("HashFunction class");
}
/* Applies Sha256 to a string and returns a hash. */ /* Applies Sha256 to a string and returns a hash. */
public static String applySha256(final String input) throws RuntimeException { public static String applySha256(final String input) throws RuntimeException {
try { try {
@@ -22,4 +27,5 @@ public final class HashFunction {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} }
} }

View File

@@ -7,12 +7,17 @@ import java.security.*;
import java.security.spec.InvalidKeySpecException; import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec; import java.security.spec.X509EncodedKeySpec;
import model.Message;
public final class Security { public final class Security {
public static final String PRIVATE_KEY = "KeyPair/privateKey"; public static final String PRIVATE_KEY = "KeyPair/privateKey";
public static final String PUBLIC_KEY = "KeyPair/publicKey"; public static final String PUBLIC_KEY = "KeyPair/publicKey";
private Security() {
throw new IllegalStateException("Security class");
}
public static PublicKey getPublic() public static PublicKey getPublic()
throws NoSuchAlgorithmException, IOException, InvalidKeySpecException { throws NoSuchAlgorithmException, IOException, InvalidKeySpecException {
final byte[] keyBytes = Files.readAllBytes(new File(PUBLIC_KEY).toPath()); final byte[] keyBytes = Files.readAllBytes(new File(PUBLIC_KEY).toPath());
@@ -37,6 +42,10 @@ public final class Security {
return rsa.sign(); 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) { public static boolean verifySignature(final String data, final byte[] signature, final PublicKey publicKey) {
try { try {
final Signature verifier = Signature.getInstance("SHA1withRSA"); final Signature verifier = Signature.getInstance("SHA1withRSA");