repositories / dotfiles
dotfiles
bugabingas dorkfiles
owned by admin
tools/aes.java
Rawimport static java.lang.System.err;
import static java.lang.System.out;
import java.io.ByteArrayOutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Base64;
import javax.crypto.Cipher;
import javax.crypto.Mac;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
class aes {
private static final byte[] MAGIC = new byte[] { 'D', 'F', 'A', 'E', 'S', '1' };
private static final int KEY_BYTES = 64;
private static final int AES_KEY_BYTES = 32;
private static final int IV_BYTES = 12;
private static final int GCM_TAG_BITS = 128;
private static final int HEADER_BYTES = MAGIC.length + IV_BYTES;
enum Command {
ENCRYPT, DECRYPT, MERGE
}
public static void main(String[] arguments) throws Exception {
try {
var exit = run(arguments);
if (exit != 0) {
System.exit(exit);
}
} catch (IllegalArgumentException problem) {
err.println(problem.getMessage());
System.exit(2);
} catch (Exception problem) {
err.println(problem.getMessage());
System.exit(1);
}
}
private static int run(String[] arguments) throws Exception {
if (arguments.length == 0) {
usage();
throw new IllegalArgumentException("missing command");
}
var command = parseCommand(arguments[0]);
var wrongArgumentCount = command == Command.MERGE ? arguments.length != 6 : arguments.length != 3 && arguments.length != 5;
if (wrongArgumentCount) {
usage();
throw new IllegalArgumentException("wrong argument count");
}
if (!"--key-file".equals(arguments[1])) {
usage();
throw new IllegalArgumentException("expected --key-file");
}
var key = readKey(Path.of(arguments[2]));
if (command == Command.MERGE) {
return merge(key, Path.of(arguments[3]), Path.of(arguments[4]), Path.of(arguments[5]));
}
var input = arguments.length == 3 ? System.in.readAllBytes() : Files.readAllBytes(Path.of(arguments[3]));
var output = command == Command.ENCRYPT ? encrypt(key, input) : decrypt(key, input);
if (arguments.length == 3) {
System.out.write(output);
} else {
var outputPath = Path.of(arguments[4]);
if (Files.exists(outputPath)) {
throw new IllegalArgumentException("output path already exists: " + outputPath);
}
try (var stream = Files.newOutputStream(outputPath)) {
stream.write(output);
}
}
return 0;
}
static int merge(byte[] key, Path base, Path current, Path other) throws Exception {
var temporary = Files.createTempDirectory("dotfiles-aes-merge-");
var plainBase = temporary.resolve("base");
var plainCurrent = temporary.resolve("current");
var plainOther = temporary.resolve("other");
try {
Files.write(plainBase, mergeInput(key, base));
Files.write(plainCurrent, mergeInput(key, current));
Files.write(plainOther, mergeInput(key, other));
var process = new ProcessBuilder("git", "merge-file", "-L", "current", "-L", "base", "-L", "other",
plainCurrent.toString(), plainBase.toString(), plainOther.toString()).inheritIO().start();
var exit = process.waitFor();
if (exit > 1) {
throw new IllegalStateException("git merge-file failed with exit " + exit);
}
Files.write(current, encrypt(key, Files.readAllBytes(plainCurrent)));
return exit;
} finally {
Files.deleteIfExists(plainBase);
Files.deleteIfExists(plainCurrent);
Files.deleteIfExists(plainOther);
Files.deleteIfExists(temporary);
}
}
private static byte[] mergeInput(byte[] key, Path path) throws Exception {
var input = Files.readAllBytes(path);
return input.length == 0 ? input : decrypt(key, input);
}
static byte[] encrypt(byte[] key, byte[] plaintext) throws Exception {
if (startsWith(plaintext, MAGIC)) {
throw new IllegalArgumentException("input is already a dotfiles AES blob");
}
var split = splitKey(key);
var iv = deterministicIv(split.macKey(), plaintext);
var header = header(iv);
var cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(split.encryptionKey(), "AES"), new GCMParameterSpec(GCM_TAG_BITS, iv));
cipher.updateAAD(header);
var ciphertext = cipher.doFinal(plaintext);
var output = new ByteArrayOutputStream(header.length + ciphertext.length);
output.write(header);
output.write(ciphertext);
return output.toByteArray();
}
static byte[] decrypt(byte[] key, byte[] encrypted) throws Exception {
if (!startsWith(encrypted, MAGIC)) {
throw new IllegalArgumentException("input is not a dotfiles AES blob");
}
if (encrypted.length < HEADER_BYTES + 1) {
throw new IllegalArgumentException("encrypted blob is truncated");
}
var iv = Arrays.copyOfRange(encrypted, MAGIC.length, HEADER_BYTES);
var header = Arrays.copyOfRange(encrypted, 0, HEADER_BYTES);
var ciphertext = Arrays.copyOfRange(encrypted, HEADER_BYTES, encrypted.length);
var split = splitKey(key);
var cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(split.encryptionKey(), "AES"), new GCMParameterSpec(GCM_TAG_BITS, iv));
cipher.updateAAD(header);
return cipher.doFinal(ciphertext);
}
static boolean startsWith(byte[] bytes, byte[] prefix) {
if (bytes.length < prefix.length) {
return false;
}
for (var index = 0; index < prefix.length; index++) {
if (bytes[index] != prefix[index]) {
return false;
}
}
return true;
}
private static Command parseCommand(String input) {
if ("encrypt".startsWith(input)) {
return Command.ENCRYPT;
}
if ("decrypt".startsWith(input)) {
return Command.DECRYPT;
}
if ("merge".startsWith(input)) {
return Command.MERGE;
}
throw new IllegalArgumentException("unknown command: " + input);
}
private static byte[] readKey(Path path) throws Exception {
if (!Files.exists(path)) {
throw new IllegalArgumentException("key file does not exist: " + path);
}
var text = Files.readString(path).trim();
var key = Base64.getDecoder().decode(text);
if (key.length != KEY_BYTES) {
throw new IllegalArgumentException("key file must contain " + KEY_BYTES + " base64-encoded bytes");
}
return key;
}
private static SplitKey splitKey(byte[] key) {
return new SplitKey(
Arrays.copyOfRange(key, 0, AES_KEY_BYTES),
Arrays.copyOfRange(key, AES_KEY_BYTES, KEY_BYTES));
}
private static byte[] deterministicIv(byte[] macKey, byte[] plaintext) throws Exception {
var mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(macKey, "HmacSHA256"));
return Arrays.copyOf(mac.doFinal(plaintext), IV_BYTES);
}
private static byte[] header(byte[] iv) throws Exception {
var output = new ByteArrayOutputStream(HEADER_BYTES);
output.write(MAGIC);
output.write(iv);
return output.toByteArray();
}
private static void usage() {
out.println("Symmetrically encrypts/decrypts dotfiles data.");
out.println("Usage for streams: java tools/aes.java <encrypt|decrypt> --key-file <path>");
out.println("Usage for files: java tools/aes.java <encrypt|decrypt> --key-file <path> <input> <output>");
out.println("Usage for merges: java tools/aes.java merge --key-file <path> <base> <current> <other>");
}
static class SplitKey {
private final byte[] encryptionKey;
private final byte[] macKey;
SplitKey(byte[] encryptionKey, byte[] macKey) {
this.encryptionKey = encryptionKey;
this.macKey = macKey;
}
byte[] encryptionKey() {
return encryptionKey;
}
byte[] macKey() {
return macKey;
}
}
}