Skip to content

Commit b6f7556

Browse files
committed
multiple changes
-redid how libraries work -added top level await
1 parent d561f10 commit b6f7556

File tree

6 files changed

+428
-45
lines changed

6 files changed

+428
-45
lines changed
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package de.blazemcworld.jsscripts;
2+
3+
import com.google.gson.JsonObject;
4+
import com.google.gson.JsonParser;
5+
import net.minecraft.util.Pair;
6+
7+
import java.nio.charset.StandardCharsets;
8+
import java.nio.file.Files;
9+
import java.nio.file.Path;
10+
import java.security.*;
11+
import java.security.spec.PKCS8EncodedKeySpec;
12+
import java.security.spec.X509EncodedKeySpec;
13+
import java.util.Base64;
14+
15+
public class Crypt {
16+
17+
private static final Path sigFile = JsScripts.MC.runDirectory.toPath().resolve("JsScripts").resolve("signature.json");
18+
19+
public static Pair<String, String> generateSignatureKeyPair() {
20+
try {
21+
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
22+
keyGen.initialize(512, new SecureRandom());
23+
KeyPair pair = keyGen.generateKeyPair();
24+
return new Pair<>(base64encode(pair.getPublic().getEncoded()), base64encode(pair.getPrivate().getEncoded()));
25+
} catch (Exception e) {
26+
throw new RuntimeException(e);
27+
}
28+
}
29+
30+
public static String base64encode(byte[] bytes) {
31+
return Base64.getEncoder().encodeToString(bytes);
32+
}
33+
34+
public static byte[] base64decode(String str) {
35+
return Base64.getDecoder().decode(str);
36+
}
37+
38+
public static String sign(String data, String privateKey) {
39+
try {
40+
Signature sig = Signature.getInstance("SHA256withRSA");
41+
sig.initSign(KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(base64decode(privateKey))));
42+
sig.update(data.replace("\r", "").getBytes(StandardCharsets.UTF_8));
43+
return base64encode(sig.sign());
44+
} catch (Exception e) {
45+
throw new RuntimeException(e);
46+
}
47+
}
48+
49+
public static boolean verify(String data, String signature, String publicKey) {
50+
try {
51+
Signature sig = Signature.getInstance("SHA256withRSA");
52+
sig.initVerify(KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(base64decode(publicKey))));
53+
sig.update(data.replace("\r", "").getBytes(StandardCharsets.UTF_8));
54+
return sig.verify(base64decode(signature));
55+
} catch (Exception e) {
56+
throw new RuntimeException(e);
57+
}
58+
}
59+
60+
public static String hash(String data) {
61+
try {
62+
MessageDigest md = MessageDigest.getInstance("MD5");
63+
md.update(data.replace("\r", "").getBytes(StandardCharsets.UTF_8));
64+
return base64encode(md.digest());
65+
} catch (Exception e) {
66+
throw new RuntimeException(e);
67+
}
68+
}
69+
70+
public static Pair<String, String> getKeyPair() {
71+
try {
72+
if (!Files.isRegularFile(sigFile)) {
73+
Pair<String, String> sig = generateSignatureKeyPair();
74+
JsonObject j = new JsonObject();
75+
j.addProperty("public_key", sig.getLeft());
76+
j.addProperty("private_key", sig.getRight());
77+
Files.writeString(sigFile, j.toString());
78+
}
79+
80+
JsonObject obj = JsonParser.parseString(Files.readString(sigFile)).getAsJsonObject();
81+
return new Pair<>(
82+
obj.get("public_key").getAsString(),
83+
obj.get("private_key").getAsString()
84+
);
85+
} catch (Exception e) {
86+
throw new RuntimeException(e);
87+
}
88+
}
89+
}

src/main/java/de/blazemcworld/jsscripts/JsScriptsCmd.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
import net.minecraft.text.Text;
66
import net.minecraft.util.Formatting;
77

8+
import java.nio.file.Files;
9+
import java.nio.file.Path;
10+
811
import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.argument;
912
import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.literal;
1013

