diff --git a/.idea/encodings.xml b/.idea/encodings.xml
new file mode 100644
index 0000000..aa00ffa
--- /dev/null
+++ b/.idea/encodings.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
index f4feca1..2777d9f 100644
--- a/README.md
+++ b/README.md
@@ -2,10 +2,22 @@
This plugin adds naturally spawning Giants with AI to your Minecraft server.
-### Requested features
-- Texture variation \
- I think this can be done, but would require a texture pack
-- Boss bar \
- I don't want to get buried in that, so this would require an API, which, for performance reasons, should not be mandatory
-- Hitboxes \
- I don't know if this is still a major issue, but a person reported that the hitbox doesn't cover the whole body
\ No newline at end of file
+### Signing
+Public key goes into `resources/verifies_downloaded_jars.pem`
+
+A test (and default) keystore is provided:
+- keystore: `testkeystore`
+- storepass: `123456`
+- alias: `testkey`
+
+When using `mvn`, override with `-Djarsigner.`
+```
+mvn clean package -Djarsigner.keystore=/home/user/mykeystore.jks -Djarsigner.alias=mykey
+```
+
+To create a keystore and export public key:
+```
+keytool -keystore testkeystore2.jks -genkeypair -keyalg RSA -alias testkey -validity 999999
+keytool -exportcert -alias testkey -keystore testkeystore2.jks -file cert.cer -rfc
+openssl x509 -inform pem -in cert.cer -pubkey -noout > public_key.pem
+```
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index e99f02a..74a60cd 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,8 +5,12 @@
2.0.8-SNAPSHOT
- 11
- 11
+ 11
+ ${project.basedir}/testkeystore.jks
+ testkey
+ 123456
+ UTF-8
+ UTF-8
@@ -53,7 +57,7 @@
eu.m724
jarupdater
- 0.1.3
+ 0.1.5
@@ -79,6 +83,7 @@
3.6.0
false
+ true
@@ -93,6 +98,15 @@
eu.m724:jarupdater
+
+
+ *
+
+ META-INF/MANIFEST.MF
+ META-INF/maven/**
+
+
+
@@ -103,6 +117,30 @@
+
+ org.apache.maven.plugins
+ maven-jarsigner-plugin
+ 3.1.0
+
+
+ sign
+
+ sign
+
+
+
+ verify
+
+ verify
+
+
+
+
+ ${jarsigner.keystore}
+ ${jarsigner.alias}
+ ${jarsigner.storepass}
+
+
diff --git a/src/main/java/eu/m724/giants/GiantProcessor.java b/src/main/java/eu/m724/giants/GiantProcessor.java
index 7aff656..456c592 100644
--- a/src/main/java/eu/m724/giants/GiantProcessor.java
+++ b/src/main/java/eu/m724/giants/GiantProcessor.java
@@ -1,14 +1,12 @@
package eu.m724.giants;
import org.bukkit.Location;
-import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.entity.*;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityDeathEvent;
import org.bukkit.event.entity.EntitySpawnEvent;
-import org.bukkit.event.entity.EntityTargetLivingEntityEvent;
import org.bukkit.event.world.ChunkLoadEvent;
import org.bukkit.inventory.ItemStack;
import org.bukkit.persistence.PersistentDataType;
diff --git a/src/main/java/eu/m724/giants/GiantsPlugin.java b/src/main/java/eu/m724/giants/GiantsPlugin.java
index bfdf89d..1fd19cc 100644
--- a/src/main/java/eu/m724/giants/GiantsPlugin.java
+++ b/src/main/java/eu/m724/giants/GiantsPlugin.java
@@ -1,5 +1,6 @@
package eu.m724.giants;
+import eu.m724.giants.updater.JarVerifier;
import eu.m724.giants.updater.PluginUpdater;
import eu.m724.giants.updater.UpdateCommand;
import org.bstats.bukkit.Metrics;
@@ -10,7 +11,8 @@ import org.bukkit.entity.LivingEntity;
import org.bukkit.plugin.java.JavaPlugin;
import java.io.File;
-import java.lang.reflect.Constructor;
+import java.io.IOException;
+import java.io.InputStream;
public class GiantsPlugin extends JavaPlugin implements CommandExecutor {
private final File configFile = new File(getDataFolder(), "config.yml");
@@ -28,7 +30,7 @@ public class GiantsPlugin extends JavaPlugin implements CommandExecutor {
UpdateCommand updateCommand = null;
if (configuration.updater != null) {
- PluginUpdater updater = PluginUpdater.build(this, configuration.updater);
+ PluginUpdater updater = PluginUpdater.build(this, getFile(), configuration.updater);
updater.initNotifier();
updateCommand = new UpdateCommand(updater);
}
@@ -37,7 +39,22 @@ public class GiantsPlugin extends JavaPlugin implements CommandExecutor {
giantProcessor.start();
- // bStats is optional
+ new Metrics(this, 14131);
+
+ try (InputStream keyInputStream = getResource("verifies_downloaded_jars.pem")) {
+ JarVerifier.verifyWithRsaKey(
+ getFile().getPath().replace(".paper-remapped/", ""), // paper remapping removes data from manifest
+ keyInputStream
+ );
+ } catch (IOException e) {
+ getLogger().warning(e.getMessage());
+ getLogger().warning("Failed checking JAR signature. This is not important right now, but it usually forecasts future problems.");
+ } catch (JarVerifier.VerificationException e) {
+ getLogger().warning(e.getMessage());
+ getLogger().warning("Plugin JAR is of invalid signature. It's possible that the signature has changed, in which case it's normal. But I can't verify that, you must see the version changelog yourself.");
+ }
+
+ /* bStats is optional. not anymore
try {
Class> clazz = Class.forName("eu.m724.giants.bukkit.Metrics");
Constructor> constructor = clazz.getDeclaredConstructor(JavaPlugin.class, int.class);
@@ -45,10 +62,12 @@ public class GiantsPlugin extends JavaPlugin implements CommandExecutor {
getLogger().info("Enabled bStats");
} catch (Exception e) {
getLogger().info("Not using bStats (" + e.getClass().getName() + ")");
- }
+ }*/
}
+
+
// TODO api, untested
/**
diff --git a/src/main/java/eu/m724/giants/updater/JarVerifier.java b/src/main/java/eu/m724/giants/updater/JarVerifier.java
new file mode 100644
index 0000000..e4a0133
--- /dev/null
+++ b/src/main/java/eu/m724/giants/updater/JarVerifier.java
@@ -0,0 +1,134 @@
+package eu.m724.giants.updater;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.CodeSigner;
+import java.security.GeneralSecurityException;
+import java.security.KeyFactory;
+import java.security.PublicKey;
+import java.security.cert.Certificate;
+import java.security.interfaces.RSAPublicKey;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.Base64;
+import java.util.Enumeration;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+import java.util.jar.Manifest;
+
+public class JarVerifier {
+ /**
+ * Loads an RSA public key from a PEM file
+ *
+ * @param keyInputStream inputStream of the public key file
+ * @return {@link RSAPublicKey} instance of the public key
+ * @throws IOException if reading the key input stream failed
+ */
+ private static RSAPublicKey loadPublicKey(InputStream keyInputStream) throws IOException {
+ // Read the key file
+ String keyContent = new String(keyInputStream.readAllBytes());
+
+ // Remove PEM headers and newlines
+ keyContent = keyContent.replace("-----BEGIN PUBLIC KEY-----", "")
+ .replace("-----END PUBLIC KEY-----", "")
+ .replaceAll("\\s+", "");
+
+ // Decode the key
+ byte[] keyBytes = Base64.getDecoder().decode(keyContent);
+
+ // Create public key specification
+ X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes);
+
+ // Generate public key
+ try {
+ KeyFactory kf = KeyFactory.getInstance("RSA");
+ return (RSAPublicKey) kf.generatePublic(spec);
+ } catch (GeneralSecurityException e) {
+ // because this shouldn't happen
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Verifies if a JAR file's signature matches an RSA public key
+ *
+ * @param jarPath the path of the JAR file
+ * @param keyInputStream inputStream of the public key file
+ * @throws VerificationException if verification failed
+ */
+ public static void verifyWithRsaKey(String jarPath, InputStream keyInputStream) throws VerificationException {
+ try {
+ // Load the RSA public key
+ RSAPublicKey publicKey = loadPublicKey(keyInputStream);
+
+ // Open the JAR file
+ try (JarFile jarFile = new JarFile(jarPath, true)) {
+ byte[] buffer = new byte[8192];
+ Enumeration entries = jarFile.entries();
+
+ // Get manifest to check signature files
+ Manifest manifest = jarFile.getManifest();
+ if (manifest == null) {
+ throw new VerificationException("JAR has no manifest");
+ }
+
+ while (entries.hasMoreElements()) {
+ JarEntry entry = entries.nextElement();
+
+ if (entry.isDirectory() || entry.getName().startsWith("META-INF/")) {
+ continue;
+ }
+
+ int bytesRead = 0;
+ // Read entry to trigger signature verification
+ try (InputStream is = jarFile.getInputStream(entry)) {
+ while ((bytesRead += is.read(buffer)) != -1) {
+ if (bytesRead > 1024 * 1024 * 100) { // unusual for a file to have >100 MiB
+ throw new IOException("File too large: " + entry.getName());
+ }
+ }
+ }
+
+ // Get signers for this entry
+ CodeSigner[] signers = entry.getCodeSigners();
+ if (signers == null || signers.length == 0) {
+ throw new VerificationException("Unsigned entry: " + entry.getName());
+ }
+
+ // Check if any signer's public key matches our RSA key
+ boolean keyMatch = false;
+ for (CodeSigner signer : signers) {
+ for (Certificate cert : signer.getSignerCertPath().getCertificates()) {
+ PublicKey certPublicKey = cert.getPublicKey();
+ if (certPublicKey instanceof RSAPublicKey) {
+ RSAPublicKey rsaKey = (RSAPublicKey) certPublicKey;
+ if (rsaKey.getModulus().equals(publicKey.getModulus()) &&
+ rsaKey.getPublicExponent().equals(publicKey.getPublicExponent())) {
+ keyMatch = true;
+ break;
+ }
+ }
+ }
+ if (keyMatch) break;
+ }
+
+ if (!keyMatch) {
+ throw new VerificationException("Entry not signed with matching RSA key: " + entry.getName());
+ }
+ }
+ }
+
+ } catch (IOException e) {
+ throw new VerificationException("Verification error: " + e.getMessage(), e);
+ }
+ }
+
+ public static class VerificationException extends Exception {
+ public VerificationException(String message) {
+ super(message);
+ }
+
+ public VerificationException(String message, Exception exception) {
+ super(message, exception);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/eu/m724/giants/updater/PluginEnvironment.java b/src/main/java/eu/m724/giants/updater/PluginEnvironment.java
index ce22d9d..25c6653 100644
--- a/src/main/java/eu/m724/giants/updater/PluginEnvironment.java
+++ b/src/main/java/eu/m724/giants/updater/PluginEnvironment.java
@@ -6,9 +6,7 @@ import org.bukkit.plugin.Plugin;
import java.nio.file.Path;
public class PluginEnvironment extends ConstantEnvironment {
- public PluginEnvironment(Plugin plugin, String channel) {
- super(plugin.getDescription().getVersion(),
- channel,
- Path.of(plugin.getClass().getProtectionDomain().getCodeSource().getLocation().getPath()));
+ public PluginEnvironment(Plugin plugin, String channel, Path path) {
+ super(plugin.getDescription().getVersion(), channel, path);
}
}
\ No newline at end of file
diff --git a/src/main/java/eu/m724/giants/updater/PluginUpdater.java b/src/main/java/eu/m724/giants/updater/PluginUpdater.java
index 28aba62..9af7f19 100644
--- a/src/main/java/eu/m724/giants/updater/PluginUpdater.java
+++ b/src/main/java/eu/m724/giants/updater/PluginUpdater.java
@@ -1,24 +1,30 @@
package eu.m724.giants.updater;
-import eu.m724.jarupdater.Updater;
import eu.m724.jarupdater.download.Downloader;
import eu.m724.jarupdater.download.SimpleDownloader;
import eu.m724.jarupdater.environment.Environment;
import eu.m724.jarupdater.live.GiteaMetadataDAO;
import eu.m724.jarupdater.live.MetadataDAO;
import eu.m724.jarupdater.live.MetadataFacade;
+import eu.m724.jarupdater.updater.Updater;
import org.bukkit.plugin.Plugin;
+import java.io.File;
+import java.nio.file.NoSuchFileException;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
+
public class PluginUpdater extends Updater {
private final Plugin plugin;
+ boolean updatePending = false;
private PluginUpdater(Environment environment, MetadataFacade metadataProvider, Downloader downloader, Plugin plugin) {
super(environment, metadataProvider, downloader);
this.plugin = plugin;
}
- public static PluginUpdater build(Plugin plugin, String channel) {
- Environment environment = new PluginEnvironment(plugin, channel);
+ public static PluginUpdater build(Plugin plugin, File file, String channel) {
+ Environment environment = new PluginEnvironment(plugin, channel, file.toPath());
MetadataDAO metadataDAO = new GiteaMetadataDAO("https://git.m724.eu/Minecon724/giants-metadata", "master");
MetadataFacade metadataFacade = new MetadataFacade(environment, metadataDAO);
Downloader downloader = new SimpleDownloader("giants");
@@ -30,4 +36,26 @@ public class PluginUpdater extends Updater {
UpdateNotifier updateNotifier = new UpdateNotifier(plugin, this, (version) -> {});
updateNotifier.register();
}
+
+ @Override
+ public CompletableFuture installLatestVersion() throws NoSuchFileException {
+ return installLatestVersion(true);
+ }
+
+ public CompletableFuture installLatestVersion(boolean verify) throws NoSuchFileException {
+ if (this.downloaded == null) {
+ throw new NoSuchFileException("Download it first");
+ } else {
+ return this.downloaded.thenCompose((file) -> {
+ if (verify) {
+ try {
+ JarVerifier.verifyWithRsaKey(file.getPath(), plugin.getResource("verifies_downloaded_jars.pem"));
+ } catch (JarVerifier.VerificationException e) {
+ throw new CompletionException(e);
+ }
+ }
+ return this.downloader.install(file, this.environment.getRunningJarFilePath().toFile());
+ });
+ }
+ }
}
diff --git a/src/main/java/eu/m724/giants/updater/UpdateCommand.java b/src/main/java/eu/m724/giants/updater/UpdateCommand.java
index e77d05b..d0d55e0 100644
--- a/src/main/java/eu/m724/giants/updater/UpdateCommand.java
+++ b/src/main/java/eu/m724/giants/updater/UpdateCommand.java
@@ -1,34 +1,32 @@
package eu.m724.giants.updater;
-import eu.m724.jarupdater.Updater;
-import eu.m724.jarupdater.object.Version;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import java.nio.file.NoSuchFileException;
-import java.text.SimpleDateFormat;
-import java.time.Instant;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
-import java.util.concurrent.CompletableFuture;
/**
* not actually a command but deserves a separate file
*/
public class UpdateCommand {
- private final Updater updater;
+ private final PluginUpdater updater;
- public UpdateCommand(Updater updater) {
+ public UpdateCommand(PluginUpdater updater) {
this.updater = updater;
}
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
sender.sendMessage("Please wait...");
sender.sendMessage("Channel: " + updater.getEnvironment().getChannel());
- CompletableFuture latestFuture = updater.getLatestVersion();
+
+ if (updater.updatePending) {
+ sender.sendMessage("Server restart required");
+ }
if (args.length == 1) { // remember this function is proxied
- latestFuture.thenAccept(metadata -> {
+ updater.getLatestVersion().thenAccept(metadata -> {
if (metadata != null) {
sender.sendMessage("An update is available!");
sender.sendMessage("Giants " + metadata.getLabel() + " released " + formatDate(metadata.getTimestamp()));
@@ -70,6 +68,7 @@ public class UpdateCommand {
try {
updater.installLatestVersion().thenAccept(v -> {
sender.sendMessage("Installation completed, restart server to apply.");
+ updater.updatePending = true;
}).exceptionally(e -> {
sender.sendMessage("Install failed, see console for details. " + e.getMessage());
e.printStackTrace();
@@ -86,7 +85,7 @@ public class UpdateCommand {
return true;
}
- private String formatDate(long timestamp) { // TODO move this
+ private String formatDate(long timestamp) {
return LocalDate.ofEpochDay(timestamp / 86400).format(DateTimeFormatter.ofPattern("dd.MM.yyyy"));
}
diff --git a/src/main/java/eu/m724/giants/updater/UpdateNotifier.java b/src/main/java/eu/m724/giants/updater/UpdateNotifier.java
index bfd7c3d..eca4bfe 100644
--- a/src/main/java/eu/m724/giants/updater/UpdateNotifier.java
+++ b/src/main/java/eu/m724/giants/updater/UpdateNotifier.java
@@ -1,7 +1,7 @@
package eu.m724.giants.updater;
-import eu.m724.jarupdater.Updater;
import eu.m724.jarupdater.object.Version;
+import eu.m724.jarupdater.updater.Updater;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
diff --git a/src/main/resources/verifies_downloaded_jars.pem b/src/main/resources/verifies_downloaded_jars.pem
new file mode 100644
index 0000000..dd6058a
--- /dev/null
+++ b/src/main/resources/verifies_downloaded_jars.pem
@@ -0,0 +1,9 @@
+-----BEGIN PUBLIC KEY-----
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxjjjayrwlo3cnv+rX1EX
+lJN9vHS9MNfvE7zFOHr2JEAx2fRosb2oRzNK0ssoHJFOgrwLWIqrLVS8bTHRujsF
+asck2Z1RY5UGe34vNQ5u5MZvm4G25LggC6+ei2kEptoAfgp9kjmeKVPiSnruLn7N
+YQc9U4nmr/vJg+SNmy00EkXFU5z3ZsLf8aCjx9rtogZzyZmVPXEDGY3ZjzZxOpv9
+TAvSQlmrc6qmLlY7XZmJMtbzCTq+qqemZBKp6WpNmEogpPgXamOrET434+oE7OCz
++WCFKsVN8qbrQdFLf1HSjghvDoIjHcGfz6cP4nBonSKIfMcr+NziAVmimfqOiDxa
+nwIDAQAB
+-----END PUBLIC KEY-----
\ No newline at end of file
diff --git a/testkeystore.jks b/testkeystore.jks
new file mode 100644
index 0000000..6c10b16
Binary files /dev/null and b/testkeystore.jks differ