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: '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
@@ -24,7 +20,9 @@ repositories {
mavenLocal()
// mavenCentral()
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) {
@@ -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')
}

View File

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

View File

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