diff --git a/build.gradle b/build.gradle index abcb4ab..51e9cdb 100644 --- a/build.gradle +++ b/build.gradle @@ -1,19 +1,15 @@ apply plugin: 'java' -apply plugin: 'eclipse' apply plugin: 'idea' def JsonSlurper = Class.forName('groovy.json.JsonSlurper'); def buildJSON = JsonSlurper.newInstance().parseText(new File("build.json").text) -if (buildJSON.application) { apply plugin: 'application' } - def baseProjectName = buildJSON?.project?.name ?: '__project_name__'; -def shellCommandName = baseProjectName -def eclipseProjectName = baseProjectName -def eclipseProjectComment = buildJSON?.project?.comment ?: '__project_name_comment__' def jarManifestMainClass = buildJSON?.project?.main ?: 'SampleMain' -if (buildJSON.application) { mainClassName = jarManifestMainClass } +if (buildJSON.application) { + mainClassName = jarManifestMainClass +} archivesBaseName = buildJSON?.project?.archiveName ?: baseProjectName sourceCompatibility = 1.8 targetCompatibility = 1.8 @@ -21,14 +17,16 @@ targetCompatibility = 1.8 def addRepo = new File(System.getProperty("user.home"), ".build_add.repo") repositories { - mavenLocal() - // mavenCentral() - maven() { url 'https://maven.aliyun.com/repository/central' } - if (addRepo.exists()) { maven() { url addRepo.text.trim() } } + mavenLocal() + // mavenCentral() + maven() { url 'https://maven.aliyun.com/repository/central' } + if (addRepo.exists()) { + maven() { url addRepo.text.trim() } + } } tasks.withType(JavaCompile) { - options.encoding = "UTF-8" + options.encoding = "UTF-8" } // '-x test' skip unit test @@ -45,9 +43,11 @@ buildscript { classpath("org.springframework.boot:spring-boot-gradle-plugin:1.5.11.RELEASE") } } -apply plugin: 'org.springframework.boot' -springBoot { - mainClass = jarManifestMainClass + +jar { + manifest { + attributes 'Main-Class': jarManifestMainClass + } } dependencies { @@ -64,29 +64,3 @@ dependencies { } } } - -eclipse { - project { - name = eclipseProjectName - comment = eclipseProjectComment - } - classpath { - defaultOutputDir = file('classes') - downloadSources = true - file { - whenMerged { classpath -> - classpath.entries.findAll { it.kind=='lib' }.each { - if ((it.path != null) && (it.sourcePath == null) && file(it.path.replace(".jar", "-sources.jar")).exists()) { - it.sourcePath = getFileReferenceFactory().fromPath(it.path.replace(".jar", "-sources.jar")) - } - } - } - } - } -} - -eclipseJdt << { - File f = file('.settings/org.eclipse.core.resources.prefs') - f.write('eclipse.preferences.version=1\n') - f.append('encoding/=utf-8') -} diff --git a/build.json b/build.json index 621ac15..0b86e23 100644 --- a/build.json +++ b/build.json @@ -1,21 +1,20 @@ { - "project": { - "name": "secure-editor-java", - "main": "me.hatter.tools.secureeditor.Main", - "archiveName": "secure-editor" - }, - "application": false, - "java": "1.8", - "builder": { - "name": "gradle", - "version": "3.1" - }, - "repo": { - "dependencies": [ - "me.hatter:commons:3.70" - ], - "testDependencies": [ - "junit:junit:4.12" - ] - } + "project": { + "name": "secure-editor-java", + "main": "me.hatter.tools.secureeditor.Main", + "archiveName": "secure-editor" + }, + "application": false, + "java": "1.8", + "builder": { + "name": "gradle", + "version": "3.1" + }, + "repo": { + "dependencies": [ + ], + "testDependencies": [ + "junit:junit:4.12" + ] + } } diff --git a/src/main/java/me/hatter/tools/secureeditor/Main.java b/src/main/java/me/hatter/tools/secureeditor/Main.java index fe58dab..8140cc1 100644 --- a/src/main/java/me/hatter/tools/secureeditor/Main.java +++ b/src/main/java/me/hatter/tools/secureeditor/Main.java @@ -1,17 +1,23 @@ package me.hatter.tools.secureeditor; -import me.hatter.tools.commons.bytes.Bytes; -import me.hatter.tools.commons.security.crypt.AESCryptTool; -import me.hatter.tools.commons.string.StringUtil; - +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.SecretKeySpec; import javax.swing.*; import java.awt.*; import java.awt.event.WindowListener; -import java.io.File; +import java.io.*; import java.lang.reflect.Proxy; +import java.nio.charset.StandardCharsets; +import java.security.spec.AlgorithmParameterSpec; +import java.util.Arrays; public class Main { + public static final int GCM_NONCE_LENGTH = 12; + public static final int GCM_TAG_LENGTH = 16; + public static void main(String[] args) throws InterruptedException { if (args.length != 4) { System.err.println("[ERROR] Arguments is not 4, actual is: " + args.length); @@ -27,39 +33,42 @@ public class Main { System.err.println("[ERROR] File is not exist or file: " + f); System.exit(-1); } - if (!StringUtil.equals("aes-256-gcm", algorithm)) { + if (!stringEquals("aes-256-gcm", algorithm)) { System.err.println("[ERROR] Algorithm is not aes-256-gcm."); System.exit(-1); } - final byte[] keyBytes = Bytes.fromHex(key).bytes(); - final byte[] nonceBytes = Bytes.fromHex(nonce).bytes(); + final byte[] keyBytes = string2Bytes(key); + final byte[] nonceBytes = string2Bytes(nonce); - final Bytes decrypted = AESCryptTool.gcmDecrypt(keyBytes, nonceBytes).from(file).toBytes(); + final byte[] message = readFile(file); + final byte[] decrypted = gcmCrypt(keyBytes, nonceBytes, false, message); - final boolean readonly = StringUtil.isOn(StringUtil.def( + final boolean readonly = stringIsOn(stringDefault( System.getenv("READONLY"), System.getProperty("readonly")) ); final FutureResult futureResult = showWindow("Secure edit file", - decrypted.string(), + new String(decrypted, StandardCharsets.UTF_8), "File is encrypted in temp file", !readonly); final String result = futureResult.getResult(); if (result != null) { - AESCryptTool.gcmEncrypt(keyBytes, nonceBytes).from(Bytes.from(result)).to(file); + final byte[] resultBytes = result.getBytes(StandardCharsets.UTF_8); + final byte[] encrypted = gcmCrypt(keyBytes, nonceBytes, true, resultBytes); + writeFile(file, encrypted); } } public static FutureResult showWindow(String title, String text, String message, boolean editable) { final FutureResult futureResult = new FutureResult(); - final JFrame frame = new JFrame(StringUtil.def(title, "Not titled")); + final JFrame frame = new JFrame(stringDefault(title, "Not titled")); frame.addWindowListener((WindowListener) Proxy.newProxyInstance( Proxy.class.getClassLoader(), new Class[]{WindowListener.class}, (proxy, method, args) -> { - if ((method != null) && StringUtil.equals("windowClosing", method.getName())) { + if ((method != null) && stringEquals("windowClosing", method.getName())) { frame.setVisible(false); frame.dispose(); futureResult.setResult(null); @@ -67,16 +76,16 @@ public class Main { return null; })); - final int rows = Integer.parseInt(StringUtil.def(System.getenv("ROWS"), "30")); - final int columns = Integer.parseInt(StringUtil.def(System.getenv("COLUMNS"), "80")); - final JTextArea textArea = new JTextArea(StringUtil.def(text, ""), rows, columns); + final int rows = Integer.parseInt(stringDefault(System.getenv("ROWS"), "30")); + final int columns = Integer.parseInt(stringDefault(System.getenv("COLUMNS"), "80")); + final JTextArea textArea = new JTextArea(stringDefault(text, ""), rows, columns); textArea.setWrapStyleWord(true); textArea.setLineWrap(true); textArea.setEditable(editable); final JScrollPane textScrollPane = new JScrollPane(textArea); - final JLabel label = new JLabel(StringUtil.def(message, "This is default message!")); + final JLabel label = new JLabel(stringDefault(message, "This is default message!")); JButton btnOK = null; if (editable) { @@ -108,10 +117,89 @@ public class Main { frame.pack(); final Dimension dim = Toolkit.getDefaultToolkit().getScreenSize(); - frame.setLocation((dim.width / 2) - (frame.getSize().width / 2), (dim.height / 2) - (frame.getSize().height / 2)); + frame.setLocation( + (dim.width / 2) - (frame.getSize().width / 2), + (dim.height / 2) - (frame.getSize().height / 2) + ); frame.setVisible(true); return futureResult; } + + private static void writeFile(File file, byte[] bytes) { + try (FileOutputStream fos = new FileOutputStream(file)) { + fos.write(bytes); + } catch (IOException e) { + throw new RuntimeException("Write file: " + file + " failed: " + e.getMessage(), e); + } + } + + private static byte[] readFile(File file) { + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + final byte[] buffer = new byte[1024 * 8]; + try (FileInputStream fis = new FileInputStream(file)) { + final int len = fis.read(buffer); + if (len == -1) { + return baos.toByteArray(); + } + baos.write(buffer, 0, len); + } catch (IOException e) { + throw new RuntimeException("Read file: " + file + " failed: " + e.getMessage(), e); + } + return baos.toByteArray(); + } + + private static byte[] gcmCrypt(byte[] key, byte[] nonce, boolean isEncrypt, byte[] message) { + if (nonce.length < GCM_NONCE_LENGTH) { + throw new RuntimeException("GCM nonce's length cannot less than " + GCM_NONCE_LENGTH + " ."); + } + try { + final SecretKey secretKey = new SecretKeySpec(key, "AES"); + final AlgorithmParameterSpec parameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, nonce); + final Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); + cipher.init(isEncrypt ? Cipher.ENCRYPT_MODE : Cipher.DECRYPT_MODE, secretKey, parameterSpec); + return cipher.doFinal(message); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private static boolean stringEquals(final String str0, final String str1) { + if (str0 == str1) { + return true; + } + return ((str0 != null) && str0.equals(str1)); + } + + private static String stringDefault(String... strs) { + if (strs != null) { + for (String s : strs) { + if ((s != null) && (!s.isEmpty())) { + return s; + } + } + } + return null; + } + + private static boolean stringIsOn(String str) { + if (str == null) { + return false; + } + return Arrays.asList("true", "1", "on", "yes").contains(str.toLowerCase().trim()); + } + + private static byte[] string2Bytes(String str) { + int len = str.length(); + if ((len % 2) != 0) { + throw new RuntimeException("String format error: " + str); + } + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + for (int i = 0; i < len; i++) { + String substr = new String(new char[]{str.charAt(i++), str.charAt(i)}); + baos.write((byte) Integer.parseInt(substr, 16)); + } + return baos.toByteArray(); + } }