mirror of
https://github.com/HMCL-dev/HMCL.git
synced 2025-09-15 06:45:42 -04:00
Add JAR integrity check
This commit is contained in:
parent
e38bfdfc73
commit
dd45e1b3db
@ -1,4 +1,9 @@
|
||||
import java.nio.file.FileSystems
|
||||
import java.security.KeyFactory
|
||||
import java.security.MessageDigest
|
||||
import java.security.Signature
|
||||
import java.security.spec.PKCS8EncodedKeySpec
|
||||
import java.util.zip.ZipFile
|
||||
|
||||
def buildnumber = System.getenv("BUILD_NUMBER") ?: "SNAPSHOT"
|
||||
def versionroot = System.getenv("VERSION_ROOT") ?: "3.1"
|
||||
@ -9,10 +14,37 @@ dependencies {
|
||||
compile rootProject.files("lib/JFoenix.jar")
|
||||
}
|
||||
|
||||
def digest(String algorithm, byte[] bytes) {
|
||||
return MessageDigest.getInstance(algorithm).digest(bytes)
|
||||
}
|
||||
|
||||
def createChecksum(File file) {
|
||||
def algorithm = "SHA-1"
|
||||
def suffix = "sha1"
|
||||
new File(file.parentFile, file.name + "." + suffix).text = MessageDigest.getInstance(algorithm).digest(file.bytes).encodeHex().toString() + "\n"
|
||||
new File(file.parentFile, file.name + "." + suffix).text = digest(algorithm, file.bytes).encodeHex().toString() + "\n"
|
||||
}
|
||||
|
||||
def attachSignature() {
|
||||
def keyLocation = System.getenv("HMCL_SIGNATURE_KEY");
|
||||
if(keyLocation == null)
|
||||
return;
|
||||
def privatekey = KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(new File(keyLocation).bytes))
|
||||
|
||||
def signer = Signature.getInstance("SHA512withRSA")
|
||||
signer.initSign(privatekey)
|
||||
new ZipFile(jar.archivePath).withCloseable { zip ->
|
||||
zip.stream()
|
||||
.sorted(Comparator.comparing({ it.name }))
|
||||
.forEach({
|
||||
signer.update(digest("SHA-512", it.name.getBytes("UTF-8")))
|
||||
signer.update(digest("SHA-512", zip.getInputStream(it).bytes))
|
||||
})
|
||||
}
|
||||
def signature = signer.sign()
|
||||
|
||||
FileSystems.newFileSystem(URI.create("jar:" + jar.archivePath.toURI()), [:]).withCloseable { zipfs ->
|
||||
zipfs.getPath("META-INF/hmcl_signature").bytes = signature
|
||||
}
|
||||
}
|
||||
|
||||
jar {
|
||||
@ -26,6 +58,7 @@ jar {
|
||||
}
|
||||
|
||||
doLast {
|
||||
attachSignature()
|
||||
createChecksum(archivePath)
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,130 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see {http://www.gnu.org/licenses/}.
|
||||
*/
|
||||
package org.jackhuang.hmcl.upgrade;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Path;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.PublicKey;
|
||||
import java.security.Signature;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.logging.Level;
|
||||
import java.util.TreeMap;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipFile;
|
||||
|
||||
import org.jackhuang.hmcl.util.DigestUtils;
|
||||
import org.jackhuang.hmcl.util.IOUtils;
|
||||
|
||||
/**
|
||||
* A class that checks the integrity of HMCL.
|
||||
*
|
||||
* @author yushijinhun
|
||||
*/
|
||||
public final class IntegrityChecker {
|
||||
private IntegrityChecker() {}
|
||||
|
||||
private static final String SIGNATURE_FILE = "META-INF/hmcl_signature";
|
||||
private static final String PUBLIC_KEY_FILE = "assets/hmcl_signature_publickey.der";
|
||||
|
||||
private static PublicKey getPublicKey() throws IOException {
|
||||
try (InputStream in = IntegrityChecker.class.getResourceAsStream("/" + PUBLIC_KEY_FILE)) {
|
||||
if (in == null) {
|
||||
throw new IOException("Public key not found");
|
||||
}
|
||||
return KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(IOUtils.readFullyAsByteArray(in)));
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new IOException("Failed to load public key", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean verifyJar(Path jarPath) throws IOException {
|
||||
PublicKey publickey = getPublicKey();
|
||||
|
||||
byte[] signature = null;
|
||||
Map<String, byte[]> fileFingerprints = new TreeMap<>();
|
||||
try (ZipFile zip = new ZipFile(jarPath.toFile())) {
|
||||
for (ZipEntry entry : zip.stream().toArray(ZipEntry[]::new)) {
|
||||
String filename = entry.getName();
|
||||
try (InputStream in = zip.getInputStream(entry)) {
|
||||
if (SIGNATURE_FILE.equals(filename)) {
|
||||
signature = IOUtils.readFullyAsByteArray(in);
|
||||
} else {
|
||||
fileFingerprints.put(filename, DigestUtils.digest("SHA-512", in));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (signature == null) {
|
||||
throw new IOException("Signature is missing");
|
||||
}
|
||||
|
||||
try {
|
||||
Signature verifier = Signature.getInstance("SHA512withRSA");
|
||||
verifier.initVerify(publickey);
|
||||
for (Entry<String, byte[]> entry : fileFingerprints.entrySet()) {
|
||||
verifier.update(DigestUtils.digest("SHA-512", entry.getKey().getBytes(UTF_8)));
|
||||
verifier.update(entry.getValue());
|
||||
}
|
||||
return verifier.verify(signature);
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new IOException("Failed to verify signature", e);
|
||||
}
|
||||
}
|
||||
|
||||
static void requireVerifiedJar(Path jar) throws IOException {
|
||||
if (!verifyJar(jar)) {
|
||||
throw new IOException("Invalid signature: " + jar);
|
||||
}
|
||||
}
|
||||
|
||||
private static Boolean selfVerified = null;
|
||||
|
||||
/**
|
||||
* Checks whether the current application is verified.
|
||||
* This method is blocking.
|
||||
*/
|
||||
public static synchronized boolean isSelfVerified() {
|
||||
if (selfVerified != null) {
|
||||
return selfVerified;
|
||||
}
|
||||
try {
|
||||
verifySelf();
|
||||
LOG.info("Successfully verified current JAR");
|
||||
selfVerified = true;
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.WARNING, "Failed to verify myself, is the JAR corrupt?", e);
|
||||
selfVerified = false;
|
||||
}
|
||||
return selfVerified;
|
||||
}
|
||||
|
||||
private static void verifySelf() throws IOException {
|
||||
Path self = LocalVersion.current().orElseThrow(() -> new IOException("Failed to find myself"))
|
||||
.getLocation();
|
||||
requireVerifiedJar(self);
|
||||
}
|
||||
}
|
@ -88,6 +88,7 @@ final class LocalRepository {
|
||||
}
|
||||
LOG.info("Downloading " + current.get());
|
||||
try {
|
||||
IntegrityChecker.requireVerifiedJar(current.get().getLocation());
|
||||
Files.createDirectories(localStorage.getParent());
|
||||
ExecutableHeaderHelper.copyWithoutHeader(current.get().getLocation(), localStorage);
|
||||
} catch (IOException e) {
|
||||
@ -105,6 +106,7 @@ final class LocalRepository {
|
||||
}
|
||||
|
||||
LOG.info("Applying update to " + target);
|
||||
IntegrityChecker.requireVerifiedJar(localStorage);
|
||||
ExecutableHeaderHelper.copyWithHeader(localStorage, target);
|
||||
}
|
||||
|
||||
|
@ -83,6 +83,10 @@ public final class UpdateChecker {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!IntegrityChecker.isSelfVerified()) {
|
||||
return;
|
||||
}
|
||||
|
||||
RemoteVersion fetched = RemoteVersion.fetch(source);
|
||||
Platform.runLater(() -> {
|
||||
if (source.equals(updateSource.get())) {
|
||||
|
@ -97,7 +97,7 @@ public final class UpdateHandler {
|
||||
|
||||
} else if (difference > 0) {
|
||||
Optional<LocalVersion> current = LocalVersion.current();
|
||||
if (current.isPresent()) {
|
||||
if (current.isPresent() && IntegrityChecker.isSelfVerified()) {
|
||||
try {
|
||||
requestUpdate(local.get().getLocation(), current.get().getLocation());
|
||||
} catch (IOException e) {
|
||||
@ -118,6 +118,7 @@ public final class UpdateHandler {
|
||||
}
|
||||
|
||||
private static void requestUpdate(Path updateTo, Path self) throws IOException {
|
||||
IntegrityChecker.requireVerifiedJar(updateTo);
|
||||
startJava(updateTo, "--apply-to", self.toString());
|
||||
}
|
||||
|
||||
@ -177,6 +178,9 @@ public final class UpdateHandler {
|
||||
if (!stored.isPresent()) {
|
||||
throw new IOException("Failed to find local repository, this shouldn't happen");
|
||||
}
|
||||
if (!IntegrityChecker.isSelfVerified()) {
|
||||
throw new IOException("Current JAR is not verified");
|
||||
}
|
||||
requestUpdate(stored.get().getLocation(), current.get().getLocation());
|
||||
System.exit(0);
|
||||
} catch (IOException e) {
|
||||
|
@ -22,6 +22,7 @@ import javafx.application.Platform;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.ui.CrashWindow;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageBox;
|
||||
import org.jackhuang.hmcl.upgrade.IntegrityChecker;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateChecker;
|
||||
|
||||
import static java.util.Collections.newSetFromMap;
|
||||
@ -107,7 +108,7 @@ public class CrashReporter implements Thread.UncaughtExceptionHandler {
|
||||
|
||||
if (checkThrowable(e)) {
|
||||
Platform.runLater(() -> new CrashWindow(text).show());
|
||||
if (!UpdateChecker.isOutdated()) {
|
||||
if (!UpdateChecker.isOutdated() && IntegrityChecker.isSelfVerified()) {
|
||||
reportToServer(text);
|
||||
}
|
||||
}
|
||||
|
BIN
HMCL/src/main/resources/assets/hmcl_signature_publickey.der
Normal file
BIN
HMCL/src/main/resources/assets/hmcl_signature_publickey.der
Normal file
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user