feat: secure editor
This commit is contained in:
56
build.gradle
56
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/<project>=utf-8')
|
||||
}
|
||||
|
||||
37
build.json
37
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"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user