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