diff --git a/common/src/main/java/me/lucko/luckperms/common/webeditor/socket/CryptographyUtils.java b/common/src/main/java/me/lucko/luckperms/common/webeditor/socket/SignatureAlgorithm.java similarity index 57% rename from common/src/main/java/me/lucko/luckperms/common/webeditor/socket/CryptographyUtils.java rename to common/src/main/java/me/lucko/luckperms/common/webeditor/socket/SignatureAlgorithm.java index 9c74d4c34..6526061e7 100644 --- a/common/src/main/java/me/lucko/luckperms/common/webeditor/socket/CryptographyUtils.java +++ b/common/src/main/java/me/lucko/luckperms/common/webeditor/socket/SignatureAlgorithm.java @@ -37,10 +37,70 @@ import java.security.spec.X509EncodedKeySpec; import java.util.Base64; /** - * Utilities for public/private key crypto used by the web editor socket connection. + * The signature algorithm & public/private key crypto logic used by the web editor socket connection. */ -public final class CryptographyUtils { - private CryptographyUtils() {} +public enum SignatureAlgorithm { + V1_RSA(1, "RSA", "SHA256withRSA") { + @Override + public KeyPair generateKeyPair() { + try { + KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA"); + generator.initialize(4096); + return generator.generateKeyPair(); + } catch (Exception e) { + throw new RuntimeException("Exception generating keypair", e); + } + } + }, + V2_ECDSA(2, "EC", "SHA256withECDSAinP1363Format") { + @Override + public KeyPair generateKeyPair() { + try { + KeyPairGenerator generator = KeyPairGenerator.getInstance("EC"); + generator.initialize(new ECGenParameterSpec("secp256r1")); + return generator.generateKeyPair(); + } catch (Exception e) { + throw new RuntimeException("Exception generating keypair", e); + } + } + }; + + /** + * The selected {@link SignatureAlgorithm} for the current environment. + */ + public static final SignatureAlgorithm INSTANCE; + + static { + // select an instance to use based on the available algorithms + SignatureAlgorithm instance = V1_RSA; + try { + KeyPairGenerator.getInstance(V2_ECDSA.keyFactoryAlgorithm); + Signature.getInstance(V2_ECDSA.signatureAlgorithm); + instance = V2_ECDSA; + } catch (Exception e) { + // ignore + } + INSTANCE = instance; + } + + private final int protocolVersion; + private final String keyFactoryAlgorithm; + private final String signatureAlgorithm; + + SignatureAlgorithm(int protocolVersion, String keyFactoryAlgorithm, String signatureAlgorithm) { + this.protocolVersion = protocolVersion; + this.keyFactoryAlgorithm = keyFactoryAlgorithm; + this.signatureAlgorithm = signatureAlgorithm; + } + + /** + * Gets the corresponding protocol version + * + * @return the protocol version + */ + public int protocolVersion() { + return this.protocolVersion; + } /** * Parse a public key from the given string. @@ -49,11 +109,11 @@ public final class CryptographyUtils { * @return the parsed public key * @throws IllegalArgumentException if the input was invalid */ - public static PublicKey parsePublicKey(String base64String) throws IllegalArgumentException { + public PublicKey parsePublicKey(String base64String) throws IllegalArgumentException { try { byte[] bytes = Base64.getDecoder().decode(base64String); X509EncodedKeySpec spec = new X509EncodedKeySpec(bytes); - KeyFactory rsa = KeyFactory.getInstance("EC"); + KeyFactory rsa = KeyFactory.getInstance(this.keyFactoryAlgorithm); return rsa.generatePublic(spec); } catch (Exception e) { throw new IllegalArgumentException("Exception parsing public key", e); @@ -65,15 +125,7 @@ public final class CryptographyUtils { * * @return the generated key pair */ - public static KeyPair generateKeyPair() { - try { - KeyPairGenerator generator = KeyPairGenerator.getInstance("EC"); - generator.initialize(new ECGenParameterSpec("secp256r1")); - return generator.generateKeyPair(); - } catch (Exception e) { - throw new RuntimeException("Exception generating keypair", e); - } - } + public abstract KeyPair generateKeyPair(); /** * Signs {@code msg} using the given {@link PrivateKey}. @@ -82,9 +134,9 @@ public final class CryptographyUtils { * @param msg the message * @return a base64 string containing the signature */ - public static String sign(PrivateKey privateKey, String msg) { + public String sign(PrivateKey privateKey, String msg) { try { - Signature sign = Signature.getInstance("SHA256withECDSAinP1363Format"); + Signature sign = Signature.getInstance(this.signatureAlgorithm); sign.initSign(privateKey); sign.update(msg.getBytes(StandardCharsets.UTF_8)); @@ -103,9 +155,9 @@ public final class CryptographyUtils { * @param signatureBase64 the provided signature * @return true if the signature is ok */ - public static boolean verify(PublicKey publicKey, String msg, String signatureBase64) { + public boolean verify(PublicKey publicKey, String msg, String signatureBase64) { try { - Signature sign = Signature.getInstance("SHA256withECDSAinP1363Format"); + Signature sign = Signature.getInstance(this.signatureAlgorithm); sign.initVerify(publicKey); sign.update(msg.getBytes(StandardCharsets.UTF_8)); diff --git a/common/src/main/java/me/lucko/luckperms/common/webeditor/socket/WebEditorSocket.java b/common/src/main/java/me/lucko/luckperms/common/webeditor/socket/WebEditorSocket.java index 6e0d990e0..6173349d7 100644 --- a/common/src/main/java/me/lucko/luckperms/common/webeditor/socket/WebEditorSocket.java +++ b/common/src/main/java/me/lucko/luckperms/common/webeditor/socket/WebEditorSocket.java @@ -47,8 +47,6 @@ import java.util.concurrent.TimeoutException; public class WebEditorSocket { - private static final int PROTOCOL_VERSION = 2; - /** The plugin */ private final LuckPermsPlugin plugin; /** The sender who created the editor session */ @@ -114,7 +112,7 @@ public class WebEditorSocket { String publicKey = Base64.getEncoder().encodeToString(this.pluginKeyPair.getPublic().getEncoded()); JsonObject socket = new JsonObject(); - socket.addProperty("protocolVersion", PROTOCOL_VERSION); + socket.addProperty("protocolVersion", SignatureAlgorithm.INSTANCE.protocolVersion()); socket.addProperty("channelId", channelId); socket.addProperty("publicKey", publicKey); @@ -132,7 +130,7 @@ public class WebEditorSocket { */ public void send(JsonObject msg) { String encoded = GsonProvider.normal().toJson(msg); - String signature = CryptographyUtils.sign(this.pluginKeyPair.getPrivate(), encoded); + String signature = SignatureAlgorithm.INSTANCE.sign(this.pluginKeyPair.getPrivate(), encoded); JsonObject frame = new JObject() .add("msg", encoded) diff --git a/common/src/main/java/me/lucko/luckperms/common/webeditor/socket/listener/HandlerHello.java b/common/src/main/java/me/lucko/luckperms/common/webeditor/socket/listener/HandlerHello.java index 94f17515b..bb4cd7851 100644 --- a/common/src/main/java/me/lucko/luckperms/common/webeditor/socket/listener/HandlerHello.java +++ b/common/src/main/java/me/lucko/luckperms/common/webeditor/socket/listener/HandlerHello.java @@ -27,7 +27,7 @@ package me.lucko.luckperms.common.webeditor.socket.listener; import com.google.gson.JsonObject; import me.lucko.luckperms.common.locale.Message; -import me.lucko.luckperms.common.webeditor.socket.CryptographyUtils; +import me.lucko.luckperms.common.webeditor.socket.SignatureAlgorithm; import me.lucko.luckperms.common.webeditor.socket.SocketMessageType; import me.lucko.luckperms.common.webeditor.socket.WebEditorSocket; import me.lucko.luckperms.common.webeditor.store.RemoteSession; @@ -73,7 +73,7 @@ public class HandlerHello implements Handler { String nonce = getStringOrThrow(msg, "nonce"); String sessionId = getStringOrThrow(msg, "sessionId"); String browser = msg.get("browser").getAsString(); - PublicKey remotePublicKey = CryptographyUtils.parsePublicKey(msg.get("publicKey").getAsString()); + PublicKey remotePublicKey = SignatureAlgorithm.INSTANCE.parsePublicKey(msg.get("publicKey").getAsString()); // check if the public keys are the same // (this allows the same editor to re-connect, but prevents new connections) diff --git a/common/src/main/java/me/lucko/luckperms/common/webeditor/socket/listener/WebEditorSocketListener.java b/common/src/main/java/me/lucko/luckperms/common/webeditor/socket/listener/WebEditorSocketListener.java index 6295a7288..0e7bb1010 100644 --- a/common/src/main/java/me/lucko/luckperms/common/webeditor/socket/listener/WebEditorSocketListener.java +++ b/common/src/main/java/me/lucko/luckperms/common/webeditor/socket/listener/WebEditorSocketListener.java @@ -27,7 +27,7 @@ package me.lucko.luckperms.common.webeditor.socket.listener; import com.google.gson.JsonObject; import me.lucko.luckperms.common.util.gson.GsonProvider; -import me.lucko.luckperms.common.webeditor.socket.CryptographyUtils; +import me.lucko.luckperms.common.webeditor.socket.SignatureAlgorithm; import me.lucko.luckperms.common.webeditor.socket.SocketMessageType; import me.lucko.luckperms.common.webeditor.socket.WebEditorSocket; import okhttp3.Response; @@ -126,7 +126,7 @@ public class WebEditorSocketListener extends WebSocketListener { // check signature to ensure the message is from the connected editor PublicKey remotePublicKey = this.socket.getRemotePublicKey(); - boolean verified = remotePublicKey != null && CryptographyUtils.verify(remotePublicKey, innerMsg, signature); + boolean verified = remotePublicKey != null && SignatureAlgorithm.INSTANCE.verify(remotePublicKey, innerMsg, signature); // parse the inner message JsonObject msg = GsonProvider.parser().parse(innerMsg).getAsJsonObject(); diff --git a/common/src/main/java/me/lucko/luckperms/common/webeditor/store/WebEditorStore.java b/common/src/main/java/me/lucko/luckperms/common/webeditor/store/WebEditorStore.java index 7e806d8ac..3e45f6d6f 100644 --- a/common/src/main/java/me/lucko/luckperms/common/webeditor/store/WebEditorStore.java +++ b/common/src/main/java/me/lucko/luckperms/common/webeditor/store/WebEditorStore.java @@ -29,7 +29,7 @@ import com.google.common.base.Supplier; import com.google.common.base.Suppliers; import me.lucko.luckperms.common.config.ConfigKeys; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; -import me.lucko.luckperms.common.webeditor.socket.CryptographyUtils; +import me.lucko.luckperms.common.webeditor.socket.SignatureAlgorithm; import java.security.KeyPair; import java.util.concurrent.CompletableFuture; @@ -51,7 +51,7 @@ public class WebEditorStore { this.keystore = new WebEditorKeystore(plugin.getBootstrap().getConfigDirectory().resolve("editor-keystore.json")); Supplier> keyPair = () -> CompletableFuture.supplyAsync( - CryptographyUtils::generateKeyPair, + SignatureAlgorithm.INSTANCE::generateKeyPair, plugin.getBootstrap().getScheduler().async() ); diff --git a/common/src/test/java/me/lucko/luckperms/common/webeditor/socket/CryptographyUtilsTest.java b/common/src/test/java/me/lucko/luckperms/common/webeditor/socket/CryptographyUtilsTest.java deleted file mode 100644 index b01ad1383..000000000 --- a/common/src/test/java/me/lucko/luckperms/common/webeditor/socket/CryptographyUtilsTest.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * This file is part of LuckPerms, licensed under the MIT License. - * - * Copyright (c) lucko (Luck) - * Copyright (c) contributors - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package me.lucko.luckperms.common.webeditor.socket; - -import org.junit.jupiter.api.Test; - -import java.security.KeyPair; -import java.security.PublicKey; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -public class CryptographyUtilsTest { - - @Test - public void testKeypairGenerate() { - CryptographyUtils.generateKeyPair(); - } - - @Test - public void testSignVerify() { - KeyPair keyPair = CryptographyUtils.generateKeyPair(); - - String signature = CryptographyUtils.sign(keyPair.getPrivate(), "test"); - assertTrue(CryptographyUtils.verify(keyPair.getPublic(), "test", signature)); - - assertFalse(CryptographyUtils.verify(keyPair.getPublic(), "test", "bleh")); - assertFalse(CryptographyUtils.verify(keyPair.getPublic(), "test", "")); - assertFalse(CryptographyUtils.verify(keyPair.getPublic(), "test", null)); - } - - @Test - public void testParseAndVerify() { - // the base64 values are generated from javascript crypto.subtle - PublicKey publicKey = CryptographyUtils.parsePublicKey("MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEkF5EWzdsbmVOYprtfMleBZYASm7AXBQQCE29xR2hpGkjVi4Fra/KPazRShqyGvQXY24sINsxIPEd4XamDfFAaQ=="); - assertTrue(CryptographyUtils.verify(publicKey, "hello world", "XAZJMxOlR5Mcq7nJxU4oS1fYyViYH1FZxWOXwOC+LRXYF8KeP58k5KLTjc35L974t3RukwAqflul0HY64bJT3w==")); - } - -} diff --git a/common/src/test/java/me/lucko/luckperms/common/webeditor/socket/SignatureAlgorithmTest.java b/common/src/test/java/me/lucko/luckperms/common/webeditor/socket/SignatureAlgorithmTest.java new file mode 100644 index 000000000..b784999b9 --- /dev/null +++ b/common/src/test/java/me/lucko/luckperms/common/webeditor/socket/SignatureAlgorithmTest.java @@ -0,0 +1,73 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package me.lucko.luckperms.common.webeditor.socket; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +import java.security.KeyPair; +import java.security.PublicKey; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class SignatureAlgorithmTest { + + @ParameterizedTest + @EnumSource + public void testKeypairGenerate(SignatureAlgorithm algorithm) { + algorithm.generateKeyPair(); + } + + @ParameterizedTest + @EnumSource + public void testSignVerify(SignatureAlgorithm algorithm) { + KeyPair keyPair = algorithm.generateKeyPair(); + + String signature = algorithm.sign(keyPair.getPrivate(), "test"); + assertTrue(algorithm.verify(keyPair.getPublic(), "test", signature)); + + assertFalse(algorithm.verify(keyPair.getPublic(), "test", "bleh")); + assertFalse(algorithm.verify(keyPair.getPublic(), "test", "")); + assertFalse(algorithm.verify(keyPair.getPublic(), "test", null)); + } + + @Test + public void testParseAndVerifyRSA() { + // the base64 values are generated from javascript crypto.subtle + PublicKey publicKey = SignatureAlgorithm.V1_RSA.parsePublicKey("MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA+Fhv9SmQIENpseq81/BCmGJ8Pf94X4yNdsrNaZ0uNGasUlIM/+aRrSA8X586BEGc7qZeVidegW4yM3LufaDnkoAyIQij7IfHzO09H3VdbLWF+RQY/dWj/cd6O2QkrjMXsW+LKkeKAsY2KTzxoyR9vLcTAP229mQPxiFWWidMAVeU3EzHWtV74UAuycG1Ja3kRtS031lugJJKAlgXUVlAF8tI+aoTQljpndptrhRBPtnUxtCCxj8jFM5houD5010zXIAzsAjg2NPHl/R/qypfHFMWYlcCGIbKMN61gM6lRyglLC+2dxSBw7b+GHTGHwoK3UMhqyonlRAP4W+UpA/tT/LazXHXalYOz8IYcnQgb4Np7pw2TFY2HA5sR4ZfCTnE1bemlWMHbjBc5CAnb7KyZVpFsxPLvcuSLaM4t3CyXBSwDJTMNj9aLSYg6FNAwnEcskRdgkrf23+1E30CaOsIKv4Um7SlnB+6qnxmRWpcs4rWPuS7IJXemaYks+gkgZr+Wt6ITPx8NRbyO1eLwsOOyN6g6DcZwc/2MTl1ItbP0+jAvE2NIU1KU0+uuyobZh1cldDGfaboshh9Ni9D4SSWzugPN9Ohs0QueEo3qi1Z6Jv9Jx2Bx1QlIV7FNEGpU6kDknRejewm+qzl5m0fxnfNH46x2FneUisqTea9Vo9suN8CAwEAAQ=="); + assertTrue(SignatureAlgorithm.V1_RSA.verify(publicKey, "hello world", "Z9XU+AIcHF7Y96grX/NLuNN2fI3nmuXfFss1QbTg80j3Jh8jZRMyFRfWz7rc1OToEsrAQXFY426nxN7JdXTTSvw4kErIn6amvTJBqEqWB+rA1FKJqnsXbl3gIG8UqwqBfMlYh5tYhBddsKVc3jW+4kPPGBgUfxgneHcpocgrwi3aX1vqvxJ4y49M1hs0hFH1VnO1VXcffQWnZRnEuUccYH61DHZHiFyWfo2SF6wdNMJG51idUBgZY7zyMnLRzL+07N9MrDJHkc9J4O5HRDuvVefoRNcvW/tpeVDMsLynP3psmyt33euds6LkdVtExolngepKAuGE9JBtnjFEWFakQ+INhvHZ7P4jGiLKRf7kDdckLqJxsH25w6MYzsq4jHTVbrzKehUAx0nnWhL3QrSLTwvly0WHTd4yd/rTVM2JUb+z5FPzuVQP6VgQmrwXYAhA6swkE/1poBWOgsCIe7rvHn3PYvU1D66fXe4lHbyQMAmAu39GLu3RpnmeXuiUT/yygMqvb1Rr8hTeOkjxQOuus+70ybkC21nVCigRFpI4ktvILe09F8jkL6VFHYtz6fKqXKJBTT2gIKijFCJeqCgkCxNnoLeOU+hsS3pZQUwuZS7l0Eyax1eKyelOv6zg10j2ido7/55dE3U0OZGOQ6VWRq8NiPuURId5NzFGURkDda8=")); + } + + @Test + public void testParseAndVerifyECDSA() { + // the base64 values are generated from javascript crypto.subtle + PublicKey publicKey = SignatureAlgorithm.V2_ECDSA.parsePublicKey("MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEkF5EWzdsbmVOYprtfMleBZYASm7AXBQQCE29xR2hpGkjVi4Fra/KPazRShqyGvQXY24sINsxIPEd4XamDfFAaQ=="); + assertTrue(SignatureAlgorithm.V2_ECDSA.verify(publicKey, "hello world", "XAZJMxOlR5Mcq7nJxU4oS1fYyViYH1FZxWOXwOC+LRXYF8KeP58k5KLTjc35L974t3RukwAqflul0HY64bJT3w==")); + } + +}