Add JAR integrity check

This commit is contained in:
yushijinhun 2018-07-31 19:39:21 +08:00
parent e38bfdfc73
commit dd45e1b3db
No known key found for this signature in database
GPG Key ID: 5BC167F73EA558E4
7 changed files with 177 additions and 3 deletions

View File

@ -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)
}
}

View File

@ -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);
}
}

View File

@ -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);
}

View File

@ -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())) {

View File

@ -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) {

View File

@ -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);
}
}