From b14e53946421f723f45ddc24b8ee4d19143e671b Mon Sep 17 00:00:00 2001 From: Sen Date: Fri, 30 May 2025 01:43:37 +0200 Subject: [PATCH] add key serialization --- .../src/main/java/client/gui/GuiServer.java | 8 +- .../client/network/ClientLoginHandler.java | 2 +- .../main/java/common/util/EncryptUtil.java | 76 +++++++++++++++++-- common/src/main/java/common/util/Util.java | 40 ++++++++++ server/src/main/java/server/Server.java | 4 +- 5 files changed, 117 insertions(+), 13 deletions(-) diff --git a/client/src/main/java/client/gui/GuiServer.java b/client/src/main/java/client/gui/GuiServer.java index 06d83e2..7adbaf6 100644 --- a/client/src/main/java/client/gui/GuiServer.java +++ b/client/src/main/java/client/gui/GuiServer.java @@ -2,8 +2,6 @@ package client.gui; import java.security.KeyPair; import java.security.PublicKey; -import java.util.Base64; - import client.gui.GuiConnect.ServerInfo; import client.gui.element.ActButton; import client.gui.element.ButtonCallback; @@ -97,7 +95,7 @@ public class GuiServer extends Gui implements FieldCallback { this.keyButton = this.add(new ActButton(0, 120, 391, 24, new ButtonCallback() { public void use(ActButton elem, PressType action) { if(GuiServer.this.keypair == null) { - GuiServer.this.keypair = EncryptUtil.generateKeyPair(); + GuiServer.this.keypair = EncryptUtil.createKeypair(); GuiServer.this.keyDigest = EncryptUtil.getXorSha512Hash(GuiServer.this.keypair.getPublic().getEncoded()); GuiServer.this.keyLabel.setText("Anmelde-Pubkey: RSA-2048 " + GuiServer.this.keyDigest); GuiServer.this.keyButton.setText("Schlüsselpaar entfernen"); @@ -152,7 +150,7 @@ public class GuiServer extends Gui implements FieldCallback { this.copyKeyButton = this.add(new ActButton(395, 120, 85, 24, new ButtonCallback() { public void use(ActButton elem, PressType action) { if(GuiServer.this.keypair != null) - Window.setClipboard(Base64.getEncoder().encodeToString(GuiServer.this.keypair.getPublic().getEncoded())); + Window.setClipboard(EncryptUtil.getArmoredPubkey(GuiServer.this.keypair.getPublic(), GuiServer.this.userBox.getText())); } }, "Kopieren")); this.copyKeyButton.enabled = this.keypair != null; @@ -176,7 +174,7 @@ public class GuiServer extends Gui implements FieldCallback { this.copyIdButton = this.add(new ActButton(395, 300, 85, 24, new ButtonCallback() { public void use(ActButton elem, PressType action) { if(GuiServer.this.serverKey != null) - Window.setClipboard(Base64.getEncoder().encodeToString(GuiServer.this.serverKey.getEncoded())); + Window.setClipboard(EncryptUtil.getArmoredPubkey(GuiServer.this.serverKey, GuiServer.this.nameBox.getText())); } }, "Kopieren")); this.copyIdButton.enabled = this.serverKey != null; diff --git a/client/src/main/java/client/network/ClientLoginHandler.java b/client/src/main/java/client/network/ClientLoginHandler.java index ae2923a..1912696 100755 --- a/client/src/main/java/client/network/ClientLoginHandler.java +++ b/client/src/main/java/client/network/ClientLoginHandler.java @@ -67,7 +67,7 @@ public class ClientLoginHandler extends NetHandler implements IClientLoginHandle return; } this.connection.setConnectionState(PacketRegistry.LOGIN); - final SecretKey secret = EncryptUtil.createNewSharedKey(); + final SecretKey secret = EncryptUtil.createSharedKey(); final PublicKey pubkey = packet.getKey(); final byte[] token = packet.getToken(); if(this.server.getServerKey() == null) { diff --git a/common/src/main/java/common/util/EncryptUtil.java b/common/src/main/java/common/util/EncryptUtil.java index be8b831..9d0e0d2 100644 --- a/common/src/main/java/common/util/EncryptUtil.java +++ b/common/src/main/java/common/util/EncryptUtil.java @@ -15,6 +15,9 @@ import java.security.spec.EncodedKeySpec; import java.security.spec.InvalidKeySpecException; import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.X509EncodedKeySpec; +import java.util.Arrays; +import java.util.Base64; + import javax.crypto.BadPaddingException; import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; @@ -27,7 +30,7 @@ import javax.crypto.spec.SecretKeySpec; import common.log.Log; public class EncryptUtil { - public static SecretKey createNewSharedKey() { + public static SecretKey createSharedKey() { try { KeyGenerator keygen = KeyGenerator.getInstance("AES"); keygen.init(128); @@ -38,7 +41,7 @@ public class EncryptUtil { } } - public static KeyPair generateKeyPair() { + public static KeyPair createKeypair() { try { KeyPairGenerator pairgen = KeyPairGenerator.getInstance("RSA"); pairgen.initialize(2048); @@ -79,14 +82,14 @@ public class EncryptUtil { } public static byte[] encryptData(Key key, byte[] data) { - return cipherOperation(Cipher.ENCRYPT_MODE, key, data); + return cipher(Cipher.ENCRYPT_MODE, key, data); } public static byte[] decryptData(Key key, byte[] data) { - return cipherOperation(Cipher.DECRYPT_MODE, key, data); + return cipher(Cipher.DECRYPT_MODE, key, data); } - private static byte[] cipherOperation(int mode, Key key, byte[] data) { + private static byte[] cipher(int mode, Key key, byte[] data) { try { return createCipher(mode, key.getAlgorithm(), key).doFinal(data); } @@ -137,4 +140,67 @@ public class EncryptUtil { } return Util.getHexString(xor); } + + public static String getArmoredPubkey(PublicKey pubkey, String cn) { + StringBuilder sb = new StringBuilder("tcr-rsa-2048 "); + sb.append(Base64.getEncoder().encodeToString(pubkey.getEncoded())); + sb.append(' ').append(Base64.getEncoder().encodeToString(crc24(pubkey.getEncoded()))); + return cn == null || cn.isEmpty() ? sb.toString() : sb.append(' ').append(Util.sanitizeCommonName(cn)).toString(); + } + + public static Tuple parseArmoredPubkey(String armor) throws IllegalArgumentException { + String[] tok = armor.trim().split(" "); + if(tok.length != 3 && tok.length != 4) + throw new IllegalArgumentException("Key muss aus 3 oder 4 Segmenten bestehen"); + if(!tok[0].equals("tcr-rsa-2048")) + throw new IllegalArgumentException("Algorithmus '" + tok[0] + "' ist nicht unterstützt, es wird derzeit nur tcr-rsa-2048 verwendet"); + byte[] key; + try { + key = Base64.getDecoder().decode(tok[1]); + } + catch(IllegalArgumentException e) { + throw new IllegalArgumentException("Schlüssel ist nicht im Base64-Format", e); + } + byte[] hash; + try { + hash = Base64.getDecoder().decode(tok[2]); + } + catch(IllegalArgumentException e) { + throw new IllegalArgumentException("Prüfwert ist nicht im Base64-Format", e); + } + if(hash.length != 3) + throw new IllegalArgumentException("Prüfwert hat die falsche Länge, erwarte 3 Bytes, habe " + hash.length + " Bytes"); + byte[] keyHash = crc24(key); + if(!Arrays.equals(hash, keyHash)) + throw new IllegalArgumentException("Prüfwert ist falsch, erwarte " + Util.getHexString(hash) + ", habe " + Util.getHexString(keyHash)); + PublicKey pubkey; + try { + EncodedKeySpec spec = new X509EncodedKeySpec(key); + KeyFactory factory = KeyFactory.getInstance("RSA"); + pubkey = factory.generatePublic(spec); + } + catch(NoSuchAlgorithmException | InvalidKeySpecException e) { + throw new IllegalArgumentException("Öffentlicher Schlüssel konnte nicht dekodiert werden", e); + } + if(tok.length == 4 && !Util.isValidCommonName(tok[3])) + throw new IllegalArgumentException("Name muss aus 'a-z' '0-9' und '-' bestehen, kann keine aufeinander folgenden Bindestriche haben und darf nicht damit beginnen oder darin enden"); + return new Tuple(pubkey, tok.length == 4 ? tok[3] : null); + } + + private static final long CRC24_INIT = 0xB704CEL; + private static final long CRC24_POLY = 0x1864CFBL; + + private static byte[] crc24(byte[] data) { + long crc = CRC24_INIT; + for(byte bt : data) { + crc ^= (long)bt << 16; + for(int z = 0; z < 8; z++) { + crc <<= 1; + if((crc & 0x1000000) != 0) + crc ^= CRC24_POLY; + } + } + int value = (int)(crc & 0xFFFFFFL); + return new byte[] {(byte)((value >> 16) & 0xff), (byte)((value >> 8) & 0xff), (byte)(value & 0xff)}; + } } diff --git a/common/src/main/java/common/util/Util.java b/common/src/main/java/common/util/Util.java index 6b614ee..4780100 100644 --- a/common/src/main/java/common/util/Util.java +++ b/common/src/main/java/common/util/Util.java @@ -431,4 +431,44 @@ int utf_len(const char *str) { } return bytes; } + + public static String breakString(String str, int width) { + StringBuilder sb = new StringBuilder(); + for(int z = 0; z < str.length() / width; z++) { + sb.append(str.substring(z * width, (z + 1) * width)).append('\n'); + } + return sb.append(str.substring(str.length() - (str.length() % width), str.length())).toString(); + } + + public static String sanitizeCommonName(String str) { + str = str.trim(); + StringBuilder sb = new StringBuilder(); + boolean hyphen = true; + for(int z = 0; z < str.length(); z++) { + char ch = Character.toLowerCase(str.charAt(z)); + if((ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9')) { + sb.append(ch); + hyphen = false; + } + else if(ch == '-' || ch == ' ') { + if(!hyphen) + sb.append('-'); + hyphen = true; + } + } + return sb.length() > 0 && sb.charAt(sb.length() - 1) == '-' ? sb.substring(0, sb.length() - 1) : sb.toString(); + } + + public static boolean isValidCommonName(String str) { + boolean hyphen = true; + for(int z = 0; z < str.length(); z++) { + char ch = Character.toLowerCase(str.charAt(z)); + if((ch < 'a' || ch > 'z') && (ch < '0' || ch > '9') && ch != '-') + return false; + if(ch == '-' && hyphen) + return false; + hyphen = ch == '-'; + } + return !hyphen; + } } diff --git a/server/src/main/java/server/Server.java b/server/src/main/java/server/Server.java index 8884244..3896e52 100755 --- a/server/src/main/java/server/Server.java +++ b/server/src/main/java/server/Server.java @@ -464,7 +464,7 @@ public final class Server implements IThreadListener { long wtime = this.loadServerConfig(); if(this.keyPair == null) { Log.SYSTEM.info("Generiere neues Schlüsselpaar"); - this.keyPair = EncryptUtil.generateKeyPair(); + this.keyPair = EncryptUtil.createKeypair(); } // if(dtime == -1L) // { // dtime = World.START_TIME; @@ -499,7 +499,7 @@ public final class Server implements IThreadListener { } else { Log.SYSTEM.info("Generiere temporäres Schlüsselpaar"); - this.keyPair = EncryptUtil.generateKeyPair(); + this.keyPair = EncryptUtil.createKeypair(); Config.clear(); UniverseRegistry.clear(); Config.set("daylightCycle", "false", false);