diff --git a/POO/src/communication/filetransfer/FileTransferUtils.java b/POO/src/communication/filetransfer/FileTransferUtils.java index 734bf08..ee7535a 100644 --- a/POO/src/communication/filetransfer/FileTransferUtils.java +++ b/POO/src/communication/filetransfer/FileTransferUtils.java @@ -102,7 +102,7 @@ public class FileTransferUtils { } /** - * @param imageString The base64 encoded string of an image. + * @param imageString The base64 encoded string of an image. * @return A buffered image corresponding to the given base64 encoded string. * @throws IOException */ diff --git a/POO/src/communication/tcp/TCPClient.java b/POO/src/communication/tcp/TCPClient.java index c5ca789..76e445a 100644 --- a/POO/src/communication/tcp/TCPClient.java +++ b/POO/src/communication/tcp/TCPClient.java @@ -74,7 +74,7 @@ public class TCPClient { } /** - * Method used when the session is over. Set all attribute references to null, + * Method used when the session is over. Set all attributes' references to null, * interrupt the inputThread and close the streams and the socket. */ public void destroyAll() { diff --git a/POO/src/communication/udp/CommunicationUDP.java b/POO/src/communication/udp/CommunicationUDP.java index f20cbe2..12021e9 100644 --- a/POO/src/communication/udp/CommunicationUDP.java +++ b/POO/src/communication/udp/CommunicationUDP.java @@ -20,7 +20,7 @@ public class CommunicationUDP extends Thread { private ObserverUserList obsList; /** - * Create the class that will manage the userlist and contain a UDPClient and a + * Create the object that will manage the userlist and contain a UDPClient and a * UDPServer. Since the applications will run on localhost, it needs to know * every UDPServer ports used in order to replicate a broadcast behaviour. * @@ -63,7 +63,7 @@ public class CommunicationUDP extends Thread { this.obsList = o; } - // -------------- USER LIST UPDATE FUNCTION --------------// + // -------------- USER LIST UPDATE METHODS -------------- // /** * Add a new user to the userlist and notify the observer. @@ -122,13 +122,13 @@ public class CommunicationUDP extends Thread { this.users.clear(); } - // -------------- CHECKERS --------------// + // -------------- CHECKERS -------------- // /** * Check if there is an user in the list that has the given id. * * @param id The user's id. - * @return true if the user is in the list + * @return True if the user is in the list * false otherwise. */ protected boolean containsUserFromID(String id) { @@ -144,7 +144,7 @@ public class CommunicationUDP extends Thread { * Check if there is an user in the list that has the given pseudo. * * @param pseudo The user's pseudo. - * @return true if the user is in the list + * @return True if the user is in the list * false otherwise. */ public boolean containsUserFromPseudo(String pseudo) { @@ -157,7 +157,7 @@ public class CommunicationUDP extends Thread { return false; } - // -------------- GETTERS --------------// + // -------------- GETTERS -------------- // /** * Return the user with the given pseudo if it exists in the list. @@ -191,7 +191,7 @@ public class CommunicationUDP extends Thread { return -1; } - // -------------- SEND MESSAGES --------------// + // -------------- SEND MESSAGES METHODS -------------- // /** * Send a message indicating this application's user is connected to every @@ -279,6 +279,9 @@ public class CommunicationUDP extends Thread { e.printStackTrace(); } } + + + // -------------- OTHERS -------------- // /** * Notify the observer with the updated list diff --git a/POO/src/communication/udp/UDPClient.java b/POO/src/communication/udp/UDPClient.java index 44ab75c..c796e6b 100644 --- a/POO/src/communication/udp/UDPClient.java +++ b/POO/src/communication/udp/UDPClient.java @@ -14,7 +14,7 @@ class UDPClient { private DatagramSocket sockUDP; /** - * Create a UDP client on the specified port. It will be used to notify the + * Create an UDP client on the specified port. It will be used to notify the * other users of this application's user state (Connected/Disconnected/Pseudo * changed). * diff --git a/POO/src/communication/udp/UDPServer.java b/POO/src/communication/udp/UDPServer.java index 9f3040d..ee117dd 100644 --- a/POO/src/communication/udp/UDPServer.java +++ b/POO/src/communication/udp/UDPServer.java @@ -17,7 +17,7 @@ class UDPServer extends Thread { /** - * Create a UDP Server on the specified port. It will be used to read the + * Create an UDP Server on the specified port. It will be used to read the * other users states (Connected/Disconnected/Pseudo). * * @param port diff --git a/POO/src/database/SQLiteCreateTables.java b/POO/src/database/SQLiteCreateTables.java index 45aa061..754931e 100644 --- a/POO/src/database/SQLiteCreateTables.java +++ b/POO/src/database/SQLiteCreateTables.java @@ -6,57 +6,101 @@ import java.sql.Statement; class SQLiteCreateTables { - + /** + * Create the user table if it does not exist in the database. + * An user is characterized by : + * - an unique username, + * - a password salt, + * - a database_key salt, + * - a password hash encrypted by the database_key, + * - the encrypted database_key, + * - the initialization vector used to encrypt the database key. + * + * @param connec The opened connection to the database. + * @throws SQLException + */ protected static void createTableUser(Connection connec) throws SQLException { String createTableUser = "CREATE TABLE IF NOT EXISTS user (\r\n" + " id INTEGER PRIMARY KEY AUTOINCREMENT,\r\n" + " username VARCHAR (50) NOT NULL\r\n" + " UNIQUE ON CONFLICT ROLLBACK,\r\n" - + " pwd_salt BLOB,\r\n" + + " pwd_salt BLOB,\r\n" + " db_datakey_salt BLOB,\r\n" - + " encrypted_pwd_hashsalt BLOB,\r\n" + + " encrypted_pwd_hashsalt BLOB,\r\n" + " encrypted_db_datakey BLOB,\r\n" - + " iv_datakey BLOB\r\n" - + ");"; + + " iv_datakey BLOB\r\n" + ");"; Statement stmt = connec.createStatement(); stmt.execute(createTableUser); } + /** + * Create the conversation table if it does not exist in the database. + * A conversation is characterized by : + * - the id of the user who sends the messages, + * - the id of the user who receives the messages, + * - an initialization vector used to encrypt the conversation's messages. + * + * During an session between two users, two conversations are created. + * + * @param connec The opened connection to the database. + * @throws SQLException + */ protected static void createTableConversation(Connection connec) throws SQLException { String createTableConversation = "CREATE TABLE IF NOT EXISTS conversation (\r\n" + " id_conversation INTEGER PRIMARY KEY AUTOINCREMENT,\r\n" - + " id_emetteur INTEGER REFERENCES user (id) \r\n" - + " NOT NULL,\r\n" - + " id_recepteur INTEGER REFERENCES user (id) \r\n" - + " NOT NULL,\r\n" - +" iv_conversation BLOB NOT NULL" - + ");"; + + " id_sender INTEGER REFERENCES user (id) \r\n" + " NOT NULL,\r\n" + + " id_receiver INTEGER REFERENCES user (id) \r\n" + " NOT NULL,\r\n" + + " iv_conversation BLOB NOT NULL" + ");"; Statement stmt = connec.createStatement(); stmt.execute(createTableConversation); } + /** + * Create the message table if it does not exist in the database. + * A message is characterized by : + * - the id of the conversation it belongs, + * - the id of its type, + * - its content, + * - the date when it was emitted, + * - its extension if it is a file (text or image). + * + * @param connec The opened connection to the database. + * @throws SQLException + */ protected static void createTableMessage(Connection connec) throws SQLException { String createTableMessage = "CREATE TABLE IF NOT EXISTS message (\r\n" + " id_message INTEGER PRIMARY KEY AUTOINCREMENT,\r\n" + " id_conversation INTEGER REFERENCES conversation (id_conversation) \r\n" + " NOT NULL,\r\n" + " id_type INTEGER REFERENCES type (id_type) \r\n" - + " NOT NULL,\r\n" + + " NOT NULL,\r\n" + " content BLOB,\r\n" - + " date INTEGER NOT NULL,\r\n" - + " extension VARCHAR (20) \r\n" - + ");\r\n"; + + " date INTEGER NOT NULL,\r\n" + + " extension VARCHAR (20) \r\n" + ");\r\n"; Statement stmt = connec.createStatement(); stmt.execute(createTableMessage); } + /** + * Create the (message) type table if it does not exist and insert the different + * types of message in the database. + * A type is characterized by : + * - a label. + * + * This table only exists because the type "enumeration" does not exist in SQLite. + * It is a static table that contains the different types of message stored in the database. + * + * @param connec The opened connection to the database. + * @throws SQLException + */ protected static void createTableType(Connection connec) throws SQLException { - String createTableType = "CREATE TABLE IF NOT EXISTS type (\r\n" + " id_type INTEGER PRIMARY KEY,\r\n" + String createTableType = "CREATE TABLE IF NOT EXISTS type (\r\n" + + " id_type INTEGER PRIMARY KEY,\r\n" + " label VARCHAR (20) NOT NULL\r\n" + ");"; Statement stmt = connec.createStatement(); diff --git a/POO/src/database/SQLiteEncprytion.java b/POO/src/database/SQLiteEncprytion.java deleted file mode 100644 index 38c0916..0000000 --- a/POO/src/database/SQLiteEncprytion.java +++ /dev/null @@ -1,84 +0,0 @@ -package database; - - -import java.util.Base64; -import java.util.Random; - -import javax.crypto.*; -import javax.crypto.spec.*; -import java.security.*; -import java.security.spec.*; - -class SQLiteEncprytion { - - private static final Random RANDOM = new SecureRandom(); - private static final int ITERATIONS = 10000; - private static final int KEY_LENGTH = 256; - protected static final String encryptAlgorithm = "AES/CBC/PKCS5Padding"; - - protected static byte[] getNextSalt() { - byte[] salt = new byte[24]; - RANDOM.nextBytes(salt); - return salt; - } - - - protected static byte[] hash(char[] password, byte[] salt) { - return SQLiteEncprytion.getKey(password, salt).getEncoded(); - } - - - protected static SecretKey getKey(char[] password, byte[] salt) { - PBEKeySpec saltpwd = new PBEKeySpec(password, salt, ITERATIONS, KEY_LENGTH); - - try { - SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1"); - SecretKey tmp = skf.generateSecret(saltpwd); - SecretKey key = new SecretKeySpec(tmp.getEncoded(), "AES"); - return key; - } catch (InvalidKeySpecException | NoSuchAlgorithmException e) { - e.printStackTrace(); - } finally { - saltpwd.clearPassword(); - } - return null; - } - - - public static IvParameterSpec generateIv() { - byte[] iv = new byte[16]; - new SecureRandom().nextBytes(iv); - return new IvParameterSpec(iv); - } - - - protected static byte[] encrypt(String algorithm, byte[] input, SecretKey key, IvParameterSpec iv) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException{ - Cipher cipher = Cipher.getInstance(algorithm); - cipher.init(Cipher.ENCRYPT_MODE, key, iv); - byte[] cipherText = cipher.doFinal(input); - return Base64.getEncoder().encode(cipherText); - } - - - protected static byte[] decryptByte(String algorithm, byte[] cipherText, SecretKey key, IvParameterSpec iv) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException{ - Cipher cipher = Cipher.getInstance(algorithm); - cipher.init(Cipher.DECRYPT_MODE, key, iv); - byte[] plainText = cipher.doFinal(Base64.getDecoder().decode(cipherText)); - return plainText; - } - - protected static String decryptString(String algorithm, byte[] cipherText, SecretKey key, IvParameterSpec iv) throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException { - return new String(SQLiteEncprytion.decryptByte(algorithm, cipherText, key, iv) ); - } - - - public static byte[] keyToByte(SecretKey key) { - return Base64.getEncoder().encode(key.getEncoded()); - } - - - public static SecretKey byteToKey(byte[] encodedKey) { - byte[] decodedKey = Base64.getDecoder().decode(encodedKey); - return new SecretKeySpec(decodedKey, 0, decodedKey.length, "AES"); - } -} diff --git a/POO/src/database/SQLiteEncryption.java b/POO/src/database/SQLiteEncryption.java new file mode 100644 index 0000000..c7fb761 --- /dev/null +++ b/POO/src/database/SQLiteEncryption.java @@ -0,0 +1,158 @@ +package database; + +import java.util.Base64; +import java.util.Random; + +import javax.crypto.*; +import javax.crypto.spec.*; +import java.security.*; +import java.security.spec.*; + +class SQLiteEncryption { + + private static final Random RANDOM = new SecureRandom(); + private static final int ITERATIONS = 10000; + private static final int KEY_LENGTH = 256; + protected static final String encryptAlgorithm = "AES/CBC/PKCS5Padding"; + + /** + * Return a 24 bytes salt. + * + * @return The salt in a byte array. + */ + protected static byte[] getNextSalt() { + byte[] salt = new byte[24]; + RANDOM.nextBytes(salt); + return salt; + } + + /** + * Return the hash of the given password with the given salt. + * + * @param password + * @param salt + * @return The hash in a byte array. + */ + protected static byte[] hash(char[] password, byte[] salt) { + return SQLiteEncryption.getKey(password, salt).getEncoded(); + } + + /** + * Return a secret key generated with the given password and salt. + * + * @param password + * @param salt + * @return The secret key. + */ + protected static SecretKey getKey(char[] password, byte[] salt) { + PBEKeySpec saltpwd = new PBEKeySpec(password, salt, ITERATIONS, KEY_LENGTH); + + try { + SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1"); + SecretKey tmp = skf.generateSecret(saltpwd); + SecretKey key = new SecretKeySpec(tmp.getEncoded(), "AES"); + return key; + } catch (InvalidKeySpecException | NoSuchAlgorithmException e) { + e.printStackTrace(); + } finally { + saltpwd.clearPassword(); + } + return null; + } + + /** + * Return a 16 bytes Initialization vector. + * + * @return The Initialization vector. + */ + protected static IvParameterSpec generateIv() { + byte[] iv = new byte[16]; + RANDOM.nextBytes(iv); + return new IvParameterSpec(iv); + } + + /** + * Encrypt the given input (byte array) with the given algorithm, secretKey and + * initialization vector. + * + * + * @param algorithm + * @param input + * @param key + * @param iv + * @return The encrypted input in a byte array. + * + * @throws NoSuchAlgorithmException + * @throws NoSuchPaddingException + * @throws InvalidKeyException + * @throws InvalidAlgorithmParameterException + * @throws IllegalBlockSizeException + * @throws BadPaddingException + */ + protected static byte[] encrypt(String algorithm, byte[] input, SecretKey key, IvParameterSpec iv) + throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, + InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException { + Cipher cipher = Cipher.getInstance(algorithm); + cipher.init(Cipher.ENCRYPT_MODE, key, iv); + byte[] cipherText = cipher.doFinal(input); + return Base64.getEncoder().encode(cipherText); + } + + /** + * Decrypt the given input (byte array) with the given algorithm, secretKey and + * initialization vector. + * + * @param algorithm + * @param input + * @param key + * @param iv + * @return The decrypted input in a byte array. + * + * @throws NoSuchAlgorithmException + * @throws NoSuchPaddingException + * @throws InvalidKeyException + * @throws InvalidAlgorithmParameterException + * @throws IllegalBlockSizeException + * @throws BadPaddingException + */ + protected static byte[] decryptByte(String algorithm, byte[] input, SecretKey key, IvParameterSpec iv) + throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, + InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException { + Cipher cipher = Cipher.getInstance(algorithm); + cipher.init(Cipher.DECRYPT_MODE, key, iv); + byte[] plainText = cipher.doFinal(Base64.getDecoder().decode(input)); + return plainText; + } + + /** + * Decrypt the given input (byte array) with the given algorithm, secretKey and + * initialization vector. + * + * @param algorithm + * @param input + * @param key + * @param iv + * @return The decrypted input as a String. + * + * @throws InvalidKeyException + * @throws NoSuchAlgorithmException + * @throws NoSuchPaddingException + * @throws InvalidAlgorithmParameterException + * @throws IllegalBlockSizeException + * @throws BadPaddingException + */ + protected static String decryptString(String algorithm, byte[] input, SecretKey key, IvParameterSpec iv) + throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, + InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException { + return new String(SQLiteEncryption.decryptByte(algorithm, input, key, iv)); + } + + protected static byte[] keyToByte(SecretKey key) { + return Base64.getEncoder().encode(key.getEncoded()); + } + + protected static SecretKey byteToKey(byte[] encodedKey) { + byte[] decodedKey = Base64.getDecoder().decode(encodedKey); + return new SecretKeySpec(decodedKey, 0, decodedKey.length, "AES"); + } +} diff --git a/POO/src/database/SQLiteManager.java b/POO/src/database/SQLiteManager.java index 707a4ba..60a3135 100644 --- a/POO/src/database/SQLiteManager.java +++ b/POO/src/database/SQLiteManager.java @@ -1,6 +1,5 @@ package database; -import java.io.File; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; @@ -19,7 +18,6 @@ import javax.crypto.NoSuchPaddingException; import javax.crypto.SecretKey; import javax.crypto.spec.IvParameterSpec; - import main.Utilisateur; import messages.MauvaisTypeMessageException; import messages.Message; @@ -30,50 +28,64 @@ import messages.MessageFichier; public class SQLiteManager { private static final String DATABASE_RELATIVE_PATH = "../database"; - - public static String[] hardcodedNames = {"Olivia","Liam","Benjamin","Sophia","Charlotte","Noah","Elijah","Isabella", - "Oliver","Emma","William","Amelia","Evelyn","James","Mia","Ava","Lucas","Mason","Ethan","Harper"}; - + + public static String[] hardcodedNames = { "Olivia", "Liam", "Benjamin", "Sophia", "Charlotte", "Noah", "Elijah", + "Isabella", "Oliver", "Emma", "William", "Amelia", "Evelyn", "James", "Mia", "Ava", "Lucas", "Mason", + "Ethan", "Harper" }; + private Connection connec; private int numDatabase; private SecretKey dbDataKey; + + /** + * Create the object that will interact with the database. Each time a + * SQLiteManager is created, it creates the tables for the application to work + * if they do not exist already. In the context of a local usage of the + * application, this constructor takes a number that is used to manage different + * database's files (see openConnection() ). + * + * @param numDatabase + */ public SQLiteManager(int numDatabase) { this.numDatabase = numDatabase; this.openConnection(); try { - + SQLiteCreateTables.createTableUser(this.connec); SQLiteCreateTables.createTableConversation(this.connec); SQLiteCreateTables.createTableType(this.connec); SQLiteCreateTables.createTableMessage(this.connec); - + } catch (SQLException e) { this.closeConnection(); - File db = new File("../database"+this.numDatabase+".db"); - if(db.delete()) { - System.out.println("supp"); - }else { - System.out.println("no supp"); - } e.printStackTrace(); } this.closeConnection(); } + + // -------------- CONNECTION METHODS -------------- // + + /** + * Open a connection to the file DATABASE_RELATIVE_PATH+this.numDatabase+".db" + */ private void openConnection() { - String url = "jdbc:sqlite:"+ DATABASE_RELATIVE_PATH + this.numDatabase + ".db"; + String url = "jdbc:sqlite:" + DATABASE_RELATIVE_PATH + this.numDatabase + ".db"; try { this.connec = DriverManager.getConnection(url); -// System.out.println("Connection to bdd established"); } catch (SQLException e) { System.out.println(e.getMessage()); } } + + /** + * Close this object connection, if it exists + */ private void closeConnection() { try { if (this.connec != null) { @@ -84,36 +96,52 @@ public class SQLiteManager { } } + - public int insertAllMessages(ArrayList messages, String usernameSender, String usernameReceiver) throws SQLException{ + // -------------- INSERT METHODS -------------- // + + /** + * Insert a collection of message in the database. They all correspond to the + * conversation between the sender and the receiver. The users and conversations + * are inserted in the database if they do not exist when this method is called. + * + * @param messages + * @param usernameSender + * @param usernameReceiver + * @return The number of messages inserted + * @throws SQLException + */ + public int insertAllMessages(ArrayList messages, String usernameSender, String usernameReceiver) + throws SQLException { int nbRows = 0; this.openConnection(); - + int idSender = this.getIDUser(usernameSender); int idReceiver = this.getIDUser(usernameReceiver); - - if(idSender == -1) { + + if (idSender == -1) { this.insertUser(usernameSender); idSender = this.getIDUser(usernameSender); } - - if(idReceiver == -1) { + + if (idReceiver == -1) { this.insertUser(usernameReceiver); - idReceiver = this.getIDUser(usernameReceiver); + idReceiver = this.getIDUser(usernameReceiver); } - + int idConversation = getIDConversation(idSender, idReceiver); - - if(idConversation == -1) { + + if (idConversation == -1) { this.insertConversation(idSender, idReceiver); idConversation = getIDConversation(idSender, idReceiver); } - + IvParameterSpec ivConversation = this.getIvConversation(idConversation); - + + // Disable autocommit to efficiently insert all the messages. this.connec.setAutoCommit(false); - - for(Message m : messages) { + + for (Message m : messages) { try { nbRows += this.insertMessage(idConversation, m, ivConversation); } catch (SQLException e) { @@ -121,103 +149,66 @@ public class SQLiteManager { this.connec.rollback(); } } - + + // Commit once all the messages are inserted this.connec.commit(); - + this.closeConnection(); - - //System.out.println("Nombre de message(s) insérée(s) : " + nbRows); - + return nbRows; } + + /** + * Insert a message corresponding to a given conversation (respresented by its + * id). The message content is first encrypted with the database key and the + * given iv. + * + * @param idConversation + * @param m + * @param iv + * @return The number of rows inserted (it should be 1). + * @throws SQLException + */ + private int insertMessage(int idConversation, Message m, IvParameterSpec iv) throws SQLException { - public ArrayList getHistoriquesMessages(String usernameOther, String pseudoOther) throws SQLException { - - this.openConnection(); - - - ArrayList messages = new ArrayList(); - - String usernameSelf = Utilisateur.getSelf().getId(); - - int idSelf = this.getIDUser(usernameSelf); - int idOther = this.getIDUser(usernameOther); - - int idConversationSelf = this.getIDConversation(idSelf, idOther); - int idConversationOther = this.getIDConversation(idOther, idSelf); - IvParameterSpec ivConversation = this.getIvConversation(idConversationSelf); -// String str = "datetime(d1,'unixepoch','localtime')"; - - - String getHistoriqueRequest = "SELECT id_conversation, id_type, content, date, extension " - + "FROM message " - + "WHERE id_conversation IN (?,?) " - + "ORDER by date"; - - PreparedStatement prepStmt = this.connec.prepareStatement(getHistoriqueRequest); - prepStmt.setInt(1, idConversationSelf); - prepStmt.setInt(2, idConversationOther); - ResultSet res = prepStmt.executeQuery(); - - //Retrieve the messages one by one - //Create the appropriate message object depending on the type and sender/receiver - //and add the message in the list - while(res.next()) { - int idType = res.getInt("id_type"); - String type = this.getType(idType); - - String content = null; - try { - content = this.bytesToStringContent(res.getBytes("content"), ivConversation); - } catch (InvalidKeyException | NoSuchAlgorithmException | NoSuchPaddingException - | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException - | SQLException e1) { - //System.out.println("erreur déchiffrement"); - } - - Message message = null; - String extension; - - if(!type.equals("")) { - try { - switch(type) { - case "text": - message = new MessageTexte(TypeMessage.TEXTE, content); - break; - case "file": - extension = res.getString("extension"); - message = new MessageFichier(TypeMessage.FICHIER, content, extension); - break; - default: - extension = res.getString("extension"); - message = new MessageFichier(TypeMessage.IMAGE, content, extension); - } - - } catch (MauvaisTypeMessageException e) { - e.printStackTrace(); - } - } - - if(res.getInt("id_conversation") == idConversationSelf) { - message.setSender("Moi"); - }else{ - message.setSender(pseudoOther); - } - message.setDateMessage(res.getString("date")); - if(content != null) { - messages.add(message); - } - + String dateMessage = m.getDateMessage(); + String extension = this.processExtension(m); + String type = this.processMessageType(m); + int idType = this.getIDType(type); + + byte[] content = null; + + try { + content = this.stringToBytesContent(m, iv); + } catch (InvalidKeyException | NoSuchAlgorithmException | NoSuchPaddingException + | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) { + + e.printStackTrace(); } - - - this.closeConnection(); - - return messages; + + String insertMessageRequest = "INSERT INTO message(id_conversation, id_type, content, date, extension) " + + "VALUES (?, ?, ?, ?, ?);"; + + PreparedStatement prepStmt = this.connec.prepareStatement(insertMessageRequest); + prepStmt.setInt(1, idConversation); + prepStmt.setInt(2, idType); + prepStmt.setBytes(3, content); + prepStmt.setString(4, dateMessage); + prepStmt.setString(5, extension); + + int nbRows = prepStmt.executeUpdate(); + + return nbRows; } - + + /** + * Insert an user in the database with only its username. + * + * @param username + * @throws SQLException + */ private void insertUser(String username) throws SQLException { String insertUserRequest = "INSERT INTO user (username) " + "VALUES (?);"; @@ -225,8 +216,141 @@ public class SQLiteManager { prepStmt.setString(1, username); prepStmt.executeUpdate(); } + + /** + * Insert the two conversations corresponding to a session between two users + * (they both can be sender and receiver). + * + * @param idUser1 + * @param idUser2 + * @throws SQLException + */ + private void insertConversation(int idUser1, int idUser2) throws SQLException { + String insertConversationRequest = "INSERT INTO conversation (id_sender, id_receiver, iv_conversation) " + + "VALUES " + "(?, ?, ?)," + "(?, ?, ?);"; + + byte[] ivConversation = SQLiteEncryption.generateIv().getIV(); + + PreparedStatement prepStmt = this.connec.prepareStatement(insertConversationRequest); + prepStmt.setInt(1, idUser1); + prepStmt.setInt(2, idUser2); + prepStmt.setBytes(3, ivConversation); + prepStmt.setInt(4, idUser2); + prepStmt.setInt(5, idUser1); + prepStmt.setBytes(6, ivConversation); + + prepStmt.executeUpdate(); + } + + // -------------- GET METHODS -------------- // + + /** + * Get the message record between two users. In context, it only needs the + * username and the id of the other user since we can get this application's + * user's data with Utilisateur.getSelf(). + * + * @param usernameOther + * @param pseudoOther + * @return the messages previously exchanged in chronological order or null if + * there is none. + * @throws SQLException + */ + public ArrayList getMessageRecord(String usernameOther, String pseudoOther) throws SQLException { + + this.openConnection(); + + ArrayList messages = new ArrayList(); + + String usernameSelf = Utilisateur.getSelf().getId(); + + // Get the ids from the usernames + int idSelf = this.getIDUser(usernameSelf); + int idOther = this.getIDUser(usernameOther); + + // Get the two conversations corresponding to the exchanges between the two + // users + int idConversationSelf = this.getIDConversation(idSelf, idOther); + int idConversationOther = this.getIDConversation(idOther, idSelf); + IvParameterSpec ivConversation = this.getIvConversation(idConversationSelf); + + // Get all the messages + String getHistoriqueRequest = "SELECT id_conversation, id_type, content, date, extension " + "FROM message " + + "WHERE id_conversation IN (?,?) " + "ORDER by date"; + + PreparedStatement prepStmt = this.connec.prepareStatement(getHistoriqueRequest); + prepStmt.setInt(1, idConversationSelf); + prepStmt.setInt(2, idConversationOther); + ResultSet res = prepStmt.executeQuery(); + + // Process the messages one by one + // Create the appropriate message object depending on the type and + // sender/receiver + // and add the message in the list + while (res.next()) { + int idType = res.getInt("id_type"); + String type = this.getType(idType); + + String content = null; + try { + // Decrypt the message's content with the database key and the conversation's + // iv. + content = this.bytesToStringContent(res.getBytes("content"), ivConversation); + } catch (InvalidKeyException | NoSuchAlgorithmException | NoSuchPaddingException + | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException + | SQLException e1) { + } + + Message message = null; + String extension; + + if (!type.equals("")) { + try { + switch (type) { + case "text": + message = new MessageTexte(TypeMessage.TEXTE, content); + break; + case "file": + extension = res.getString("extension"); + message = new MessageFichier(TypeMessage.FICHIER, content, extension); + break; + default: + extension = res.getString("extension"); + message = new MessageFichier(TypeMessage.IMAGE, content, extension); + } + + } catch (MauvaisTypeMessageException e) { + e.printStackTrace(); + } + } + + if (res.getInt("id_conversation") == idConversationSelf) { + message.setSender("Moi"); + } else { + message.setSender(pseudoOther); + } + message.setDateMessage(res.getString("date")); + if (content != null) { + messages.add(message); + } + + } + + this.closeConnection(); + + return messages; + } + + + /** + * Return the id of the user with the given username if it exists in the + * database. + * + * @param username + * @return The id of the user or -1 if he does not exist in the database. + * @throws SQLException + */ private int getIDUser(String username) throws SQLException { String getIDRequest = "SELECT id " + " FROM user" + " WHERE username = ? ;"; @@ -241,29 +365,19 @@ public class SQLiteManager { } - - private void insertConversation(int idSender, int idReceiver) throws SQLException { - String insertConversationRequest = "INSERT INTO conversation (id_emetteur, id_recepteur, iv_conversation) " + "VALUES " - + "(?, ?, ?)," - + "(?, ?, ?);"; - - byte[] ivConversation = SQLiteEncprytion.generateIv().getIV(); - - PreparedStatement prepStmt = this.connec.prepareStatement(insertConversationRequest); - prepStmt.setInt(1, idSender); - prepStmt.setInt(2, idReceiver); - prepStmt.setBytes(3, ivConversation); - prepStmt.setInt(4, idReceiver); - prepStmt.setInt(5, idSender); - prepStmt.setBytes(6, ivConversation); - - prepStmt.executeUpdate(); - } - + /** + * Get the id of the conversation between two users (represented by their ids). + * + * @param idSender + * @param idReceiver + * @return The id of the conversation or -1 if the conversation does not exists + * in the database. + * @throws SQLException + */ private int getIDConversation(int idSender, int idReceiver) throws SQLException { - String getIDRequest = "SELECT id_conversation " + "FROM conversation " + "WHERE id_emetteur = ? " - + "AND id_recepteur = ? ;"; + String getIDRequest = "SELECT id_conversation " + "FROM conversation " + "WHERE id_sender = ? " + + "AND id_receiver = ? ;"; PreparedStatement prepStmt = this.connec.prepareStatement(getIDRequest); prepStmt.setInt(1, idSender); @@ -275,10 +389,20 @@ public class SQLiteManager { } return -1; } + + /** + * Get the initialization vector for a given conversation (represented by its + * id). + * + * @param idConversation + * @return The iv corresponding to the conversation or null if the conversation + * does not exist in the database. + * @throws SQLException + */ private IvParameterSpec getIvConversation(int idConversation) throws SQLException { String getIvRequest = "SELECT iv_conversation " + "FROM conversation " + "WHERE id_conversation = ?;"; - + PreparedStatement prepStmt = this.connec.prepareStatement(getIvRequest); prepStmt.setInt(1, idConversation); ResultSet res = prepStmt.executeQuery(); @@ -286,121 +410,170 @@ public class SQLiteManager { if (res.next()) { return new IvParameterSpec(res.getBytes("iv_conversation")); } - + return null; } - - + /** + * Get the id of the message type corresponding to the given label. + * + * @param label + * @return The id of the message type or -1 if it does not exist in the + * database. + * @throws SQLException + */ private int getIDType(String label) throws SQLException { String getIDRequest = "SELECT id_type FROM type WHERE label = ?;"; - + PreparedStatement prepStmt = this.connec.prepareStatement(getIDRequest); prepStmt.setString(1, label); - + ResultSet res = prepStmt.executeQuery(); - + if (res.next()) { return res.getInt("id_type"); } return -1; } + - + /** + * Get the label of the message type corresponding to the given id. + * + * @param idType + * @return The label of the message type or "" if it does not exist in the + * database. + * @throws SQLException + */ private String getType(int idType) throws SQLException { String getTypeRequest = "SELECT label FROM type WHERE id_type = ?;"; - + PreparedStatement prepStmt = this.connec.prepareStatement(getTypeRequest); prepStmt.setInt(1, idType); - + ResultSet res = prepStmt.executeQuery(); - - if(res.next()) { + + if (res.next()) { return res.getString("label"); } - + return ""; } + + // -------------- PROCESSING MESSAGE METHODS -------------- // - private byte[] stringToBytesContent(Message m, IvParameterSpec iv) throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException { + /** + * Convert a message in bytes. First get the content of the message. Then + * encrypt it with the database_key and the given iv (initialization vector). + * + * @param m + * @param iv + * @return The bytes of the encrypted message's content. + * + * @throws InvalidKeyException + * @throws NoSuchAlgorithmException + * @throws NoSuchPaddingException + * @throws InvalidAlgorithmParameterException + * @throws IllegalBlockSizeException + * @throws BadPaddingException + */ + private byte[] stringToBytesContent(Message m, IvParameterSpec iv) + throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, + InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException { String content; if (m.getTypeMessage() == TypeMessage.TEXTE) { MessageTexte messageTxt = (MessageTexte) m; content = messageTxt.getContenu(); - }else { + } else { MessageFichier messageFichier = (MessageFichier) m; content = messageFichier.getContenu(); } - byte[] encryptedContent = SQLiteEncprytion.encrypt(SQLiteEncprytion.encryptAlgorithm, content.getBytes(), this.dbDataKey, iv); + byte[] encryptedContent = SQLiteEncryption.encrypt(SQLiteEncryption.encryptAlgorithm, content.getBytes(), + this.dbDataKey, iv); return encryptedContent; - - } - - private String bytesToStringContent(byte[] encryptedContent, IvParameterSpec iv) throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException { - return SQLiteEncprytion.decryptString(SQLiteEncprytion.encryptAlgorithm, encryptedContent, this.dbDataKey, iv); + } + /** + * Convert bytes in a String. In this context, the bytes correspond to the + * encrypted content of a message. + * + * @param encryptedContent + * @param iv + * @return The String corresponding to the decrypted bytes. + * + * @throws InvalidKeyException + * @throws NoSuchAlgorithmException + * @throws NoSuchPaddingException + * @throws InvalidAlgorithmParameterException + * @throws IllegalBlockSizeException + * @throws BadPaddingException + */ + private String bytesToStringContent(byte[] encryptedContent, IvParameterSpec iv) + throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, + InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException { + return SQLiteEncryption.decryptString(SQLiteEncryption.encryptAlgorithm, encryptedContent, this.dbDataKey, iv); + } + + + /** + * Process the type of a message + * + * @param m + * @return The type of the message + */ private String processMessageType(Message m) { switch (m.getTypeMessage()) { - case TEXTE: return "text"; - case FICHIER: return "file"; - case IMAGE: return "image"; - default: return ""; + case TEXTE: + return "text"; + case FICHIER: + return "file"; + case IMAGE: + return "image"; + default: + return ""; } } + + /** + * Process the extension of a message + * + * @param m + * @return The extension of the message. null if it is a simple text message. + */ private String processExtension(Message m) { - if(m.getTypeMessage() == TypeMessage.TEXTE) { + if (m.getTypeMessage() == TypeMessage.TEXTE) { return null; - }else { + } else { MessageFichier mFile = (MessageFichier) m; return mFile.getExtension(); } } + // -------------- USER SECURITY RELATED METHODS -------------- // + - private int insertMessage(int idConversation, Message m, IvParameterSpec iv) throws SQLException { - - - String dateMessage = m.getDateMessage(); - String extension = this.processExtension(m); - String type = this.processMessageType(m); - int idType = this.getIDType(type); - - byte[] content = null; - - try { - content = this.stringToBytesContent(m, iv); - } catch (InvalidKeyException | NoSuchAlgorithmException | NoSuchPaddingException - | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) { - - e.printStackTrace(); - } - - String insertMessageRequest = "INSERT INTO message(id_conversation, id_type, content, date, extension) " - + "VALUES (?, ?, ?, ?, ?);"; - - PreparedStatement prepStmt = this.connec.prepareStatement(insertMessageRequest); - prepStmt.setInt(1, idConversation); - prepStmt.setInt(2, idType); - prepStmt.setBytes(3, content); - prepStmt.setString(4, dateMessage); - prepStmt.setString(5, extension); - - int nbRows = prepStmt.executeUpdate(); - - return nbRows; - } - + /** + * Creates a new user in the database from a given username and password. The + * username is stored in plain text. An encrypted hash of the password is + * stored. The key used to encrypt the password's hash is itself encrypted then + * stored. Every other useful data to decrypt the key and compare the password's + * hash is stored as plaintext. + * + * Fail if a user with the given username already exists in the database. + * + * @param username + * @param password + */ public void createNewUserEncrypt(String username, String password) { - - - String algo = SQLiteEncprytion.encryptAlgorithm; - + + String algo = SQLiteEncryption.encryptAlgorithm; + KeyGenerator keyGen = null; try { keyGen = KeyGenerator.getInstance("AES"); @@ -408,37 +581,35 @@ public class SQLiteManager { } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } - - byte[] passwordSalt = SQLiteEncprytion.getNextSalt(); - byte[] dbDataKeySalt = SQLiteEncprytion.getNextSalt(); - - SecretKey dbDataKey = keyGen.generateKey(); - - SecretKey dbDataEncryptKey = SQLiteEncprytion.getKey(password.toCharArray(), dbDataKeySalt); - IvParameterSpec ivDbDataKey = SQLiteEncprytion.generateIv(); - - byte[] passwordHash = SQLiteEncprytion.hash(password.toCharArray(), passwordSalt); - + + byte[] passwordSalt = SQLiteEncryption.getNextSalt(); + byte[] dbDataKeySalt = SQLiteEncryption.getNextSalt(); + + SecretKey dbDataKey = keyGen.generateKey(); + + SecretKey dbDataEncryptKey = SQLiteEncryption.getKey(password.toCharArray(), dbDataKeySalt); + IvParameterSpec ivDbDataKey = SQLiteEncryption.generateIv(); + + byte[] passwordHash = SQLiteEncryption.hash(password.toCharArray(), passwordSalt); + byte[] dbDataKeyEncrypted = null; byte[] encryptedPasswordHash = null; - + try { - dbDataKeyEncrypted = SQLiteEncprytion.encrypt( - algo, SQLiteEncprytion.keyToByte(dbDataKey), dbDataEncryptKey, ivDbDataKey); - encryptedPasswordHash = SQLiteEncprytion.encrypt( - algo, passwordHash , dbDataKey, ivDbDataKey); - + dbDataKeyEncrypted = SQLiteEncryption.encrypt(algo, SQLiteEncryption.keyToByte(dbDataKey), dbDataEncryptKey, + ivDbDataKey); + encryptedPasswordHash = SQLiteEncryption.encrypt(algo, passwordHash, dbDataKey, ivDbDataKey); + } catch (InvalidKeyException | NoSuchAlgorithmException | NoSuchPaddingException | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) { - // TODO Auto-generated catch block e.printStackTrace(); } - + this.openConnection(); - + String createUserRequest = "INSERT INTO user(username, pwd_salt, db_datakey_salt, encrypted_pwd_hashsalt, encrypted_db_datakey, iv_datakey) " + "VALUES (?, ?, ?, ?, ?, ?); "; - + PreparedStatement prepStmt = null; try { prepStmt = this.connec.prepareStatement(createUserRequest); @@ -448,115 +619,142 @@ public class SQLiteManager { prepStmt.setBytes(4, encryptedPasswordHash); prepStmt.setBytes(5, dbDataKeyEncrypted); prepStmt.setBytes(6, ivDbDataKey.getIV()); - + } catch (SQLException e) { - // TODO Auto-generated catch block e.printStackTrace(); } - + try { prepStmt.executeUpdate(); System.out.println("Utilisateur crée"); } catch (SQLException e) { System.out.println("Nom d'utilisateur déjà pris"); } - + this.closeConnection(); - + } + /** + * Check if a given password has the same hash (= they're the same) as the one + * stored in the database for a given username. * * @param username * @param password - * @return -1 if user do not exists, - * 0 if password incorrect, - * 1 if password correct + * @return -1 if user do not exists, 0 if password is incorrect, 1 if password + * is correct * @throws SQLException */ public int checkPwd(String username, char[] password) throws SQLException { - + this.openConnection(); - + String selectUserDataRequest = "SELECT pwd_salt, db_datakey_salt, encrypted_pwd_hashsalt, encrypted_db_datakey, iv_datakey " - + "FROM user " - + "WHERE username = ?;"; + + "FROM user " + "WHERE username = ?;"; PreparedStatement prepStmt; prepStmt = this.connec.prepareStatement(selectUserDataRequest); prepStmt.setString(1, username); - + ResultSet res = prepStmt.executeQuery(); - if(!res.next()) { + if (!res.next()) { return -1; } - + byte[] passwordSalt = res.getBytes("pwd_salt"); + + if (passwordSalt == null) { + return 0; + } + byte[] dbDataKeySalt = res.getBytes("db_datakey_salt"); - - SecretKey dbDataEncryptKey = SQLiteEncprytion.getKey(password, dbDataKeySalt); - IvParameterSpec iv = new IvParameterSpec(res.getBytes("iv_datakey")); - + + SecretKey dbDataEncryptKey = SQLiteEncryption.getKey(password, dbDataKeySalt); + + byte[] ivBytes = res.getBytes("iv_datakey"); + + if (ivBytes == null) { + return 0; + } + + IvParameterSpec iv = new IvParameterSpec(ivBytes); byte[] encryptedDbDataKey = res.getBytes("encrypted_db_datakey"); - - + SecretKey dbDataKey = null; try { - dbDataKey = SQLiteEncprytion.byteToKey( - SQLiteEncprytion.decryptByte(SQLiteEncprytion.encryptAlgorithm, encryptedDbDataKey, dbDataEncryptKey, iv) - ); + dbDataKey = SQLiteEncryption.byteToKey(SQLiteEncryption.decryptByte(SQLiteEncryption.encryptAlgorithm, + encryptedDbDataKey, dbDataEncryptKey, iv)); } catch (InvalidKeyException | NoSuchAlgorithmException | NoSuchPaddingException | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) { - //System.out.println("Problème déchiffrement clé db"); } - - this.dbDataKey = dbDataKey; - + byte[] encryptedPasswordHash = res.getBytes("encrypted_pwd_hashsalt"); - - - byte[] passwordHash = SQLiteEncprytion.hash(password, passwordSalt); - + + byte[] passwordHash = SQLiteEncryption.hash(password, passwordSalt); + this.closeConnection(); - - boolean checkHash = this.checkHashPwd(passwordHash ,encryptedPasswordHash, dbDataKey, iv); - if(checkHash) { + + boolean checkHash = this.checkHashPwd(passwordHash, encryptedPasswordHash, dbDataKey, iv); + if (checkHash) { + + // Set the database key to be used in future encryptions if the given password is + // correct. + this.dbDataKey = dbDataKey; return 1; } - return 0; + + return 0; } - - private boolean checkHashPwd(byte[] passwordHash, byte[] encryptedPasswordHash, SecretKey dbDataKey, IvParameterSpec iv) { - + + /** + * Check if two given hash are the same once the second one has been decrypted + * with the given key and iv. + * + * @param passwordHash + * @param encryptedPasswordHash + * @param dbDataKey + * @param iv + * @return true if the first hash is equal to the second one once decrypted, + * false otherwise. + * + */ + private boolean checkHashPwd(byte[] passwordHash, byte[] encryptedPasswordHash, SecretKey dbDataKey, + IvParameterSpec iv) { + byte[] expectedHash = "".getBytes(); try { - expectedHash = SQLiteEncprytion.decryptByte(SQLiteEncprytion.encryptAlgorithm, encryptedPasswordHash, dbDataKey, iv); + expectedHash = SQLiteEncryption.decryptByte(SQLiteEncryption.encryptAlgorithm, encryptedPasswordHash, + dbDataKey, iv); } catch (InvalidKeyException | NoSuchAlgorithmException | NoSuchPaddingException | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) { } - - - - if (passwordHash.length != expectedHash.length) return false; - for (int i = 0; i < passwordHash.length; i++) { - if (passwordHash[i] != expectedHash[i]) return false; - } - return true; + + if (passwordHash.length != expectedHash.length) + return false; + for (int i = 0; i < passwordHash.length; i++) { + if (passwordHash[i] != expectedHash[i]) + return false; + } + return true; } + + // Main to create 20 users in the database with the given number public static void main(String[] args) { - String[] hardcodedNames = {"Olivia","Liam","Benjamin","Sophia","Charlotte","Noah","Elijah","Isabella", - "Oliver","Emma","William","Amelia","Evelyn","James","Mia","Ava","Lucas","Mason","Ethan","Harper"}; - + String[] hardcodedNames = { "Olivia", "Liam", "Benjamin", "Sophia", "Charlotte", "Noah", "Elijah", "Isabella", + "Oliver", "Emma", "William", "Amelia", "Evelyn", "James", "Mia", "Ava", "Lucas", "Mason", "Ethan", + "Harper" }; + String pwdPrefix = "aze1$"; - - SQLiteManager sqlManager = new SQLiteManager(1); - - for(int i=0; i messagesOut; private SQLiteManager sqlManager; private ArrayList files; - - + /** + * Create the controller for this session. It will manage all the objects used + * to send/receive messages and files as well as the ones interacting with the + * database. It will handle the actions performed on the view and call the + * appropriate methods to display messages and data on the view. * - * @param vue - * @param socketComm - * @param idOther - * @param pseudoOther - * @param sqlManager + * @param vue The corresponding view + * @param socketComm The socket used to send/receive messages + * @param idOther The other user's id + * @param pseudoOther The other user's pseudo + * @param sqlManager The SQLManager instance to retrieve/insert + * users,conversations,messages from/into the database * @throws IOException */ - protected ControleurSession(VueSession vue, Socket socketComm, String idOther, String pseudoOther, SQLiteManager sqlManager) throws IOException { + protected ControleurSession(VueSession vue, Socket socketComm, String idOther, String pseudoOther, + SQLiteManager sqlManager) throws IOException { this.vue = vue; this.tcpClient = new TCPClient(socketComm); this.tcpClient.setObserverInputThread(this); @@ -57,173 +62,247 @@ public class ControleurSession implements ActionListener, ObserverInputMessage, this.tcpClient.startInputThread(); this.messagesIn = new ArrayList(); this.messagesOut = new ArrayList(); - + this.idOther = idOther; this.pseudoOther = pseudoOther; - + this.sqlManager = sqlManager; - + this.files = new ArrayList(); } - // ---------- ACTION LISTENER OPERATIONS ----------// + + // ---------- ACTION LISTENER OPERATIONS ---------- // @Override public void actionPerformed(ActionEvent e) { - //If the button "Envoyer" is pressed + // If the button "Envoyer" is pressed if ((JButton) e.getSource() == this.vue.getButtonEnvoyer()) { String messageContent = this.vue.getInputedText(); System.out.println(messageContent); - - if(!this.files.isEmpty()) { + + if (!this.files.isEmpty()) { this.processSelectedFiles(messageContent); - if(!this.files.isEmpty()) { + if (!this.files.isEmpty()) { this.askFileTransfer(); - + this.vue.resetZoneSaisie(); messageContent = ""; } } - - - //If the text field is not empty + + // If the text field is not empty if (!messageContent.equals("")) { - - //Retrieve the date and prepare the messages to send/display + + // Retrieve the date and prepare the messages to send/display MessageTexte messageOut = null; - + try { messageOut = new MessageTexte(TypeMessage.TEXTE, messageContent); messageOut.setSender(Utilisateur.getSelf().getPseudo()); } catch (MauvaisTypeMessageException e2) { e2.printStackTrace(); } - - + try { - this.tcpClient.sendMessage(messageOut); + this.tcpClient.sendMessage(messageOut); } catch (IOException e1) { e1.printStackTrace(); } - + messageOut.setSender("Moi"); this.vue.appendMessage(messageOut); this.vue.resetZoneSaisie(); - + this.messagesOut.add(messageOut); } } - - //If the button "Importer" is pressed - if((JButton) e.getSource() == this.vue.getButtonImportFile()) { - //Display a file chooser to select one or several files + + // If the button "Importer" is pressed + if ((JButton) e.getSource() == this.vue.getButtonImportFile()) { + + // Display a file chooser to select one or several files JFileChooser fc = new JFileChooser(); fc.setMultiSelectionEnabled(true); int returVal = fc.showDialog(this.vue, "Importer"); - - - if(returVal == JFileChooser.APPROVE_OPTION) { + + // If the user clicked on "Importer", + // Retrieve all the files he clicked on. + // The files are stored in this.files + // and their names are display in the ChatInput. + if (returVal == JFileChooser.APPROVE_OPTION) { File[] files = fc.getSelectedFiles(); Collections.addAll(this.files, files); - for(File file : files) { + for (File file : files) { this.vue.appendInputedText(file.getName()); this.vue.appendInputedText(";"); } } - + } } + + // ---------- KEY LISTENER METHODS ---------- // @Override - public void keyTyped(KeyEvent e) {} - - @Override - public void keyPressed(KeyEvent e) { - if(e.getKeyCode() == KeyEvent.VK_ENTER) { - if(!e.isShiftDown()) { - this.vue.getButtonEnvoyer().doClick(); - } - - } + public void keyTyped(KeyEvent e) { } + @Override - public void keyReleased(KeyEvent e) {} + public void keyPressed(KeyEvent e) { + if (e.getKeyCode() == KeyEvent.VK_ENTER) { + if (!e.isShiftDown()) { + this.vue.getButtonEnvoyer().doClick(); + } + + } + } + + @Override + public void keyReleased(KeyEvent e) { + } + - protected ArrayList getHistorique(){ + // ---------- OTHERS ---------- // + + /** + * Create and send the message to ask for a file transfer. + */ + private void askFileTransfer() { try { - ArrayList historique = this.sqlManager.getHistoriquesMessages(idOther, pseudoOther); + MessageFichier messageOut = new MessageFichier(TypeMessage.FICHIER_INIT, "" + this.files.size(), ""); + this.tcpClient.sendMessage(messageOut); + } catch (MauvaisTypeMessageException | IOException e1) { + e1.printStackTrace(); + } + } + + + /** + * Create and send the answer with the port on which the FileTransferServer is + * listening. + * + * @param port + */ + private void answerFileTransfer(int port) { + try { + MessageFichier messageOut = new MessageFichier(TypeMessage.FICHIER_ANSWER, "" + port, ""); + this.tcpClient.sendMessage(messageOut); + } catch (MauvaisTypeMessageException | IOException e1) { + e1.printStackTrace(); + } + } + + + /** + * Retrieve the files' names from the given input using ";" as a separator + * Removes the files whose names are missing. + * + * This method is used to check if a file's name has been deleted/overwritten in + * the ChatInput. Indeed, the only way to cancel the import of a file is by + * deleting its name from the ChatInput. + * + * @param input + */ + private void processSelectedFiles(String input) { + String[] tmp = input.split(";"); + ArrayList potentialFiles = new ArrayList(); + Collections.addAll(potentialFiles, tmp); + + for (File file : this.files) { + if (!potentialFiles.contains(file.getName())) { + this.files.remove(file); + } + } + } + + + /** + * Retrieve the messages previously exchanged between the current user of the + * application and the other user of this session + * + * @return The ArrayList of all previous messages + */ + protected ArrayList getHistorique() { + try { + ArrayList historique = this.sqlManager.getMessageRecord(idOther, pseudoOther); return historique; } catch (SQLException e) { e.printStackTrace(); return new ArrayList(); - + } } + - private void processSelectedFiles(String input) { - String[] tmp = input.split(";"); - ArrayList potentialFiles = new ArrayList(); - Collections.addAll(potentialFiles, tmp); - - for(File file: this.files) { - if(!potentialFiles.contains(file.getName()) ) { - this.files.remove(file); - } - } - } - - private void askFileTransfer() { + /** + * Method used when the session is over. Insert every message exchanged in the + * database, set all attributes' references to null, and call destroyAll() on + * the TCPClient. + */ + protected void destroyAll() { + String idSelf = Utilisateur.getSelf().getId(); + String idOther = this.idOther; + try { - MessageFichier messageOut = new MessageFichier(TypeMessage.FICHIER_INIT, ""+this.files.size(), ""); - this.tcpClient.sendMessage(messageOut); - } catch (MauvaisTypeMessageException | IOException e1) { - e1.printStackTrace(); + this.sqlManager.insertAllMessages(messagesOut, idSelf, idOther); + this.sqlManager.insertAllMessages(messagesIn, idOther, idSelf); + } catch (SQLException e) { + e.printStackTrace(); } + + this.vue = null; + this.tcpClient.destroyAll(); + this.tcpClient = null; } + - private void answerFileTransfer(int port) { - try { - MessageFichier messageOut = new MessageFichier(TypeMessage.FICHIER_ANSWER, ""+port, ""); - this.tcpClient.sendMessage(messageOut); - } catch (MauvaisTypeMessageException | IOException e1) { - e1.printStackTrace(); - } - } - - //Method called when a message is received from the TCP socket + // ---------- OBSERVERS ---------- // + + // Method called when a message is received from the TCP socket @Override public void updateInput(Object o, Object arg) { Message message = (Message) arg; - - switch(message.getTypeMessage()) { + + switch (message.getTypeMessage()) { + + // If it is a simple text message, display it case TEXTE: System.out.println(message.toString()); this.vue.appendMessage(message); this.messagesIn.add(message); break; + + // If it is an image, display a thumbnail case IMAGE: this.vue.appendImage(message); - - if(message.getSender().equals("Moi")) { + + if (message.getSender().equals("Moi")) { this.messagesOut.add(message); - }else { + } else { this.messagesIn.add(message); } break; + + // If it is a file, display a message saying whether it has been sent/received. case FICHIER: this.vue.appendMessage(message); - - if(message.getSender().equals("Moi")) { + + if (message.getSender().equals("Moi")) { this.messagesOut.add(message); - }else { + } else { this.messagesIn.add(message); } break; - + + // If it is a demand for a file transfer, create a new FileTransferServer, start + // it + // and answer back with "FICHIER_ANSWER" message containing the port of the + // server. case FICHIER_INIT: try { MessageFichier mFichier = (MessageFichier) arg; @@ -232,57 +311,42 @@ public class ControleurSession implements ActionListener, ObserverInputMessage, int port = fts.getPort(); fts.start(); this.answerFileTransfer(port); - + } catch (IOException e) { e.printStackTrace(); } break; + + // If it is an answer for a file transfer, create a FilteTransferClient + // with the port received and the path of the file(s) to send, + // and send the files. case FICHIER_ANSWER: try { MessageFichier mFichier = (MessageFichier) arg; int port = Integer.parseInt(mFichier.getContenu()); - + @SuppressWarnings("unchecked") - FileTransferClient ftc = new FileTransferClient(port ,(ArrayList) this.files.clone(), this); - + FileTransferClient ftc = new FileTransferClient(port, (ArrayList) this.files.clone(), this); + ftc.sendFiles(); this.files.clear(); } catch (IOException | InterruptedException e) { e.printStackTrace(); } - + break; - - //Do nothing + + // Do nothing default: } - + } - //If the other user closes the session or the communication is broken - //Disable the view (TextArea, Buttons..) and display a message + // If the other user closes the session or the communication is broken + // Disable the view (TextArea, Buttons..) and display a message @Override public void updateSocketState(Object o, Object arg) { - this.vue.endSession(this.pseudoOther); - } - - /** - * - */ - protected void destroyAll() { - String idSelf = Utilisateur.getSelf().getId(); - String idOther = this.idOther; - - try { - this.sqlManager.insertAllMessages(messagesOut, idSelf, idOther); - this.sqlManager.insertAllMessages(messagesIn, idOther, idSelf); - } catch (SQLException e) { - e.printStackTrace(); - } - - this.vue = null; - this.tcpClient.destroyAll(); - this.tcpClient = null; + this.vue.endSession(this.pseudoOther); } } \ No newline at end of file diff --git a/POO/src/session/VueSession.java b/POO/src/session/VueSession.java index 98b6e96..6f50bd2 100644 --- a/POO/src/session/VueSession.java +++ b/POO/src/session/VueSession.java @@ -39,9 +39,6 @@ import messages.Message.TypeMessage; public class VueSession extends JPanel { - /** - * - */ private static final long serialVersionUID = 1L; private JButton sendMessage; @@ -58,12 +55,7 @@ public class VueSession extends JPanel { this.setBorder(new EmptyBorder(5, 5, 5, 5)); this.setLayout(new BorderLayout(0, 0)); - this.chatInput = new JTextArea(); - this.chatInput.setColumns(10); - this.chatInput.setLineWrap(true); - this.chatInput.setWrapStyleWord(true); - this.chatInput.addKeyListener(this.c); - + // Create the display zone this.chatWindow = new JTextPane(); this.chatWindow.setEditable(false); this.chatWindow.setEditorKit(new WrapEditorKit()); @@ -71,21 +63,32 @@ public class VueSession extends JPanel { JScrollPane chatScroll = new JScrollPane(this.chatWindow); chatScroll.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED); + // Create the input zone + this.chatInput = new JTextArea(); + this.chatInput.setColumns(10); + this.chatInput.setLineWrap(true); + this.chatInput.setWrapStyleWord(true); + this.chatInput.addKeyListener(this.c); + JPanel bottom = new JPanel(); bottom.setLayout(new BorderLayout(0, 0)); - // remap "ENTER" to "none" to avoid "\n" in the input area after sending message + // Remap "ENTER" to "none" to avoid "\n" in the input area when pressing "ENTER" + // to send a message KeyStroke enter = KeyStroke.getKeyStroke("ENTER"); this.chatInput.getInputMap().put(enter, "none"); KeyStroke shiftEnter = KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, KeyEvent.SHIFT_DOWN_MASK); this.chatInput.getInputMap().put(shiftEnter, "insert-break"); - this.importFile = new JButton("Importer.."); - this.importFile.addActionListener(this.c); - + // Create a scroller to be able to send messages of several lines JScrollPane inputScroll = new JScrollPane(this.chatInput); inputScroll.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); + // Import file button + this.importFile = new JButton("Importer.."); + this.importFile.addActionListener(this.c); + + // Send message button this.sendMessage = new JButton("Envoyer"); this.sendMessage.addActionListener(this.c); @@ -93,6 +96,7 @@ public class VueSession extends JPanel { bottom.add(inputScroll, BorderLayout.CENTER); bottom.add(this.sendMessage, BorderLayout.EAST); + // Add the components to the view this.add(chatScroll, BorderLayout.CENTER); this.add(bottom, BorderLayout.SOUTH); @@ -102,32 +106,30 @@ public class VueSession extends JPanel { } + // -------------- GETTERS -------------- // + protected JButton getButtonEnvoyer() { return this.sendMessage; } + protected JButton getButtonImportFile() { return this.importFile; } + protected String getInputedText() { return this.chatInput.getText(); } - - protected void appendInputedText(String str) { - this.chatInput.append(str); - } - - - protected void resetZoneSaisie() { - this.chatInput.setText(""); - - } - - private void setCaretToEnd() { - this.chatWindow.setCaretPosition(this.chatWindow.getDocument().getLength()); - } + + // -------------- DISPLAY METHODS -------------- // + + /** + * Append the given string to the ChatWindow. + * + * @param str + */ protected void appendString(String str) { try { Document doc = this.chatWindow.getDocument(); @@ -137,12 +139,16 @@ public class VueSession extends JPanel { } } + + /** + * Append the given Message to the ChatWindow. + * + * @param message + */ protected void appendMessage(Message message) { try { StyledDocument sdoc = this.chatWindow.getStyledDocument(); - // sdoc.setParagraphAttributes(sdoc.getLength(), message.toString().length()-1, - // style, false); sdoc.insertString(sdoc.getLength(), message.toString(), null); } catch (BadLocationException e) { @@ -150,9 +156,17 @@ public class VueSession extends JPanel { } } + + /** + * Append an icon with the image contained in the given message. The message has + * to contain the base64 encoded bytes of the image as a String for it to be + * decoded and displayed. + * + * @param message + */ protected void appendImage(Message message) { this.setCaretToEnd(); - + String imgString = message.toString(); Icon ic; try { @@ -160,34 +174,52 @@ public class VueSession extends JPanel { ic = new ImageIcon(img); this.chatWindow.insertIcon(ic); this.appendString("\n"); - + } catch (IOException e) { e.printStackTrace(); } - - + } + private void printLineSeparator() { this.appendString("------------------------------------------\n"); } - protected void endSession(String pseudoOther) { - this.printLineSeparator(); - this.appendString(pseudoOther + " a mis fin à la session."); - this.chatInput.setEnabled(false); - this.chatInput.setFocusable(false); - this.sendMessage.setEnabled(false); - this.importFile.setEnabled(false); + /** + * Append the given string to the ChatInput. + * + * @param str + */ + protected void appendInputedText(String str) { + this.chatInput.append(str); } + + protected void resetZoneSaisie() { + this.chatInput.setText(""); + + } + + + // -------------- OTHERS -------------- // + + private void setCaretToEnd() { + this.chatWindow.setCaretPosition(this.chatWindow.getDocument().getLength()); + } + + + /** + * Retrieve all the previous messages from the controller and display them by + * appending them one by one to the ChatWindow. + */ private void displayHistorique() { ArrayList historique = this.c.getHistorique(); for (Message m : historique) { - if(m.getTypeMessage() == TypeMessage.IMAGE) { + if (m.getTypeMessage() == TypeMessage.IMAGE) { this.appendImage(m); - }else { + } else { this.appendMessage(m); } } @@ -197,6 +229,27 @@ public class VueSession extends JPanel { } } + + /** + * Disable the ChatInput, the buttons "Importer" and "Envoyer" and display a + * message indicating the other user ended the session. + * + * @param pseudoOther + */ + protected void endSession(String pseudoOther) { + this.printLineSeparator(); + this.appendString(pseudoOther + " a mis fin à la session."); + this.chatInput.setEnabled(false); + this.chatInput.setFocusable(false); + this.sendMessage.setEnabled(false); + this.importFile.setEnabled(false); + } + + + /** + * Method used when the user closes the session. Set all attributes' references + * to null, and call destroyAll() on the controller. + */ public void destroyAll() { if (this.c != null) { this.c.destroyAll(); @@ -207,12 +260,13 @@ public class VueSession extends JPanel { this.sendMessage = null; } + // ------------- PRIVATE CLASS TO WRAP TEXT -------------// class WrapEditorKit extends StyledEditorKit { - + private static final long serialVersionUID = 1L; - + ViewFactory defaultFactory = new WrapColumnFactory(); public ViewFactory getViewFactory() { diff --git a/POO/src/standard/ControleurStandard.java b/POO/src/standard/ControleurStandard.java index b0089d9..607068b 100644 --- a/POO/src/standard/ControleurStandard.java +++ b/POO/src/standard/ControleurStandard.java @@ -242,7 +242,7 @@ public class ControleurStandard implements ActionListener, ListSelectionListener return input.readLine(); } - // ------------OBSERVERS-------------// + // ------------OBSERVERS------------- // @Override public void updateInput(Object o, Object arg) {