|
3 | 3 | import cn.nukkit.Server;
|
4 | 4 | import cn.nukkit.math.Mth;
|
5 | 5 | import cn.nukkit.resourcepacks.PackManifest.Module;
|
6 |
| - |
| 6 | +import cn.nukkit.utils.Hash; |
| 7 | +import cn.nukkit.utils.JsonUtil; |
| 8 | +import com.fasterxml.jackson.databind.JsonNode; |
| 9 | +import com.google.common.io.Files; |
| 10 | +import lombok.extern.log4j.Log4j2; |
| 11 | +import org.apache.commons.lang3.RandomStringUtils; |
| 12 | + |
| 13 | +import javax.crypto.Cipher; |
| 14 | +import javax.crypto.spec.IvParameterSpec; |
| 15 | +import javax.crypto.spec.SecretKeySpec; |
| 16 | +import java.io.ByteArrayOutputStream; |
| 17 | +import java.io.DataOutputStream; |
7 | 18 | import java.io.File;
|
8 | 19 | import java.io.IOException;
|
9 |
| -import java.nio.file.Files; |
| 20 | +import java.nio.charset.StandardCharsets; |
| 21 | +import java.nio.file.attribute.FileTime; |
10 | 22 | import java.security.MessageDigest;
|
11 | 23 | import java.security.NoSuchAlgorithmException;
|
12 |
| -import java.util.Arrays; |
13 |
| -import java.util.Optional; |
| 24 | +import java.util.*; |
| 25 | +import java.util.zip.Deflater; |
14 | 26 | import java.util.zip.ZipEntry;
|
15 | 27 | import java.util.zip.ZipFile;
|
| 28 | +import java.util.zip.ZipOutputStream; |
16 | 29 |
|
17 | 30 | import static cn.nukkit.SharedConstants.*;
|
18 | 31 |
|
| 32 | +@Log4j2 |
19 | 33 | public class ZippedResourcePack extends AbstractResourcePack {
|
20 | 34 | private final int size;
|
21 | 35 | private final byte[][] chunks;
|
22 | 36 | private final byte[] sha256;
|
23 | 37 |
|
| 38 | + private final String encryptionKey; |
| 39 | + |
24 | 40 | public ZippedResourcePack(File file) throws IOException {
|
| 41 | + this(file, true); |
| 42 | + } |
| 43 | + |
| 44 | + public ZippedResourcePack(File file, boolean encrypt) throws IOException { |
25 | 45 | if (!file.exists()) {
|
26 | 46 | throw new IllegalArgumentException(Server.getInstance().getLanguage()
|
27 | 47 | .translate("nukkit.resources.zip.not-found", file.getName()));
|
28 | 48 | }
|
29 | 49 |
|
| 50 | + byte[] bytes = Files.toByteArray(file); |
30 | 51 | try (ZipFile zip = new ZipFile(file)) {
|
31 |
| - ZipEntry entry = Optional.ofNullable(zip.getEntry("manifest.json")) |
| 52 | + ZipEntry manifestEntry = Optional.ofNullable(zip.getEntry("manifest.json")) |
32 | 53 | .orElse(zip.getEntry("pack_manifest.json"));
|
33 | 54 |
|
34 |
| - if (entry == null) { |
| 55 | + if (manifestEntry == null) { |
35 | 56 | throw new IllegalArgumentException(Server.getInstance().getLanguage()
|
36 | 57 | .translate("nukkit.resources.zip.no-manifest"));
|
37 | 58 | }
|
38 | 59 |
|
39 |
| - manifest = PackManifest.load(zip.getInputStream(entry)); |
40 |
| - } |
| 60 | + manifest = PackManifest.load(zip.getInputStream(manifestEntry)); |
41 | 61 |
|
42 |
| - if (!manifest.isValid()) { |
43 |
| - throw new IllegalArgumentException(Server.getInstance().getLanguage() |
44 |
| - .translate("nukkit.resources.zip.invalid-manifest")); |
45 |
| - } |
46 |
| - |
47 |
| - id = manifest.getHeader().getUuid().toString(); |
48 |
| - version = manifest.getHeader().getVersion().toString(); |
49 |
| - type = manifest.getModules().stream() |
50 |
| - .findFirst() |
51 |
| - .map(Module::getType) |
52 |
| - .orElse("resources"); |
| 62 | + if (!manifest.isValid()) { |
| 63 | + throw new IllegalArgumentException(Server.getInstance().getLanguage() |
| 64 | + .translate("nukkit.resources.zip.invalid-manifest")); |
| 65 | + } |
53 | 66 |
|
54 |
| - byte[] bytes = Files.readAllBytes(file.toPath()); |
| 67 | + id = manifest.getHeader().getUuid().toString(); |
| 68 | + version = manifest.getHeader().getVersion().toString(); |
| 69 | + type = manifest.getModules().stream() |
| 70 | + .findFirst() |
| 71 | + .map(Module::getType) |
| 72 | + .orElse("resources"); |
| 73 | + |
| 74 | + String encryptionKey; |
| 75 | + if (RESOURCE_PACK_ENCRYPTION && encrypt) { |
| 76 | + Random random = new Random(Hash.xxh64(bytes)); |
| 77 | + encryptionKey = generateToken(random); |
| 78 | + |
| 79 | + FileTime time = FileTime.fromMillis(0); |
| 80 | + ByteArrayOutputStream baos = new ByteArrayOutputStream(); |
| 81 | + try (ZipOutputStream zos = new ZipOutputStream(baos)) { |
| 82 | + zos.setLevel(Deflater.BEST_COMPRESSION); |
| 83 | + |
| 84 | + List<Map<String, String>> pairs = new ArrayList<>(); |
| 85 | + Iterator<? extends ZipEntry> iterator = zip.entries().asIterator(); |
| 86 | + while (iterator.hasNext()) { |
| 87 | + ZipEntry entry = iterator.next(); |
| 88 | + String name = entry.getName().replace('\\', '/'); |
| 89 | + byte[] content = zip.getInputStream(entry).readAllBytes(); |
| 90 | + |
| 91 | + if (name.endsWith(".json") || name.endsWith(".material")) { |
| 92 | + try { |
| 93 | + JsonNode root = JsonUtil.TRUSTED_JSON_MAPPER.readTree(content); |
| 94 | + // minimize JSON file content |
| 95 | + content = JsonUtil.TRUSTED_JSON_MAPPER.writeValueAsBytes(root); |
| 96 | + } catch (Exception ignored) { |
| 97 | + } |
| 98 | + } else if (name.startsWith("subpacks/")) { |
| 99 | + //TODO: sub-packs |
| 100 | + } |
| 101 | + |
| 102 | + byte[] data; |
| 103 | + if (!"manifest.json".equalsIgnoreCase(name) |
| 104 | + && !"pack_icon.png".equalsIgnoreCase(name) |
| 105 | + && !"bug_pack_icon.png".equalsIgnoreCase(name)) { |
| 106 | + String token = generateToken(random); |
| 107 | + byte[] encodedToken = token.getBytes(StandardCharsets.UTF_8); |
| 108 | + SecretKeySpec secretKey = new SecretKeySpec(encodedToken, "AES"); |
| 109 | + Cipher cipher = Cipher.getInstance("AES/CFB8/NoPadding"); |
| 110 | + cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(Arrays.copyOf(encodedToken, 16))); |
| 111 | + data = cipher.doFinal(content); |
| 112 | + |
| 113 | + Map<String, String> pair = new HashMap<>(); |
| 114 | + pair.put("path", name); |
| 115 | + pair.put("key", token); |
| 116 | + pairs.add(pair); |
| 117 | + } else { |
| 118 | + data = content; |
| 119 | + |
| 120 | + pairs.add(Collections.singletonMap("path", name)); |
| 121 | + } |
| 122 | + |
| 123 | + zos.putNextEntry(new ZipEntry(entry) |
| 124 | + .setCreationTime(time) |
| 125 | + .setLastModifiedTime(time) |
| 126 | + .setLastAccessTime(time)); |
| 127 | + zos.write(data); |
| 128 | + zos.closeEntry(); |
| 129 | + } |
| 130 | + |
| 131 | + zos.putNextEntry(new ZipEntry("contents.json") |
| 132 | + .setCreationTime(time) |
| 133 | + .setLastModifiedTime(time) |
| 134 | + .setLastAccessTime(time)); |
| 135 | + |
| 136 | + DataOutputStream dos = new DataOutputStream(zos); |
| 137 | + dos.writeInt(0); // version |
| 138 | + dos.writeInt(0xfcb9cf9b); // magic |
| 139 | + dos.writeLong(0); |
| 140 | + |
| 141 | + byte[] encodedId = id.getBytes(StandardCharsets.UTF_8); |
| 142 | + int length = encodedId.length; |
| 143 | + zos.write(length); |
| 144 | + zos.write(encodedId); |
| 145 | + zos.write(new byte[256 - (4 + 4 + 8 + 1 + length)]); |
| 146 | + |
| 147 | + byte[] encodedKey = encryptionKey.getBytes(StandardCharsets.UTF_8); |
| 148 | + SecretKeySpec secretKey = new SecretKeySpec(encodedKey, "AES"); |
| 149 | + Cipher cipher = Cipher.getInstance("AES/CFB8/NoPadding"); |
| 150 | + cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(Arrays.copyOf(encodedKey, 16))); |
| 151 | + String json = JsonUtil.TRUSTED_JSON_MAPPER.writeValueAsString(Collections.singletonMap("content", pairs)); |
| 152 | + zos.write(cipher.doFinal(json.getBytes(StandardCharsets.UTF_8))); |
| 153 | + |
| 154 | + zos.closeEntry(); |
| 155 | + |
| 156 | + zos.finish(); |
| 157 | + bytes = baos.toByteArray(); |
| 158 | + } catch (Exception e) { |
| 159 | + encryptionKey = ""; |
| 160 | + log.error("Unable to encrypt mcpack", e); |
| 161 | + } |
| 162 | + } else { |
| 163 | + encryptionKey = ""; |
| 164 | + } |
| 165 | + this.encryptionKey = encryptionKey; |
| 166 | + } |
55 | 167 | size = bytes.length;
|
56 | 168 |
|
57 | 169 | int count = Mth.ceil(size / (float) RESOURCE_PACK_CHUNK_SIZE);
|
@@ -92,4 +204,13 @@ public byte[] getPackChunk(int index) {
|
92 | 204 |
|
93 | 205 | return chunks[index];
|
94 | 206 | }
|
| 207 | + |
| 208 | + @Override |
| 209 | + public String getEncryptionKey() { |
| 210 | + return this.encryptionKey; |
| 211 | + } |
| 212 | + |
| 213 | + private static String generateToken(Random random) { |
| 214 | + return RandomStringUtils.random(32, 0, 0, true, true, null, random); |
| 215 | + } |
95 | 216 | }
|
0 commit comments