Skip to content

Commit

Permalink
Implement SNVS authentication
Browse files Browse the repository at this point in the history
  • Loading branch information
logandhillon committed Sep 26, 2024
1 parent aebfb51 commit 4834897
Show file tree
Hide file tree
Showing 12 changed files with 125 additions and 64 deletions.
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ plugins {
}

group 'net.logandhillon'
version '0.9.5-alpha'
version '0.9.6-alpha'

repositories {
mavenCentral()
Expand Down
19 changes: 10 additions & 9 deletions src/main/java/net/logandhillon/icx/client/ICXClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import net.logandhillon.icx.common.ICXMultimediaPayload;
import net.logandhillon.icx.common.ICXPacket;
import net.logandhillon.icx.common.SNVS;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.core.LoggerContext;

Expand All @@ -19,7 +20,7 @@

public class ICXClient {
private static final Logger LOG = LoggerContext.getContext().getLogger(ICXClient.class);
private static String screenName;
private static SNVS.Token snvs;
private static InetSocketAddress serverAddr;
private static SSLSocket socket;
private static PrintWriter writer;
Expand All @@ -42,11 +43,11 @@ public void checkServerTrusted(X509Certificate[] certs, String authType) {
}
};

public static void connect(String _screenName, InetAddress _serverAddr) throws IOException {
screenName = _screenName;
serverAddr = new InetSocketAddress(_serverAddr, 195);
public static void connect(String screenName, InetAddress serverAddr) throws IOException {
snvs = new SNVS.Token(screenName, SNVS.genToken());
ICXClient.serverAddr = new InetSocketAddress(serverAddr, 195);

LOG.info("Connecting to {} as {}", serverAddr, screenName);
LOG.info("Connecting to {} as {}", ICXClient.serverAddr, snvs);

try {
SSLContext context = SSLContext.getInstance("TLS");
Expand All @@ -55,7 +56,7 @@ public static void connect(String _screenName, InetAddress _serverAddr) throws I
} catch (KeyManagementException | NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
socket.connect(serverAddr, 5000);
socket.connect(ICXClient.serverAddr, 5000);

writer = new PrintWriter(socket.getOutputStream(), true);
reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
Expand All @@ -75,7 +76,7 @@ public static void disconnect() throws IOException {

public static void send(ICXPacket.Command command, String content) {
LOG.debug("Sending {} packet", command);
writer.println(new ICXPacket(command, screenName, content).encode());
writer.println(new ICXPacket(command, snvs, content).encode());
}

public static void uploadFile(File file) {
Expand All @@ -87,8 +88,8 @@ public static void uploadFile(File file) {
}
}

public static String getScreenName() {
return screenName;
public static SNVS.Token getSnvs() {
return snvs;
}

public static InetSocketAddress getServerAddr() {
Expand Down
33 changes: 19 additions & 14 deletions src/main/java/net/logandhillon/icx/client/S2CHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,32 +43,37 @@ public void run() {
if (!ICXClient.isConnected()) break;
try {
ChatView.updateRoomName();

String msg;
if ((msg = reader.readLine()) != null) {
ICXPacket packet = ICXPacket.decode(msg);
LOG.debug("Incoming {} packet from {}", packet.command(), packet.sender());
LOG.debug("Incoming {} packet from {}", packet.command(), packet.snvs().name());

switch (packet.command()) {
case SRV_HELLO -> {
ICXClient.connectedRoomName = packet.content();
ChatView.updateRoomName();
LOG.info("Server room name is {}", packet.content());
}
case SRV_KICK -> Platform.runLater(() -> UI.reloadScene(new Scene(new LoginView()), () -> {
Alert alert = new Alert(Alert.AlertType.WARNING);
alert.setTitle("Kicked from server");
alert.setHeaderText("You have been kicked from " + ICXClient.getServerAddr());
alert.setContentText("Reason: " + packet.content());
alert.showAndWait();
}));
case SEND -> Platform.runLater(() -> ChatView.postMessage(packet.sender(), packet.content()));
case SRV_KICK -> {
Platform.runLater(() -> UI.reloadScene(new Scene(new LoginView()), () -> {
Alert alert = new Alert(Alert.AlertType.WARNING);
alert.setTitle("Kicked from server");
alert.setHeaderText("You have been kicked from " + ICXClient.getServerAddr());
alert.setContentText("Reason: " + packet.content());
alert.showAndWait();
}));
ICXClient.disconnect();
throw new SocketException("Socket closed");
}
case SEND ->
Platform.runLater(() -> ChatView.postMessage(packet.snvs().name(), packet.content()));
case UPLOAD ->
Platform.runLater(() -> ChatView.postMMP(packet.sender(), ICXMultimediaPayload.decode(packet.content())));
Platform.runLater(() -> ChatView.postMMP(packet.snvs().name(), ICXMultimediaPayload.decode(packet.content())));
case JOIN ->
Platform.runLater(() -> ChatView.postAlert(String.format("Welcome, %s!", packet.sender())));
Platform.runLater(() -> ChatView.postAlert(String.format("Welcome, %s!", packet.snvs().name())));
case EXIT ->
Platform.runLater(() -> ChatView.postAlert(String.format("Farewell, %s!", packet.sender())));
Platform.runLater(() -> ChatView.postAlert(String.format("Farewell, %s!", packet.snvs().name())));
}
}
} catch (SSLException e) {
Expand All @@ -79,7 +84,7 @@ public void run() {
showError("An error disrupted your connection: " + e.getMessage(), "Connection Error");
return;
} catch (Exception e) {
showError("Failed to parse incoming packet: " + e.getMessage(), "Unknown Error");
showError("Failed to parse incoming packet: " + e.getMessage(), e.getClass().getName());
return;
}
}
Expand Down
12 changes: 8 additions & 4 deletions src/main/java/net/logandhillon/icx/common/ICXPacket.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import java.nio.charset.StandardCharsets;
import java.util.Base64;

public record ICXPacket(ICXPacket.Command command, String sender, String content) {
public record ICXPacket(ICXPacket.Command command, SNVS.Token snvs, String content) {
private static final String VER = "ICX/0.9";

public enum Command {
Expand All @@ -18,7 +18,7 @@ public ParsingException(String reason) {
}

public String encode() {
return Base64.getEncoder().encodeToString(String.format("ICX/0.9 %s$%s$%s", command, sender, content).getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(String.format("ICX/0.9 %s$%s$%s", command, snvs, content).getBytes(StandardCharsets.UTF_8));
}

public static ICXPacket decode(String b64) throws IllegalArgumentException {
Expand All @@ -28,10 +28,14 @@ public static ICXPacket decode(String b64) throws IllegalArgumentException {
String[] brand = parts[0].split(" ");

if (parts.length < 3 || brand.length < 2) throw new ParsingException("Bad packet structure");
if (parts[1].isEmpty()) throw new ParsingException("No sender");
if (parts[1].isEmpty()) throw new ParsingException("No snvs");
if (!brand[0].equals(VER))
throw new ParsingException(String.format("Version mismatch (got %s, expected %s)", brand[0], VER));

return new ICXPacket(Command.valueOf(brand[1]), parts[1], parts[2]);
return new ICXPacket(Command.valueOf(brand[1]), SNVS.Token.fromString(parts[1]), parts[2]);
}

public ICXPacket stripToken() {
return new ICXPacket(command, snvs.withoutToken(), content);
}
}
51 changes: 51 additions & 0 deletions src/main/java/net/logandhillon/icx/common/SNVS.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package net.logandhillon.icx.common;

import java.net.InetAddress;
import java.security.SecureRandom;
import java.util.Base64;

public class SNVS {
private static final SecureRandom RANDOM = new SecureRandom();
public static final String SEPARATOR = "\u001f";

/**
* @return 256-bit (32 char) b64 token
*/
public static String genToken() {
byte[] key = new byte[24]; // 24 chars = 32 char b64
RANDOM.nextBytes(key);

return Base64.getUrlEncoder().withoutPadding().encodeToString(key);
}

public record InetToken(InetAddress registrant, String token) {
}

public record Token(String name, String token) {
@Override
public String toString() {
return name + SEPARATOR + token;
}

public static boolean validate(String snvs) {
return snvs != null && !snvs.isBlank() && !snvs.contains(SEPARATOR) && !snvs.contains("$");
}

public boolean validate() {
return SNVS.Token.validate(this.name);
}

public static Token fromString(String payload) throws IllegalArgumentException {
String[] snvs = payload.split(SEPARATOR, 2);
if (snvs[1].contains(SEPARATOR) || snvs[1].contains("$"))
throw new IllegalArgumentException("Invalid screen name");
if (snvs[0] == null || snvs[0].isEmpty() || snvs[1].isEmpty())
throw new IllegalArgumentException("Malformed SNVS");
return new Token(snvs[0], snvs[1]);
}

public Token withoutToken() {
return new Token(name, null);
}
}
}
29 changes: 15 additions & 14 deletions src/main/java/net/logandhillon/icx/server/C2SHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import net.logandhillon.icx.common.ICXMultimediaPayload;
import net.logandhillon.icx.common.ICXPacket;
import net.logandhillon.icx.common.SNVS;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.core.LoggerContext;

Expand All @@ -15,7 +16,7 @@ public class C2SHandler extends Thread {
private final Socket socket;
private final InetAddress addr;
private PrintWriter writer;
private String sender;
private SNVS.Token snvs;
private boolean isFresh = true;

public C2SHandler(Socket socket) {
Expand All @@ -39,7 +40,7 @@ public void run() {
OutputStream output = socket.getOutputStream();
writer = new PrintWriter(output, true);

sendPacket(writer, new ICXPacket(ICXPacket.Command.SRV_HELLO, "SERVER", ICXServer.PROPERTIES.roomName()));
sendPacket(writer, new ICXPacket(ICXPacket.Command.SRV_HELLO, NameRegistry.SERVER, ICXServer.PROPERTIES.roomName()));

String msg;
while ((msg = reader.readLine()) != null) {
Expand All @@ -52,25 +53,25 @@ public void run() {
try {
if (packet.command() != ICXPacket.Command.JOIN)
throw new RuntimeException("You are not registered!");
ICXServer.NAME_REGISTRY.registerName(packet.sender(), addr);
ICXServer.NAME_REGISTRY.registerName(packet.snvs(), addr);
ICXServer.CLIENT_WRITERS.add(writer);
isFresh = false;
this.sender = packet.sender();
} catch (RuntimeException ex) {
sendPacket(writer, new ICXPacket(ICXPacket.Command.SRV_KICK, "SERVER", ex.getMessage()));
this.snvs = packet.snvs();
} catch (Exception ex) {
sendPacket(writer, new ICXPacket(ICXPacket.Command.SRV_KICK, NameRegistry.SERVER, ex.getMessage()));
socket.close();
}
}

// throw error if name cannot be verified to that IP
if (!this.sender.equals(packet.sender()) || !ICXServer.NAME_REGISTRY.verifyName(packet.sender(), addr))
throw new RuntimeException("Failed to verify name registration");
if (!this.snvs.equals(packet.snvs()) || !ICXServer.NAME_REGISTRY.verifyName(packet.snvs(), addr))
throw new RuntimeException("SNVS failed");

switch (packet.command()) {
case SEND -> {
if (packet.content().isBlank())
throw new RuntimeException("Message content cannot be blank");
LOG.info("{}: '{}'", packet.sender(), packet.content());
LOG.info("{}: '{}'", packet.snvs().name(), packet.content());
}
case UPLOAD ->
ICXMultimediaPayload.parseOrThrow(packet.content()); // verify packet integrity or throw
Expand All @@ -81,9 +82,9 @@ public void run() {
case SRV_ERR -> throw new RuntimeException("Illegal command");
}

ICXServer.broadcast(packet);
} catch (RuntimeException e) {
sendPacket(writer, new ICXPacket(ICXPacket.Command.SRV_ERR, "SERVER", e.getMessage()));
ICXServer.broadcast(packet.stripToken());
} catch (Exception e) {
sendPacket(writer, new ICXPacket(ICXPacket.Command.SRV_ERR, NameRegistry.SERVER, e.getMessage()));
}
}
} catch (SocketException e) {
Expand All @@ -92,9 +93,9 @@ public void run() {
LOG.error("Error handling client: {}", e.getMessage());
} finally {
try {
ICXServer.NAME_REGISTRY.releaseName(this.sender);
ICXServer.NAME_REGISTRY.releaseName(this.snvs);
ICXServer.CLIENT_WRITERS.remove(this.writer);
ICXServer.broadcast(new ICXPacket(ICXPacket.Command.EXIT, this.sender, null));
ICXServer.broadcast(new ICXPacket(ICXPacket.Command.EXIT, this.snvs, null));
socket.close();
LOG.info("Disconnected");
} catch (IOException e) {
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/net/logandhillon/icx/server/ICXServer.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public static void broadcast(ICXPacket packet) {
}

public static void start() {
NAME_REGISTRY.registerName("SERVER", InetAddress.getLoopbackAddress());
NAME_REGISTRY.registerName(NameRegistry.SERVER, InetAddress.getLoopbackAddress());
int port = 195; // ooh, fun fact port 194 is IRC, so port 195 is a homage to that

System.setProperty("javax.net.ssl.keyStore", ICXServer.PROPERTIES.keystoreFile());
Expand Down
26 changes: 15 additions & 11 deletions src/main/java/net/logandhillon/icx/server/NameRegistry.java
Original file line number Diff line number Diff line change
@@ -1,28 +1,32 @@
package net.logandhillon.icx.server;

import net.logandhillon.icx.common.SNVS;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.core.LoggerContext;

import java.net.InetAddress;
import java.util.HashMap;
import java.util.Objects;

public class NameRegistry {
private static final Logger LOG = LoggerContext.getContext().getLogger(NameRegistry.class);
private static final HashMap<String, InetAddress> REGISTRY = new HashMap<>();
private static final HashMap<String, SNVS.InetToken> REGISTRY = new HashMap<>();
public static final SNVS.Token SERVER = new SNVS.Token("SERVER", SNVS.genToken());

public void registerName(String name, InetAddress registrant) {
if (REGISTRY.containsKey(name)) throw new RuntimeException("Name taken");
LOG.info("Registering '{}' to {}", name, registrant);
REGISTRY.put(name, registrant);
public void registerName(SNVS.Token snvs, InetAddress registrant) {
if (REGISTRY.containsKey(snvs.name())) throw new RuntimeException("Name taken");
if (!snvs.validate()) throw new RuntimeException("Malformed or invalid SNVS");
LOG.info("Registering '{}' to {}", snvs.name(), registrant);
REGISTRY.put(snvs.name(), new SNVS.InetToken(registrant, snvs.token()));
}

public void releaseName(String name) {
if (name == null) return;
LOG.info("Releasing '{}'", name);
REGISTRY.remove(name);
public void releaseName(SNVS.Token snvs) {
if (snvs == null || snvs.name() == null) return;
LOG.info("Releasing '{}'", snvs.name());
REGISTRY.remove(snvs.name());
}

public boolean verifyName(String name, InetAddress addr) {
return REGISTRY.containsKey(name) && REGISTRY.get(name) == addr;
public boolean verifyName(SNVS.Token snvs, InetAddress origin) {
return REGISTRY.containsKey(snvs.name()) && REGISTRY.get(snvs.name()).registrant() == origin && Objects.equals(REGISTRY.get(snvs.name()).token(), snvs.token());
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package net.logandhillon.icx.server;

import net.logandhillon.icx.common.SNVS;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
Expand Down Expand Up @@ -28,13 +29,7 @@ public static void launch() {

// generate keystore password
System.out.print("Generating secure key... ");
SecureRandom random = new SecureRandom();
char[] password = new char[32];

for (int i = 0; i < 32; i++) {
int randomAscii = 32 + random.nextInt(126 - 32 + 1);
password[i] = (char) randomAscii;
}
char[] password = SNVS.genToken().toCharArray(); // just use the already existing SNVS token generation system
System.out.println("[OK]");

try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public MessageComponent(String sender, String message) {

getChildren().add(new Label(message));

if (sender.equals(ICXClient.getScreenName())) setAlignment(Pos.CENTER_RIGHT);
if (sender.equals(ICXClient.getSnvs().name())) setAlignment(Pos.CENTER_RIGHT);
else setAlignment(Pos.CENTER_LEFT);

lastSender = sender;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public MultimediaComponent(String sender, ICXMultimediaPayload mmp) {
getChildren().addAll(header);
}

Pos alignment = sender.equals(ICXClient.getScreenName()) ? Pos.CENTER_RIGHT : Pos.CENTER_LEFT;
Pos alignment = sender.equals(ICXClient.getSnvs().name()) ? Pos.CENTER_RIGHT : Pos.CENTER_LEFT;

getChildren().add(switch (mmp.fileType()) {
case IMAGE -> new ImagePreviewComponent(mmp.content());
Expand Down
Loading

0 comments on commit 4834897

Please sign in to comment.