@@ -17,6 +20,7 @@ public void register() {
1720
JsScripts.displayChat(Text.literal("/jsscripts reload - Reload all scripts").formatted(Formatting.AQUA));
1821
JsScripts.displayChat(Text.literal("/jsscripts gen_types - Generate .d.ts files").formatted(Formatting.AQUA));
1922
JsScripts.displayChat(Text.literal("/jsscripts list - List all currently enabled scripts.").formatted(Formatting.AQUA));
23+
JsScripts.displayChat(Text.literal("/jsscripts sign - Adds your signature to a script.").formatted(Formatting.AQUA));
2024
return 1;
2125
})
2226
.then(literal("reload")
@@ -62,6 +66,28 @@ public void register() {
6266
return 1;
6367
})
6468
)
69+
.then(literal("sign")
70+
.executes((e) -> {
71+
JsScripts.displayChat(Text.literal("Invalid usage! Usage:").formatted(Formatting.AQUA));
72+
JsScripts.displayChat(Text.literal("/jsscripts sign <script>").formatted(Formatting.AQUA));
73+
return 1;
74+
})
75+
.then(argument("script", StringArgumentType.string())
76+
.executes((e) -> {
77+
try {
78+
Path p = ScriptManager.scriptDir.toPath().resolve(e.getArgument("script", String.class));
79+
String src = Files.readString(p);
80+
src += "\n//SIGNED " + Crypt.sign(src.replaceAll("\\n?\\r?//SIGNED .+", ""), Crypt.getKeyPair().getRight());
81+
Files.writeString(p, src);
82+
JsScripts.displayChat(Text.literal("Signed script!").formatted(Formatting.AQUA));
83+
} catch (Exception err) {
84+
JsScripts.displayChat(Text.literal("Error signing script!").formatted(Formatting.AQUA));
85+
err.printStackTrace();
86+
}
87+
return 1;
88+
})
89+
)
90+
)
6591
));
6692
}
6793

src/main/java/de/blazemcworld/jsscripts/Script.java

Lines changed: 86 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@
88

99
import java.io.File;
1010
import java.nio.file.Files;
11+
import java.util.ArrayList;
12+
import java.util.HashMap;
13+
import java.util.List;
14+
import java.util.Map;
15+
import java.util.function.Consumer;
1116
import java.util.regex.Matcher;
1217
import java.util.regex.Pattern;
1318

