From 41627c277919499ba21827c98816189cb5cfd327 Mon Sep 17 00:00:00 2001 From: Hatter Jiang Date: Sat, 2 Dec 2023 12:15:05 +0800 Subject: [PATCH] feat: secure editor --- .gitignore | 62 ++-------- build.gradle | 92 ++++++++++++++ build.json | 21 ++++ .../tools/secureeditor/FutureResult.java | 23 ++++ .../me/hatter/tools/secureeditor/Main.java | 114 ++++++++++++++++++ 5 files changed, 259 insertions(+), 53 deletions(-) create mode 100644 build.gradle create mode 100644 build.json create mode 100644 src/main/java/me/hatter/tools/secureeditor/FutureResult.java create mode 100644 src/main/java/me/hatter/tools/secureeditor/Main.java diff --git a/.gitignore b/.gitignore index 5760a7a..94b7950 100644 --- a/.gitignore +++ b/.gitignore @@ -1,54 +1,10 @@ -# ---> Java -# Compiled class file -*.class - -# Log file -*.log - -# BlueJ files -*.ctxt - -# Mobile Tools for Java (J2ME) -.mtj.tmp/ - -# Package Files # -*.jar -*.war -*.nar -*.ear -*.zip -*.tar.gz -*.rar - -# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml -hs_err_pid* -replay_pid* - -# ---> macOS -# General +build +classes .DS_Store -.AppleDouble -.LSOverride - -# Icon must end with two \r -Icon - -# Thumbnails -._* - -# Files that might appear in the root of a volume -.DocumentRevisions-V100 -.fseventsd -.Spotlight-V100 -.TemporaryItems -.Trashes -.VolumeIcon.icns -.com.apple.timemachine.donotpresent - -# Directories potentially created on remote AFP share -.AppleDB -.AppleDesktop -Network Trash Folder -Temporary Items -.apdisk - +.gradle +.classpath +.project +.settings +*.iml +*.ipr +*.iws \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..abcb4ab --- /dev/null +++ b/build.gradle @@ -0,0 +1,92 @@ +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 } +archivesBaseName = buildJSON?.project?.archiveName ?: baseProjectName +sourceCompatibility = 1.8 +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() } } +} + +tasks.withType(JavaCompile) { + options.encoding = "UTF-8" +} + +// '-x test' skip unit test +defaultTasks 'build' + +buildscript { + repositories { + mavenLocal() + maven() { url 'https://maven.aliyun.com/repository/central' } + mavenCentral() + jcenter() + } + dependencies { + classpath("org.springframework.boot:spring-boot-gradle-plugin:1.5.11.RELEASE") + } +} +apply plugin: 'org.springframework.boot' +springBoot { + mainClass = jarManifestMainClass +} + +dependencies { + compile files(fileTree(dir: 'lib', includes: ['*.jar'], excludes: ['*-sources.jar', '*-javadoc.jar'])) + + if (buildJSON.repo != null && buildJSON.repo.dependencies != null) { + buildJSON.repo.dependencies.each { + compile("${it}") + } + } + if (buildJSON.repo != null && buildJSON.repo.testDependencies != null) { + buildJSON.repo.testDependencies.each { + testCompile("${it}") + } + } +} + +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 new file mode 100644 index 0000000..621ac15 --- /dev/null +++ b/build.json @@ -0,0 +1,21 @@ +{ + "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" + ] + } +} diff --git a/src/main/java/me/hatter/tools/secureeditor/FutureResult.java b/src/main/java/me/hatter/tools/secureeditor/FutureResult.java new file mode 100644 index 0000000..39eaeb9 --- /dev/null +++ b/src/main/java/me/hatter/tools/secureeditor/FutureResult.java @@ -0,0 +1,23 @@ +package me.hatter.tools.secureeditor; + +import java.util.concurrent.CountDownLatch; + +public class FutureResult { + private volatile String result; + private final CountDownLatch countDown; + + public FutureResult() { + this.result = null; + this.countDown = new CountDownLatch(1); + } + + public void setResult(String result) { + this.result = result; + countDown.countDown(); + } + + public String getResult() throws InterruptedException { + countDown.await(); + return result; + } +} diff --git a/src/main/java/me/hatter/tools/secureeditor/Main.java b/src/main/java/me/hatter/tools/secureeditor/Main.java new file mode 100644 index 0000000..87a4a99 --- /dev/null +++ b/src/main/java/me/hatter/tools/secureeditor/Main.java @@ -0,0 +1,114 @@ +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.swing.*; +import java.awt.*; +import java.awt.event.WindowListener; +import java.io.File; +import java.lang.reflect.Proxy; + + +public class Main { + public static void main(String[] args) throws InterruptedException { + if (args.length != 4) { + System.err.println("[ERROR] Arguments is not 4, actual is: " + args.length); + System.exit(-1); + } + final String f = args[0]; + final String algorithm = args[1]; + final String key = args[2]; + final String nonce = args[3]; + + final File file = new File(f); + if ((!file.exists()) || (!file.isFile())) { + System.err.println("[ERROR] File is not exist or file: " + f); + System.exit(-1); + } + if (!StringUtil.equals("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 Bytes decrypted = AESCryptTool.gcmDecrypt(keyBytes, nonceBytes).from(file).toBytes(); + + final boolean readonly = StringUtil.isOn(System.getProperty("readonly")); + final FutureResult futureResult = showWindow("Secure edit file", + decrypted.string(), + "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); + } + } + + + 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")); + frame.addWindowListener((WindowListener) Proxy.newProxyInstance( + Proxy.class.getClassLoader(), + new Class[]{WindowListener.class}, + (proxy, method, args) -> { + if ((method != null) && StringUtil.equals("windowClosing", method.getName())) { + frame.setVisible(false); + frame.dispose(); + futureResult.setResult(null); + } + 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); + 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!")); + + JButton btnOK = null; + if (editable) { + btnOK = new JButton("OK!"); + btnOK.addActionListener(e -> { + final String t = textArea.getText(); + frame.setVisible(false); + frame.dispose(); + futureResult.setResult(t); + }); + } + final JButton btnCancel = new JButton("Cancel"); + btnCancel.addActionListener(e -> { + frame.setVisible(false); + frame.dispose(); + futureResult.setResult(null); + }); + + final JPanel pane = new JPanel(); + if (btnOK != null) { + pane.add(btnOK); + } + pane.add(btnCancel); + + frame.getContentPane().add(label, BorderLayout.NORTH); + frame.getContentPane().add(pane, BorderLayout.SOUTH); + frame.getContentPane().add(textScrollPane, BorderLayout.CENTER); + + 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.setVisible(true); + + return futureResult; + } +}