feat: secure editor

This commit is contained in:
2024-01-01 13:59:42 +08:00
parent 580dd4fe72
commit 6c6e4e4db6
3 changed files with 140 additions and 79 deletions

View File

@@ -1,19 +1,15 @@
apply plugin: 'java' apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'idea' apply plugin: 'idea'
def JsonSlurper = Class.forName('groovy.json.JsonSlurper'); def JsonSlurper = Class.forName('groovy.json.JsonSlurper');
def buildJSON = JsonSlurper.newInstance().parseText(new File("build.json").text) def buildJSON = JsonSlurper.newInstance().parseText(new File("build.json").text)
if (buildJSON.application) { apply plugin: 'application' }
def baseProjectName = buildJSON?.project?.name ?: '__project_name__'; 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' def jarManifestMainClass = buildJSON?.project?.main ?: 'SampleMain'
if (buildJSON.application) { mainClassName = jarManifestMainClass } if (buildJSON.application) {
mainClassName = jarManifestMainClass
}
archivesBaseName = buildJSON?.project?.archiveName ?: baseProjectName archivesBaseName = buildJSON?.project?.archiveName ?: baseProjectName
sourceCompatibility = 1.8 sourceCompatibility = 1.8
targetCompatibility = 1.8 targetCompatibility = 1.8
@@ -24,7 +20,9 @@ repositories {
mavenLocal() mavenLocal()
// mavenCentral() // mavenCentral()
maven() { url 'https://maven.aliyun.com/repository/central' } maven() { url 'https://maven.aliyun.com/repository/central' }
if (addRepo.exists()) { maven() { url addRepo.text.trim() } } if (addRepo.exists()) {
maven() { url addRepo.text.trim() }
}
} }
tasks.withType(JavaCompile) { tasks.withType(JavaCompile) {
@@ -45,9 +43,11 @@ buildscript {
classpath("org.springframework.boot:spring-boot-gradle-plugin:1.5.11.RELEASE") classpath("org.springframework.boot:spring-boot-gradle-plugin:1.5.11.RELEASE")
} }
} }
apply plugin: 'org.springframework.boot'
springBoot { jar {
mainClass = jarManifestMainClass manifest {
attributes 'Main-Class': jarManifestMainClass
}
} }
dependencies { 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')
}

View File

@@ -12,7 +12,6 @@
}, },
"repo": { "repo": {
"dependencies": [ "dependencies": [
"me.hatter:commons:3.70"
], ],
"testDependencies": [ "testDependencies": [
"junit:junit:4.12" "junit:junit:4.12"

View File

@@ -1,17 +1,23 @@
package me.hatter.tools.secureeditor; package me.hatter.tools.secureeditor;
import me.hatter.tools.commons.bytes.Bytes; import javax.crypto.Cipher;
import me.hatter.tools.commons.security.crypt.AESCryptTool; import javax.crypto.SecretKey;
import me.hatter.tools.commons.string.StringUtil; import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import javax.swing.*; import javax.swing.*;
import java.awt.*; import java.awt.*;
import java.awt.event.WindowListener; import java.awt.event.WindowListener;
import java.io.File; import java.io.*;
import java.lang.reflect.Proxy; import java.lang.reflect.Proxy;
import java.nio.charset.StandardCharsets;
import java.security.spec.AlgorithmParameterSpec;
import java.util.Arrays;
public class Main { 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 { public static void main(String[] args) throws InterruptedException {
if (args.length != 4) { if (args.length != 4) {
System.err.println("[ERROR] Arguments is not 4, actual is: " + args.length); 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.err.println("[ERROR] File is not exist or file: " + f);
System.exit(-1); 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.err.println("[ERROR] Algorithm is not aes-256-gcm.");
System.exit(-1); System.exit(-1);
} }
final byte[] keyBytes = Bytes.fromHex(key).bytes(); final byte[] keyBytes = string2Bytes(key);
final byte[] nonceBytes = Bytes.fromHex(nonce).bytes(); 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.getenv("READONLY"),
System.getProperty("readonly")) System.getProperty("readonly"))
); );
final FutureResult futureResult = showWindow("Secure edit file", final FutureResult futureResult = showWindow("Secure edit file",
decrypted.string(), new String(decrypted, StandardCharsets.UTF_8),
"File is encrypted in temp file", "File is encrypted in temp file",
!readonly); !readonly);
final String result = futureResult.getResult(); final String result = futureResult.getResult();
if (result != null) { 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) { public static FutureResult showWindow(String title, String text, String message, boolean editable) {
final FutureResult futureResult = new FutureResult(); 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( frame.addWindowListener((WindowListener) Proxy.newProxyInstance(
Proxy.class.getClassLoader(), Proxy.class.getClassLoader(),
new Class[]{WindowListener.class}, new Class[]{WindowListener.class},
(proxy, method, args) -> { (proxy, method, args) -> {
if ((method != null) && StringUtil.equals("windowClosing", method.getName())) { if ((method != null) && stringEquals("windowClosing", method.getName())) {
frame.setVisible(false); frame.setVisible(false);
frame.dispose(); frame.dispose();
futureResult.setResult(null); futureResult.setResult(null);
@@ -67,16 +76,16 @@ public class Main {
return null; return null;
})); }));
final int rows = Integer.parseInt(StringUtil.def(System.getenv("ROWS"), "30")); final int rows = Integer.parseInt(stringDefault(System.getenv("ROWS"), "30"));
final int columns = Integer.parseInt(StringUtil.def(System.getenv("COLUMNS"), "80")); final int columns = Integer.parseInt(stringDefault(System.getenv("COLUMNS"), "80"));
final JTextArea textArea = new JTextArea(StringUtil.def(text, ""), rows, columns); final JTextArea textArea = new JTextArea(stringDefault(text, ""), rows, columns);
textArea.setWrapStyleWord(true); textArea.setWrapStyleWord(true);
textArea.setLineWrap(true); textArea.setLineWrap(true);
textArea.setEditable(editable); textArea.setEditable(editable);
final JScrollPane textScrollPane = new JScrollPane(textArea); 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; JButton btnOK = null;
if (editable) { if (editable) {
@@ -108,10 +117,89 @@ public class Main {
frame.pack(); frame.pack();
final Dimension dim = Toolkit.getDefaultToolkit().getScreenSize(); 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); frame.setVisible(true);
return futureResult; 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();
}
} }