@@ -19,18 +24,76 @@ public class Script {
1924

2025
@SuppressWarnings("CanBeFinal")
2126
public Runnable onDisable = null;
27+
private final String hash;
28+
private final Map<String, Value> exports = new HashMap<>();
29+
private final Map<String, List<Runnable>> exportCallbacks = new HashMap<>();
2230

23-
public Script(File file) throws Exception {
31+
public Script(File file, boolean trusted) throws Exception {
2432
this.file = file;
33+
34+
String rawSource = Files.readString(file.toPath());
35+
hash = Crypt.hash(rawSource);
36+
37+
String noSignatureSrc = rawSource.replaceAll("\\n?\\r?//SIGNED .+", "");
38+
String[] lines = rawSource.split("\n");
39+
40+
for (String line : lines) {
41+
if (trusted) {
42+
break;
43+
}
44+
if (line.startsWith("//SIGNED ")) {
45+
String signature = line.substring(9);
46+
if (ScriptManager.isTrusted(noSignatureSrc, signature.trim())) {
47+
trusted = true;
48+
}
49+
}
50+
}
51+
52+
if (!trusted) {
53+
throw new Exception("No trusted signature found in " + file);
54+
}
55+
2556
ctx = Context.newBuilder()
2657
.allowAllAccess(true)
2758
.logHandler(System.out)
2859
.build();
60+
61+
for (String line : lines) {
62+
if (line.startsWith("//DEPEND ")) {
63+
line = line.substring(9);
64+
String[] parts = line.trim().split(" ");
65+
if (parts.length <= 1) {
66+
ctx.close();
67+
throw new Exception("Invalid dependency comment.");
68+
}
69+
String libName = parts[0];
70+
String source = parts[1];
71+
72+
ScriptAccess pending = new ScriptAccess();
73+
ctx.getBindings("js").putMember(libName, pending);
74+
75+
source = ScriptManager.resolveAliases(source);
76+
77+
if (source.startsWith("file:")) {
78+
ScriptManager.addUnknown(source, null, pending::set, true);
79+
} else if (source.startsWith("https:")) {
80+
if (parts.length != 3) {
81+
ctx.close();
82+
throw new Exception("Invalid dependency comment.");
83+
}
84+
String hash = parts[2];
85+
ScriptManager.addUnknown(source, hash, pending::set, false);
86+
} else {
87+
throw new Exception("Invalid dependency source protocol.");
88+
}
89+
}
90+
}
91+
2992
Value bindings = ctx.getBindings("js");
3093
bindings.putMember("script", this);
3194

3295
StringBuilder source = new StringBuilder();
33-
Matcher m = importReplacePattern.matcher(Files.readString(file.toPath()));
96+
Matcher m = importReplacePattern.matcher(rawSource);
3497

3598
while (m.find()) {
3699
String className = m.group(3).replace('/', '.');
@@ -44,7 +107,7 @@ public Script(File file) throws Exception {
44107
}
45108
m.appendTail(source);
46109

47-
ctx.eval(Source.newBuilder("js", source.toString(), file.getName()).build());
110+
ctx.eval(Source.newBuilder("js", source.toString(), file.getName()).mimeType("application/javascript+module").build());
48111
}
49112

50113

@@ -74,11 +137,27 @@ public void debug(Object... objects) {
74137

75138
@SuppressWarnings("unused")
76139
public void export(String identifier, Value obj) {
77-
ScriptManager.saveValue(identifier, obj);
140+
exports.put(identifier, obj);
141+
if (exportCallbacks.containsKey(identifier)) {
142+
for (Runnable cb : exportCallbacks.get(identifier)) {
143+
cb.run();
144+
}
145+
exportCallbacks.remove(identifier);
146+
}
78147
}
79148

80-
@SuppressWarnings("unused")
81-
public Value load(String identifier) {
82-
return ScriptManager.loadValue(identifier, ctx);
149+
public void load(String identifier, Consumer<Value> cb) {
150+
if (exports.containsKey(identifier)) {
151+
cb.accept(exports.get(identifier));
152+
return;
153+
}
154+
if (!exportCallbacks.containsKey(identifier)) {
155+
exportCallbacks.put(identifier, new ArrayList<>());
156+
}
157+
exportCallbacks.get(identifier).add(() -> cb.accept(exports.get(identifier)));
158+
}
159+
160+
public String getHash() {
161+
return hash;
83162
}
84163
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package de.blazemcworld.jsscripts;
2+
3+
import org.graalvm.polyglot.Context;
4+
import org.graalvm.polyglot.Value;
5+
6+
import java.util.ArrayList;
7+
import java.util.List;
8+
import java.util.function.BiConsumer;
9+
import java.util.function.Consumer;
10+
import java.util.function.Function;
11+
12+
public class ScriptAccess {
13+
private final List<Consumer<Script>> waiting = new ArrayList<>();
14+
private Script value = null;
15+
16+
public void get(Consumer<Script> cb) {
17+
if (value != null) {
18+
cb.accept(value);
19+
return;
20+
}
21+
waiting.add(cb);
22+
}
23+
24+
public void set(Script s) {
25+
if (value != null) {
26+
throw new IllegalStateException("Script already set.");
27+
}
28+
value = s;
29+
for (Consumer<Script> w : waiting) {
30+
w.accept(value);
31+
}
32+
waiting.clear();
33+
}
34+
35+
@SuppressWarnings("unused")
36+
public Value load(String identifier) {
37+
return Context.getCurrent().getBindings("js").getMember("Promise")
38+
.newInstance((BiConsumer<Function<Object[], Object>, Function<Object[], Object>>)
39+
(resolve, reject) -> get(s -> s.load(identifier, (v) -> resolve.apply(new Object[]{v}))));
40+
}
41+
}

0 commit comments

Comments
 (0)