Updater for old Java
This commit is contained in:
parent
0f88955ea8
commit
94109f0faa
5 changed files with 51 additions and 192 deletions
2
pom.xml
2
pom.xml
|
@ -57,7 +57,7 @@
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>eu.m724</groupId>
|
<groupId>eu.m724</groupId>
|
||||||
<artifactId>jarupdater</artifactId>
|
<artifactId>jarupdater</artifactId>
|
||||||
<version>0.1.7</version>
|
<version>0.1.10</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
package eu.m724.giants;
|
package eu.m724.giants;
|
||||||
|
|
||||||
import eu.m724.giants.updater.JarVerifier;
|
|
||||||
import eu.m724.giants.updater.PluginUpdater;
|
import eu.m724.giants.updater.PluginUpdater;
|
||||||
import eu.m724.giants.updater.UpdateCommand;
|
import eu.m724.giants.updater.UpdateCommand;
|
||||||
|
import eu.m724.jarupdater.verify.VerificationException;
|
||||||
import org.bstats.bukkit.Metrics;
|
import org.bstats.bukkit.Metrics;
|
||||||
import org.bukkit.Location;
|
import org.bukkit.Location;
|
||||||
import org.bukkit.command.CommandExecutor;
|
import org.bukkit.command.CommandExecutor;
|
||||||
|
@ -28,32 +28,40 @@ public class GiantsPlugin extends JavaPlugin implements CommandExecutor {
|
||||||
|
|
||||||
configuration.load();
|
configuration.load();
|
||||||
|
|
||||||
UpdateCommand updateCommand = null;
|
|
||||||
if (configuration.updater != null) {
|
|
||||||
PluginUpdater updater = PluginUpdater.build(this, getFile(), configuration.updater);
|
|
||||||
updater.initNotifier();
|
|
||||||
updateCommand = new UpdateCommand(updater);
|
|
||||||
}
|
|
||||||
|
|
||||||
getCommand("giants").setExecutor(new GiantsCommand(this, configuration, updateCommand));
|
|
||||||
|
|
||||||
giantProcessor.start();
|
giantProcessor.start();
|
||||||
|
|
||||||
|
// bStats
|
||||||
new Metrics(this, 14131);
|
new Metrics(this, 14131);
|
||||||
|
|
||||||
|
// updater
|
||||||
|
PluginUpdater updater = null;
|
||||||
|
|
||||||
|
if (configuration.updater != null) {
|
||||||
try (InputStream keyInputStream = getResource("verifies_downloaded_jars.pem")) {
|
try (InputStream keyInputStream = getResource("verifies_downloaded_jars.pem")) {
|
||||||
JarVerifier.verifyWithRsaKey(
|
updater = PluginUpdater.build(this, getFile(), configuration.updater, keyInputStream);
|
||||||
getFile().getPath().replace(".paper-remapped/", ""), // paper remapping removes data from manifest
|
|
||||||
keyInputStream
|
|
||||||
);
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
getLogger().warning(e.getMessage());
|
e.printStackTrace();
|
||||||
getLogger().warning("Failed checking JAR signature. This is not important right now, but it usually indicates future problems. If this persists, re-download the JAR from SpigotMC.");
|
getLogger().severe("Failed to load updater");
|
||||||
} catch (JarVerifier.VerificationException e) {
|
}
|
||||||
|
|
||||||
|
if (updater != null) {
|
||||||
|
try {
|
||||||
|
updater.verifyJar(
|
||||||
|
getFile().getPath().replace(".paper-remapped/", "") // paper remapping removes data from manifest
|
||||||
|
);
|
||||||
|
} catch (VerificationException e) {
|
||||||
getLogger().warning(e.getMessage());
|
getLogger().warning(e.getMessage());
|
||||||
getLogger().warning("Plugin JAR is of invalid signature. If this persists, re-download the JAR from SpigotMC.");
|
getLogger().warning("Plugin JAR is of invalid signature. If this persists, re-download the JAR from SpigotMC.");
|
||||||
getLogger().warning("Did you update from 2.0.7? If yes, you must re-download 2.0.9+ from SpigotMC, then delete plugins/.paper-remapped");
|
getLogger().warning("Did you update from 2.0.7? If yes, you must re-download 2.0.9+ from SpigotMC, then delete plugins/.paper-remapped");
|
||||||
|
|
||||||
}
|
}
|
||||||
|
updater.initNotifier();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateCommand updateCommand = new UpdateCommand(updater);
|
||||||
|
|
||||||
|
getCommand("giants").setExecutor(new GiantsCommand(this, configuration, updateCommand));
|
||||||
|
|
||||||
/* bStats is optional. not anymore
|
/* bStats is optional. not anymore
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -1,140 +0,0 @@
|
||||||
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.cert.X509Certificate;
|
|
||||||
import java.security.interfaces.RSAPublicKey;
|
|
||||||
import java.security.spec.X509EncodedKeySpec;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Base64;
|
|
||||||
import java.util.Enumeration;
|
|
||||||
import java.util.List;
|
|
||||||
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<JarEntry> 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;
|
|
||||||
List<String> signerPublicKeys = new ArrayList<>();
|
|
||||||
|
|
||||||
for (CodeSigner signer : signers) {
|
|
||||||
for (Certificate cert : signer.getSignerCertPath().getCertificates()) {
|
|
||||||
PublicKey certPublicKey = cert.getPublicKey();
|
|
||||||
if (certPublicKey instanceof RSAPublicKey) {
|
|
||||||
RSAPublicKey rsaKey = (RSAPublicKey) certPublicKey;
|
|
||||||
signerPublicKeys.add(Base64.getEncoder().encodeToString(rsaKey.getEncoded()));
|
|
||||||
if (rsaKey.getModulus().equals(publicKey.getModulus()) &&
|
|
||||||
rsaKey.getPublicExponent().equals(publicKey.getPublicExponent())) {
|
|
||||||
keyMatch = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (keyMatch) break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!keyMatch) {
|
|
||||||
throw new VerificationException("Entry " + entry.getName() + " signed with " + String.join(", ", signerPublicKeys) + ", none of which match " + Base64.getEncoder().encodeToString(publicKey.getEncoded()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -7,55 +7,41 @@ import eu.m724.jarupdater.live.GiteaMetadataDAO;
|
||||||
import eu.m724.jarupdater.live.MetadataDAO;
|
import eu.m724.jarupdater.live.MetadataDAO;
|
||||||
import eu.m724.jarupdater.live.MetadataFacade;
|
import eu.m724.jarupdater.live.MetadataFacade;
|
||||||
import eu.m724.jarupdater.updater.Updater;
|
import eu.m724.jarupdater.updater.Updater;
|
||||||
|
import eu.m724.jarupdater.verify.SignatureVerifier;
|
||||||
|
import eu.m724.jarupdater.verify.VerificationException;
|
||||||
|
import eu.m724.jarupdater.verify.Verifier;
|
||||||
import org.bukkit.plugin.Plugin;
|
import org.bukkit.plugin.Plugin;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.nio.file.NoSuchFileException;
|
import java.io.IOException;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.io.InputStream;
|
||||||
import java.util.concurrent.CompletionException;
|
|
||||||
|
|
||||||
public class PluginUpdater extends Updater {
|
public class PluginUpdater extends Updater {
|
||||||
private final Plugin plugin;
|
private final Plugin plugin;
|
||||||
boolean updatePending = false;
|
boolean updatePending = false;
|
||||||
|
|
||||||
private PluginUpdater(Environment environment, MetadataFacade metadataProvider, Downloader downloader, Plugin plugin) {
|
private PluginUpdater(Environment environment, MetadataFacade metadataProvider, Downloader downloader, Verifier verifier, Plugin plugin) {
|
||||||
super(environment, metadataProvider, downloader);
|
super(environment, metadataProvider, downloader, verifier);
|
||||||
this.plugin = plugin;
|
this.plugin = plugin;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static PluginUpdater build(Plugin plugin, File file, String channel) {
|
public static PluginUpdater build(Plugin plugin, File file, String channel, InputStream keyInputStream) throws IOException {
|
||||||
Environment environment = new PluginEnvironment(plugin, channel, file.toPath());
|
Environment environment = new PluginEnvironment(plugin, channel, file.toPath());
|
||||||
MetadataDAO metadataDAO = new GiteaMetadataDAO("https://git.m724.eu/Minecon724/giants-metadata", "master");
|
MetadataDAO metadataDAO = new GiteaMetadataDAO("https://git.m724.eu/Minecon724/giants-metadata", "master");
|
||||||
MetadataFacade metadataFacade = new MetadataFacade(environment, metadataDAO);
|
MetadataFacade metadataFacade = new MetadataFacade(environment, metadataDAO);
|
||||||
Downloader downloader = new SimpleDownloader("giants");
|
Downloader downloader = new SimpleDownloader("giants");
|
||||||
|
SignatureVerifier verifier = new SignatureVerifier();
|
||||||
|
verifier.loadPublicKey(keyInputStream);
|
||||||
|
|
||||||
return new PluginUpdater(environment, metadataFacade, downloader, plugin);
|
return new PluginUpdater(environment, metadataFacade, downloader, verifier, plugin);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void verifyJar(String jarPath) throws VerificationException {
|
||||||
|
verifier.verify(jarPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void initNotifier() {
|
public void initNotifier() {
|
||||||
UpdateNotifier updateNotifier = new UpdateNotifier(plugin, this, (version) -> {});
|
UpdateNotifier updateNotifier = new UpdateNotifier(plugin, this, (version) -> {});
|
||||||
updateNotifier.register();
|
updateNotifier.register();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public CompletableFuture<Void> installLatestVersion() throws NoSuchFileException {
|
|
||||||
return installLatestVersion(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public CompletableFuture<Void> 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());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,6 +38,11 @@ public class UpdateCommand {
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
|
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
|
||||||
|
if (updater == null) {
|
||||||
|
sender.sendMessage("Updater is disabled");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
sender.sendMessage("Please wait...");
|
sender.sendMessage("Please wait...");
|
||||||
sender.sendMessage("Channel: " + updater.getEnvironment().getChannel());
|
sender.sendMessage("Channel: " + updater.getEnvironment().getChannel());
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue