mirror of
https://github.com/HMCL-dev/HMCL.git
synced 2025-09-15 14:56:05 -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.MessageDigest
|
||||||
|
import java.security.Signature
|
||||||
|
import java.security.spec.PKCS8EncodedKeySpec
|
||||||
|
import java.util.zip.ZipFile
|
||||||
|
|
||||||
def buildnumber = System.getenv("BUILD_NUMBER") ?: "SNAPSHOT"
|
def buildnumber = System.getenv("BUILD_NUMBER") ?: "SNAPSHOT"
|
||||||
def versionroot = System.getenv("VERSION_ROOT") ?: "3.1"
|
def versionroot = System.getenv("VERSION_ROOT") ?: "3.1"
|
||||||
@ -9,10 +14,37 @@ dependencies {
|
|||||||
compile rootProject.files("lib/JFoenix.jar")
|
compile rootProject.files("lib/JFoenix.jar")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def digest(String algorithm, byte[] bytes) {
|
||||||
|
return MessageDigest.getInstance(algorithm).digest(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
def createChecksum(File file) {
|
def createChecksum(File file) {
|
||||||
def algorithm = "SHA-1"
|
def algorithm = "SHA-1"
|
||||||
def suffix = "sha1"
|
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 {
|
jar {
|
||||||
@ -26,6 +58,7 @@ jar {
|
|||||||
}
|
}
|
||||||
|
|
||||||
doLast {
|
doLast {
|
||||||
|
attachSignature()
|
||||||
createChecksum(archivePath)
|
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());
|
LOG.info("Downloading " + current.get());
|
||||||
try {
|
try {
|
||||||
|
IntegrityChecker.requireVerifiedJar(current.get().getLocation());
|
||||||
Files.createDirectories(localStorage.getParent());
|
Files.createDirectories(localStorage.getParent());
|
||||||
ExecutableHeaderHelper.copyWithoutHeader(current.get().getLocation(), localStorage);
|
ExecutableHeaderHelper.copyWithoutHeader(current.get().getLocation(), localStorage);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
@ -105,6 +106,7 @@ final class LocalRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
LOG.info("Applying update to " + target);
|
LOG.info("Applying update to " + target);
|
||||||
|
IntegrityChecker.requireVerifiedJar(localStorage);
|
||||||
ExecutableHeaderHelper.copyWithHeader(localStorage, target);
|
ExecutableHeaderHelper.copyWithHeader(localStorage, target);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -83,6 +83,10 @@ public final class UpdateChecker {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!IntegrityChecker.isSelfVerified()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
RemoteVersion fetched = RemoteVersion.fetch(source);
|
RemoteVersion fetched = RemoteVersion.fetch(source);
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
if (source.equals(updateSource.get())) {
|
if (source.equals(updateSource.get())) {
|
||||||
|
@ -97,7 +97,7 @@ public final class UpdateHandler {
|
|||||||
|
|
||||||
} else if (difference > 0) {
|
} else if (difference > 0) {
|
||||||
Optional<LocalVersion> current = LocalVersion.current();
|
Optional<LocalVersion> current = LocalVersion.current();
|
||||||
if (current.isPresent()) {
|
if (current.isPresent() && IntegrityChecker.isSelfVerified()) {
|
||||||
try {
|
try {
|
||||||
requestUpdate(local.get().getLocation(), current.get().getLocation());
|
requestUpdate(local.get().getLocation(), current.get().getLocation());
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
@ -118,6 +118,7 @@ public final class UpdateHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static void requestUpdate(Path updateTo, Path self) throws IOException {
|
private static void requestUpdate(Path updateTo, Path self) throws IOException {
|
||||||
|
IntegrityChecker.requireVerifiedJar(updateTo);
|
||||||
startJava(updateTo, "--apply-to", self.toString());
|
startJava(updateTo, "--apply-to", self.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -177,6 +178,9 @@ public final class UpdateHandler {
|
|||||||
if (!stored.isPresent()) {
|
if (!stored.isPresent()) {
|
||||||
throw new IOException("Failed to find local repository, this shouldn't happen");
|
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());
|
requestUpdate(stored.get().getLocation(), current.get().getLocation());
|
||||||
System.exit(0);
|
System.exit(0);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
|
@ -22,6 +22,7 @@ import javafx.application.Platform;
|
|||||||
import org.jackhuang.hmcl.Metadata;
|
import org.jackhuang.hmcl.Metadata;
|
||||||
import org.jackhuang.hmcl.ui.CrashWindow;
|
import org.jackhuang.hmcl.ui.CrashWindow;
|
||||||
import org.jackhuang.hmcl.ui.construct.MessageBox;
|
import org.jackhuang.hmcl.ui.construct.MessageBox;
|
||||||
|
import org.jackhuang.hmcl.upgrade.IntegrityChecker;
|
||||||
import org.jackhuang.hmcl.upgrade.UpdateChecker;
|
import org.jackhuang.hmcl.upgrade.UpdateChecker;
|
||||||
|
|
||||||
import static java.util.Collections.newSetFromMap;
|
import static java.util.Collections.newSetFromMap;
|
||||||
@ -107,7 +108,7 @@ public class CrashReporter implements Thread.UncaughtExceptionHandler {
|
|||||||
|
|
||||||
if (checkThrowable(e)) {
|
if (checkThrowable(e)) {
|
||||||
Platform.runLater(() -> new CrashWindow(text).show());
|
Platform.runLater(() -> new CrashWindow(text).show());
|
||||||
if (!UpdateChecker.isOutdated()) {
|
if (!UpdateChecker.isOutdated() && IntegrityChecker.isSelfVerified()) {
|
||||||
reportToServer(text);
|
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