diff --git a/.gitignore b/.gitignore index 87afada..4d66140 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .idea/ /target/ +/server.properties diff --git a/pom.xml b/pom.xml index b0e06c3..2a98067 100644 --- a/pom.xml +++ b/pom.xml @@ -43,6 +43,11 @@ 5.7.1 test + + org.mariadb.jdbc + mariadb-java-client + 2.7.1 + diff --git a/src/fr/univ/lyon1/client/Client.java b/src/fr/univ/lyon1/client/Client.java index 19718cc..b5f33cb 100644 --- a/src/fr/univ/lyon1/client/Client.java +++ b/src/fr/univ/lyon1/client/Client.java @@ -15,11 +15,31 @@ public class Client { private ObjectInputStream in; protected boolean started = false; - public Client(String address, int port) throws IOException, InterruptedException { + public Client(String address, int port, String uuid, String password) throws IOException, InterruptedException, Exception { this.address = address; this.port = port; socket = new Socket(address, port); out = new ObjectOutputStream(socket.getOutputStream()); + while (!this.auth(uuid, password)); + } + + public boolean auth(String uuid, String password) throws IOException { + getIn(); + + out.writeUTF(uuid); + out.flush(); + out.writeUTF(password); + out.flush(); + + String response = in.readUTF(); + System.out.println(response); + + if (response.startsWith("err:")) + return false; + else if (response.equals("logged")) + return true; + else + throw new IOException("Uk message"); } public void disconnectedServer() throws IOException { @@ -66,4 +86,10 @@ public class Client { socket.close(); clientReceiveThread.interrupt(); } + + public ObjectInputStream getIn() throws IOException { + if (in == null) + in = new ObjectInputStream(socket.getInputStream()); + return in; + } } diff --git a/src/fr/univ/lyon1/client/ClientReceive.java b/src/fr/univ/lyon1/client/ClientReceive.java index ddccb12..4a906b2 100644 --- a/src/fr/univ/lyon1/client/ClientReceive.java +++ b/src/fr/univ/lyon1/client/ClientReceive.java @@ -20,7 +20,7 @@ public class ClientReceive implements Runnable { @Override public void run() { try { - in = new ObjectInputStream(socket.getInputStream()); + in = client.getIn(); } catch (IOException e) { e.printStackTrace(); return; diff --git a/src/fr/univ/lyon1/client/MainClient.java b/src/fr/univ/lyon1/client/MainClient.java index 82c088b..afc4565 100644 --- a/src/fr/univ/lyon1/client/MainClient.java +++ b/src/fr/univ/lyon1/client/MainClient.java @@ -1,20 +1,21 @@ package fr.univ.lyon1.client; -import java.io.IOException; - public class MainClient { public static void main(String[] args) { try { - if (args.length != 2) { + if (args.length != 4) { printUsage(); } else { String address = args[0]; int port = Integer.parseInt(args[1]); - Client c = new Client(address, port); + String uuid = args[2]; + String password = args[3]; + Client c = new Client(address, port, uuid, password); c.run(); } - } catch (IOException|InterruptedException e) { + } catch (Exception e) { e.printStackTrace(); + System.exit(1); } } @@ -22,5 +23,7 @@ public class MainClient { System.out.println("java client.Client
"); System.out.println("\t
: server's ip address"); System.out.println("\t: server's port"); + System.out.println("\t: user's UUID"); + System.out.println("\t: user's password"); } } diff --git a/src/fr/univ/lyon1/common/Message.java b/src/fr/univ/lyon1/common/Message.java index 15a8638..c8e5b8f 100644 --- a/src/fr/univ/lyon1/common/Message.java +++ b/src/fr/univ/lyon1/common/Message.java @@ -1,22 +1,31 @@ package fr.univ.lyon1.common; import java.io.Serializable; +import java.util.UUID; public class Message implements Serializable { - private String sender; + private User sender; private final String content; + private final UUID uuid; - public Message(String sender, String content) { + public Message(User sender, String content) { + this.uuid = UUID.randomUUID(); this.sender = sender; this.content = content; } - public void setSender(String sender) { + public Message(UUID uuid, User sender, String content) { + this.uuid = uuid; + this.sender = sender; + this.content = content; + } + + public void setSender(User sender) { this.sender = sender; } - public String getSender() { + public User getSender() { return sender; } diff --git a/src/fr/univ/lyon1/common/User.java b/src/fr/univ/lyon1/common/User.java new file mode 100644 index 0000000..5a14351 --- /dev/null +++ b/src/fr/univ/lyon1/common/User.java @@ -0,0 +1,41 @@ +package fr.univ.lyon1.common; + +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.PBEKeySpec; +import java.io.Serializable; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.KeySpec; +import java.util.Arrays; +import java.util.Base64; +import java.util.UUID; +import java.util.regex.Matcher; + +public class User implements Serializable { + private final UUID uuid; + private String username; + + public User(UUID uuid, String username) { + this.uuid = uuid; + this.username = username; + } + + public User(String username) { + this.uuid = UUID.randomUUID(); + this.username = username; + } + + public UUID getUUID() { + return uuid; + } + + public String getUsername() { + return username; + } + + @Override + public String toString() { + return username; + } +} diff --git a/src/fr/univ/lyon1/common/channel/Channel.java b/src/fr/univ/lyon1/common/channel/Channel.java new file mode 100644 index 0000000..fce3126 --- /dev/null +++ b/src/fr/univ/lyon1/common/channel/Channel.java @@ -0,0 +1,13 @@ +package fr.univ.lyon1.common.channel; + +import java.util.UUID; + +public abstract class Channel { + private final UUID uuid; + private String name; + + public Channel(UUID uuid, String name) { + this.uuid = uuid; + this.name = name; + } +} diff --git a/src/fr/univ/lyon1/common/channel/PrivateChannel.java b/src/fr/univ/lyon1/common/channel/PrivateChannel.java new file mode 100644 index 0000000..92ecbd8 --- /dev/null +++ b/src/fr/univ/lyon1/common/channel/PrivateChannel.java @@ -0,0 +1,10 @@ +package fr.univ.lyon1.common.channel; + +import java.util.UUID; + +public class PrivateChannel extends Channel { + + public PrivateChannel(UUID uuid, String name) { + super(uuid, name); + } +} diff --git a/src/fr/univ/lyon1/common/channel/PublicChannel.java b/src/fr/univ/lyon1/common/channel/PublicChannel.java new file mode 100644 index 0000000..068dec2 --- /dev/null +++ b/src/fr/univ/lyon1/common/channel/PublicChannel.java @@ -0,0 +1,9 @@ +package fr.univ.lyon1.common.channel; + +import java.util.UUID; + +public class PublicChannel extends Channel { + public PublicChannel(UUID uuid, String name) { + super(uuid, name); + } +} diff --git a/src/fr/univ/lyon1/gui/ClientGUI.java b/src/fr/univ/lyon1/gui/ClientGUI.java index 634aefc..cb19ab2 100644 --- a/src/fr/univ/lyon1/gui/ClientGUI.java +++ b/src/fr/univ/lyon1/gui/ClientGUI.java @@ -9,8 +9,8 @@ import java.io.IOException; public class ClientGUI extends Client { private final MainGui gui; - public ClientGUI(MainGui gui, String address, int port) throws IOException, InterruptedException { - super(address, port); + public ClientGUI(MainGui gui, String address, int port) throws IOException, InterruptedException, Exception { + super(address, port, null, null); this.gui = gui; } diff --git a/src/fr/univ/lyon1/server/ConnectedClient.java b/src/fr/univ/lyon1/server/ConnectedClient.java index 39d8d0b..8f3de7c 100644 --- a/src/fr/univ/lyon1/server/ConnectedClient.java +++ b/src/fr/univ/lyon1/server/ConnectedClient.java @@ -1,6 +1,8 @@ package fr.univ.lyon1.server; import fr.univ.lyon1.common.Message; +import fr.univ.lyon1.common.User; +import fr.univ.lyon1.server.models.UserModel; import java.io.EOFException; import java.io.IOException; @@ -15,11 +17,47 @@ public class ConnectedClient implements Runnable { private final Socket socket; private final ObjectOutputStream out; private ObjectInputStream in; + private User user; ConnectedClient(Server server, Socket socket) throws IOException { this.server = server; this.socket = socket; this.out = new ObjectOutputStream(socket.getOutputStream()); + + System.out.println("New user try to auth"); + while (!this.auth()); + } + + private boolean auth() throws IOException { + if (in == null) + in = new ObjectInputStream(socket.getInputStream()); + + String username = in.readUTF(); + System.out.println("username: "+username); + String password = in.readUTF(); + System.out.println("Pass: "+password); + + if (username.isEmpty() || password.isEmpty()) { + out.writeUTF("err: Login required"); + out.flush(); + return false; + } + + UserModel user = UserModel.get(username); + + if (user == null) + out.writeUTF("err: Username not found !"); + else if (!user.checkPassword(password)) + out.writeUTF("err: Password invalid !"); + else { + out.writeUTF("logged"); + out.flush(); + this.user = user; + return true; + } + out.flush(); + + return false; } public Message sendMessage(Message message) throws IOException { @@ -30,15 +68,13 @@ public class ConnectedClient implements Runnable { public void run() { try { - in = new ObjectInputStream(socket.getInputStream()); - while (true) { Message msg = (Message) in.readObject(); if (msg == null) break; - msg.setSender(String.valueOf(id)); + msg.setSender(this.user); server.broadcastMessage(msg, id); } } catch (IOException | ClassNotFoundException e) { diff --git a/src/fr/univ/lyon1/server/Database.java b/src/fr/univ/lyon1/server/Database.java new file mode 100644 index 0000000..b3651ab --- /dev/null +++ b/src/fr/univ/lyon1/server/Database.java @@ -0,0 +1,62 @@ +package fr.univ.lyon1.server; + +import java.io.*; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.util.Properties; + +public class Database { + private static Database database; + private Connection connection; + + private Database() { + Database.database = this; + try { + this.connection = getConnexion(); + } catch (IOException | SQLException err) { + err.printStackTrace(); + System.exit(1); + } + } + + private String[] getCredentials() throws NullPointerException, IOException { + Properties props = new Properties(); + File f = new File("server.properties"); + + if (!f.exists()) { + props.setProperty("db.url", "jdbc:mariadb://localhost:3306/chat"); + props.setProperty("db.user", "chat"); + props.setProperty("db.password", "password"); + + props.store(new FileWriter(f), ""); + } else { + props.load(new FileReader(f)); + } + + return new String[]{props.getProperty("db.url"), props.getProperty("db.user"), props.getProperty("db.password")}; + } + + private Connection getConnexion() throws SQLException, IOException { + String[] credentials = getCredentials(); + + try { + Class.forName("org.mariadb.jdbc.Driver"); + } catch (ClassNotFoundException err) { + System.err.println("MariaDB driver not found !"); + System.exit(1); + } + + return DriverManager.getConnection(credentials[0], credentials[1], credentials[2]); + } + + public Connection getConnection() { + return connection; + } + + public static Database getDatabase() { + if (Database.database == null) + return new Database(); + return Database.database; + } +} diff --git a/src/fr/univ/lyon1/server/Server.java b/src/fr/univ/lyon1/server/Server.java index ca711d6..3e7d64f 100644 --- a/src/fr/univ/lyon1/server/Server.java +++ b/src/fr/univ/lyon1/server/Server.java @@ -1,23 +1,27 @@ package fr.univ.lyon1.server; import fr.univ.lyon1.common.Message; +import fr.univ.lyon1.common.User; import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.UUID; public class Server { private final int port; private List clients = new ArrayList<>(); + private static User serverUser = new User(UUID.fromString("3539b6bf-5eb3-41d4-893f-cbf0caa9ca74"), "server"); Server(int port) throws IOException { this.port = port; + Database.getDatabase(); Thread connection = new Thread(new Connection(this)); connection.start(); } public ConnectedClient addClient(ConnectedClient newClient) { - Message msg = new Message( "Server", newClient.getId() + " is connected !"); + Message msg = new Message( serverUser, newClient.getId() + " is connected !"); clients.add(newClient); @@ -51,7 +55,7 @@ public class Server { clients.remove(client); - Message msg = new Message("Server", "Client "+client.getId()+" is disconnected"); + Message msg = new Message(serverUser, "Client "+client.getId()+" is disconnected"); broadcastMessage(msg, -1); diff --git a/src/fr/univ/lyon1/server/models/Model.java b/src/fr/univ/lyon1/server/models/Model.java new file mode 100644 index 0000000..aca3034 --- /dev/null +++ b/src/fr/univ/lyon1/server/models/Model.java @@ -0,0 +1,10 @@ +package fr.univ.lyon1.server.models; + +import fr.univ.lyon1.server.Database; + + +public interface Model { + Database database = Database.getDatabase(); + + +} diff --git a/src/fr/univ/lyon1/server/models/UserModel.java b/src/fr/univ/lyon1/server/models/UserModel.java new file mode 100644 index 0000000..95280db --- /dev/null +++ b/src/fr/univ/lyon1/server/models/UserModel.java @@ -0,0 +1,176 @@ +package fr.univ.lyon1.server.models; + +import fr.univ.lyon1.common.User; + +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.PBEKeySpec; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.KeySpec; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Arrays; +import java.util.Base64; +import java.util.UUID; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class UserModel extends User implements Model { + private String passwordHash; + + private final SecureRandom random = new SecureRandom(); + public static final String ID = "$1$"; + private static final int SIZE = 128; + private static final int COST = 16; + private static final String ALGORITHM = "PBKDF2WithHmacSHA1"; + private static final Pattern LAYOUT = Pattern.compile("\\$1\\$(\\d\\d?)\\$(.{43})"); + private static final String TABLE_NAME = "User"; + + public UserModel(String username, String password) { + super(username); + setPassword(password); + create(); + } + + private UserModel(UUID uuid, String username, String passwordHash) { + super(uuid, username); + this.passwordHash = passwordHash; + } + + public static UserModel get(String username) { + try { + PreparedStatement ps = database.getConnection().prepareStatement("SELECT UUID FROM "+TABLE_NAME+" WHERE username = ?"); + ps.setString(1, username); + if (ps.execute()) { + ResultSet rs = ps.getResultSet(); + if (rs.next()) + return get(UUID.fromString(rs.getString("UUID"))); + } + } catch (SQLException err) { + err.printStackTrace(); + return null; + } + return null; + } + + public static UserModel get(UUID uuid) { + try { + PreparedStatement ps = database.getConnection().prepareStatement("SELECT * FROM "+TABLE_NAME+" WHERE UUID = ?"); + ps.setString(1, uuid.toString()); + if (ps.execute()) { + ResultSet rs = ps.getResultSet(); + if (rs.next()) + return new UserModel( + UUID.fromString(rs.getString("UUID")), + rs.getString("USERNAME"), + rs.getString("PASSWORD") + ); + } + } catch (SQLException err) { + err.printStackTrace(); + return null; + } + return null; + } + + private boolean exist() { + try { + PreparedStatement ps = database.getConnection().prepareStatement("SELECT UUID FROM "+TABLE_NAME+" WHERE UUID = ?"); + ps.setString(1, super.getUUID().toString()); + ps.execute(); + return ps.getResultSet().next(); + } catch (SQLException err) { + err.printStackTrace(); + return false; + } + } + + private boolean create() { + try { + PreparedStatement ps = database.getConnection().prepareStatement("INSERT INTO "+TABLE_NAME+" (UUID, username, password) VALUES (?, ?, ?)"); + ps.setString(1, super.getUUID().toString()); + ps.setString(2, super.getUsername()); + ps.setString(3, getPasswordHash()); + return ps.executeUpdate() > 0; + } catch (SQLException err) { + err.printStackTrace(); + return false; + } + } + + public boolean save() { + if (!exist()) + return create(); + + try { + PreparedStatement ps = database.getConnection().prepareStatement("UPDATE "+TABLE_NAME+" SET username = ?, password = ? WHERE UUID = ?"); + ps.setString(1, super.getUsername()); + ps.setString(2, getPasswordHash()); + ps.setString(3, super.getUUID().toString()); + return ps.executeUpdate() > 0; + } catch (SQLException err) { + err.printStackTrace(); + return false; + } + } + + public static void generateTable() { + try { + PreparedStatement ps = database.getConnection().prepareStatement("CREATE TABLE "+TABLE_NAME+" ( UUID varchar(40) primary key, username varchar(16) unique, password varchar(256) )"); + ps.executeUpdate(); + } catch (SQLException err) { + err.printStackTrace(); + } + } + + public String getPasswordHash() { + return passwordHash; + } + + public void setPassword(String password) { + byte[] passwordSalt = new byte[SIZE / 8]; + random.nextBytes(passwordSalt); + byte[] dk = pbkdf2(password.toCharArray(), passwordSalt, 1 << COST); + byte[] hash = new byte[passwordSalt.length + dk.length]; + System.arraycopy(passwordSalt, 0, hash, 0, passwordSalt.length); + System.arraycopy(dk, 0, hash, passwordSalt.length, dk.length); + Base64.Encoder enc = Base64.getUrlEncoder().withoutPadding(); + passwordHash = ID + COST + '$' + enc.encodeToString(hash); + } + + public boolean checkPassword(String password) { + Matcher m = LAYOUT.matcher(passwordHash); + if (!m.matches()) + throw new IllegalArgumentException("Invalid token format"); + int iterations = iterations(Integer.parseInt(m.group(1))); + byte[] hash = Base64.getUrlDecoder().decode(m.group(2)); + byte[] salt = Arrays.copyOfRange(hash, 0, SIZE / 8); + byte[] check = pbkdf2(password.toCharArray(), salt, iterations); + int zero = 0; + for (int idx = 0; idx < check.length; ++idx) + zero |= hash[salt.length + idx] ^ check[idx]; + return zero == 0; + } + + private static int iterations(int cost) { + if ((cost < 0) || (cost > 30)) + throw new IllegalArgumentException("cost: " + cost); + return 1 << cost; + } + + private static byte[] pbkdf2(char[] password, byte[] salt, int iterations) { + KeySpec spec = new PBEKeySpec(password, salt, iterations, SIZE); + try { + SecretKeyFactory f = SecretKeyFactory.getInstance(ALGORITHM); + return f.generateSecret(spec).getEncoded(); + } + catch (NoSuchAlgorithmException ex) { + throw new IllegalStateException("Missing algorithm: " + ALGORITHM, ex); + } + catch (InvalidKeySpecException ex) { + throw new IllegalStateException("Invalid SecretKeyFactory", ex); + } + } +} diff --git a/src/module-info.java b/src/module-info.java index 39f3612..b6f9ef6 100644 --- a/src/module-info.java +++ b/src/module-info.java @@ -3,6 +3,9 @@ module fr.univ.lyon1.gui { requires javafx.fxml; requires org.kordamp.bootstrapfx.core; + requires java.sql; + + requires org.mariadb.jdbc; opens fr.univ.lyon1.gui to javafx.fxml; exports fr.univ.lyon1.gui;