summary refs log tree commit diff stats
path: root/src/main/java/ganarchy/friendcode/util/KeyUtil.java
blob: 79a6a458f56e42c2eac5bfb1a8654084af7bfdb3 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
package ganarchy.friendcode.util;

import org.apache.commons.codec.binary.Base64;

import javax.crypto.*;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.*;
import java.util.Properties;

/**
 * Helper to deal with private keys.
 */
public class KeyUtil {
    // it is important to use encryption, as you don't want random users
    // hosting the world code. by encrypting the world code to an user, it
    // prevents that.
    // then you can freely share the world files and nothing bad happens.

    /**
     * Reads and decrypts a private key file.
     *
     * @param keyFile The path to the key file.
     * @return The decrypted private key, or null if it doesn't exist.
     */
    public static String readKeyFile(Path keyFile) {
        byte[] file;
        byte[] out;
        try {
            file = Files.readAllBytes(keyFile);
        } catch (IOException e) {
            return null;
        }
        if (file.length < 12 + 16 + 884) { // 12 (IV) + 16 (tag) + 884 (key)
            return null;
        }
        try {
            var c = Cipher.getInstance("AES/GCM/NoPadding");
            Key key = getGlobalKey();
            byte[] iv = new byte[12];
            System.arraycopy(file, 0, iv, 0, 12);
            c.init(Cipher.DECRYPT_MODE, key, new GCMParameterSpec(128, iv));
            out = c.doFinal(file, 12, file.length - 12);
        } catch (AEADBadTagException e) {
            return null;
        } catch (
            NoSuchAlgorithmException | NoSuchPaddingException
            | InvalidAlgorithmParameterException | InvalidKeyException
            | IllegalBlockSizeException | BadPaddingException e
        ) {
            throw new RuntimeException(e);
        }
        return StandardCharsets.UTF_8.decode(ByteBuffer.wrap(out)).toString();
    }


    /**
     * Encrypts and writes a private key file.
     *
     * @param keyFile    The path to the key file.
     * @param privateKey The private key.
     * @return Whether writing the key succeeded.
     */
    public static boolean writeKeyFile(Path keyFile, String privateKey) {
        byte[] in = privateKey.getBytes(StandardCharsets.UTF_8);
        byte[] iv = new byte[12];
        byte[] out;
        try {
            var c = Cipher.getInstance("AES/GCM/NoPadding");
            Key key = getGlobalKey();
            var rand = new SecureRandom();
            rand.nextBytes(iv);
            c.init(Cipher.ENCRYPT_MODE, key, new GCMParameterSpec(128, iv));
            out = c.doFinal(in, 0, in.length);
        } catch (
            NoSuchAlgorithmException | NoSuchPaddingException
            | InvalidAlgorithmParameterException | InvalidKeyException
            | IllegalBlockSizeException | BadPaddingException e
        ) {
            throw new RuntimeException(e);
        }
        try (var file = Files.newOutputStream(keyFile)) {
            file.write(iv);
            file.write(out);
            file.flush();
            return true;
        } catch (IOException e) {
            return false;
        }
    }

    /**
     * Returns the global key used for the encryption/decryption of world keys.
     */
    private static Key getGlobalKey() {
        Properties prop = new Properties();
        if (ConfigUtil.getSettings(prop)) {
            var key = prop.getProperty("key.aes");
            if (key != null) {
                byte[] bytes = null;
                try {
                    bytes = Base64.decodeBase64(key);
                } catch (IllegalArgumentException ignored) {
                }
                if (bytes != null && bytes.length == 16) {
                    return new SecretKeySpec(bytes, "AES");
                }
            }
        }
        prop.clear();
        try {
            var gen = KeyGenerator.getInstance("AES");
            gen.init(128);
            Key key = gen.generateKey();
            var encoded = Base64.encodeBase64String(key.getEncoded());
            prop.setProperty("key.aes", encoded);
            // if the key didn't save we'll just regen world codes.
            ConfigUtil.updateSettings(prop);
            return key;
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        }
    }
}