diff --git a/HMCL/build.gradle b/HMCL/build.gradle index 467d4fcd4..792d68e18 100644 --- a/HMCL/build.gradle +++ b/HMCL/build.gradle @@ -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) } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/IntegrityChecker.java b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/IntegrityChecker.java new file mode 100644 index 000000000..e6184b575 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/IntegrityChecker.java @@ -0,0 +1,130 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2018 huangyuhui + * + * 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 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 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); + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/LocalRepository.java b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/LocalRepository.java index 95aaa7336..6df548a27 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/LocalRepository.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/LocalRepository.java @@ -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); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateChecker.java b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateChecker.java index 6c8adee7d..3280cebad 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateChecker.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateChecker.java @@ -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())) { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateHandler.java b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateHandler.java index 5c414da7a..7ff5a7100 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateHandler.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateHandler.java @@ -97,7 +97,7 @@ public final class UpdateHandler { } else if (difference > 0) { Optional 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) { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/util/CrashReporter.java b/HMCL/src/main/java/org/jackhuang/hmcl/util/CrashReporter.java index 3b4a2d5f1..c08edfd74 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/util/CrashReporter.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/util/CrashReporter.java @@ -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); } } diff --git a/HMCL/src/main/resources/assets/hmcl_signature_publickey.der b/HMCL/src/main/resources/assets/hmcl_signature_publickey.der new file mode 100644 index 000000000..d01d9ea4c Binary files /dev/null and b/HMCL/src/main/resources/assets/hmcl_signature_publickey.der differ