From 9fc681436622274883426204c85a97a0497824d1 Mon Sep 17 00:00:00 2001 From: Hatter Jiang Date: Sat, 11 Mar 2023 20:32:15 +0800 Subject: [PATCH] feat: v0.6.1, supports age decrypt --- .../tools/tinyencrypt/TinyEncryptMain.java | 7 +-- .../config/TinyEncryptConstant.java | 2 +- .../encrypt/EncryptedFileUtil.java | 29 ++++++++--- .../tools/tinyencrypt/util/AgeCliUtil.java | 51 +++++++++++++++---- 4 files changed, 69 insertions(+), 20 deletions(-) diff --git a/src/main/java/me/hatter/tools/tinyencrypt/TinyEncryptMain.java b/src/main/java/me/hatter/tools/tinyencrypt/TinyEncryptMain.java index eebe557..6fe051f 100644 --- a/src/main/java/me/hatter/tools/tinyencrypt/TinyEncryptMain.java +++ b/src/main/java/me/hatter/tools/tinyencrypt/TinyEncryptMain.java @@ -1,5 +1,6 @@ package me.hatter.tools.tinyencrypt; +import com.alibaba.fastjson.JSON; import me.hatter.tools.commons.bytes.Bytes; import me.hatter.tools.commons.exception.JumpOutException; import me.hatter.tools.commons.io.RFile; @@ -89,10 +90,10 @@ public class TinyEncryptMain { tinyEncryptArgs.comment, tinyEncryptArgs.encryptedComment); } else { if (tinyEncryptArgs.showInWindow || tinyEncryptArgs.editInWindow) { - EncryptedFileUtil.decryptInWindow(config, f, tinyEncryptArgs.pgp, tinyEncryptArgs.editInWindow); + EncryptedFileUtil.decryptInWindow(config, f, tinyEncryptArgs.pgp, tinyEncryptArgs.age, tinyEncryptArgs.editInWindow); encryptOrDecryptSuccess = false; // do not delete file } else if (tinyEncryptArgs.digest) { - final Bytes sha256 = EncryptedFileUtil.decryptAndDigest(config, f, tinyEncryptArgs.pgp); + final Bytes sha256 = EncryptedFileUtil.decryptAndDigest(config, f, tinyEncryptArgs.pgp, tinyEncryptArgs.age); if (sha256 != null) { log.info(sha256.asHex() + " - " + f); final File clearTextFile = EncryptedFileUtil.getDecryptFile(f); @@ -108,7 +109,7 @@ public class TinyEncryptMain { } encryptOrDecryptSuccess = false; // do not delete file } else { - encryptOrDecryptSuccess = EncryptedFileUtil.decryptFile(config, f, tinyEncryptArgs.pgp); + encryptOrDecryptSuccess = EncryptedFileUtil.decryptFile(config, f, tinyEncryptArgs.pgp, tinyEncryptArgs.age); } } if (encryptOrDecryptSuccess && tinyEncryptArgs.removeFile) { diff --git a/src/main/java/me/hatter/tools/tinyencrypt/config/TinyEncryptConstant.java b/src/main/java/me/hatter/tools/tinyencrypt/config/TinyEncryptConstant.java index d6a5ff4..0b4d52c 100644 --- a/src/main/java/me/hatter/tools/tinyencrypt/config/TinyEncryptConstant.java +++ b/src/main/java/me/hatter/tools/tinyencrypt/config/TinyEncryptConstant.java @@ -1,7 +1,7 @@ package me.hatter.tools.tinyencrypt.config; public class TinyEncryptConstant { - public static final String VERSION = "0.6.0"; + public static final String VERSION = "0.6.1"; public static final String ENC_FILE_EXT = ".tinyenc"; } diff --git a/src/main/java/me/hatter/tools/tinyencrypt/encrypt/EncryptedFileUtil.java b/src/main/java/me/hatter/tools/tinyencrypt/encrypt/EncryptedFileUtil.java index a75d378..42e13c4 100644 --- a/src/main/java/me/hatter/tools/tinyencrypt/encrypt/EncryptedFileUtil.java +++ b/src/main/java/me/hatter/tools/tinyencrypt/encrypt/EncryptedFileUtil.java @@ -17,6 +17,7 @@ import me.hatter.tools.commons.tlv.Tlv; import me.hatter.tools.commons.tlv.TlvUtil; import me.hatter.tools.tinyencrypt.config.TinyEncryptConfig; import me.hatter.tools.tinyencrypt.config.TinyEncryptConstant; +import me.hatter.tools.tinyencrypt.util.AgeCliUtil; import me.hatter.tools.tinyencrypt.util.CardCliUtil; import me.hatter.tools.tinyencrypt.util.NilOutputStream; import me.hatter.tools.tinyencrypt.util.SwingWindow; @@ -32,7 +33,10 @@ import java.util.zip.GZIPOutputStream; public class EncryptedFileUtil { private static final LogTool log = LogTools.getLogTool(EncryptedFileUtil.class); - public static boolean decryptToOutputStream(TinyEncryptConfig config, File file, OutputStream os, boolean pgp, AtomicReference metaRef) { + public static boolean decryptToOutputStream(TinyEncryptConfig config, File file, OutputStream os, + boolean pgp, + boolean age, + AtomicReference metaRef) { if (getDecryptFile(file) == null) { log.warn("File is not tinyenc file, skip: " + file); return false; @@ -70,6 +74,17 @@ public class EncryptedFileUtil { return false; } dataKey = dataKeyOpt.get(); + } else if (age) { + if (StringUtil.isBlank(meta.getAgeEnvelop())) { + log.error("File is not encrypted with Age envelop"); + return false; + } + final String ageCli = StringUtil.def(config.getAgeCli(), "age"); + final Optional dataKeyOpt = AgeCliUtil.decryptBytes(ageCli, meta.getAgeRecipient(), meta.getAgeEnvelop()); + if (!dataKeyOpt.isPresent()) { + return false; + } + dataKey = dataKeyOpt.get(); } else { dataKey = TinyEncryptMetaUtil.decryptDataKey(config, meta); } @@ -102,18 +117,18 @@ public class EncryptedFileUtil { } } - public static Bytes decryptAndDigest(TinyEncryptConfig config, File file, boolean pgp) { + public static Bytes decryptAndDigest(TinyEncryptConfig config, File file, boolean pgp, boolean age) { final DigestOutputStream outputStream = new DigestOutputStream(new NilOutputStream(), Digests.sha256()); - if (!decryptToOutputStream(config, file, outputStream, pgp, null)) { + if (!decryptToOutputStream(config, file, outputStream, pgp, age, null)) { return null; } return outputStream.digest(); } - public static void decryptInWindow(TinyEncryptConfig config, File file, boolean pgp, boolean editable) { + public static void decryptInWindow(TinyEncryptConfig config, File file, boolean pgp, boolean age, boolean editable) { final ByteArrayOutputStream baos = new ByteArrayOutputStream(); final AtomicReference metaRef = new AtomicReference<>(); - final boolean decryptSuccess = decryptToOutputStream(config, file, baos, pgp, metaRef); + final boolean decryptSuccess = decryptToOutputStream(config, file, baos, pgp, age, metaRef); if (!decryptSuccess) { return; } @@ -146,7 +161,7 @@ public class EncryptedFileUtil { } } - public static boolean decryptFile(TinyEncryptConfig config, File file, boolean pgp) { + public static boolean decryptFile(TinyEncryptConfig config, File file, boolean pgp, boolean age) { final File decFile = getDecryptFile(file); if (decFile == null) { log.warn("File is not tinyenc file, skip: " + decFile); @@ -160,7 +175,7 @@ public class EncryptedFileUtil { final AtomicReference meta = new AtomicReference<>(); final boolean decryptResult; try (FileOutputStream fos = new FileOutputStream(decFile)) { - decryptResult = decryptToOutputStream(config, file, fos, pgp, meta); + decryptResult = decryptToOutputStream(config, file, fos, pgp, age, meta); } if (!decryptResult) { if (decFile.length() == 0) { diff --git a/src/main/java/me/hatter/tools/tinyencrypt/util/AgeCliUtil.java b/src/main/java/me/hatter/tools/tinyencrypt/util/AgeCliUtil.java index 2fe8434..85864d0 100644 --- a/src/main/java/me/hatter/tools/tinyencrypt/util/AgeCliUtil.java +++ b/src/main/java/me/hatter/tools/tinyencrypt/util/AgeCliUtil.java @@ -1,34 +1,67 @@ package me.hatter.tools.tinyencrypt.util; import me.hatter.tools.commons.assertion.AssertUtil; +import me.hatter.tools.commons.io.RFile; +import me.hatter.tools.commons.log.LogTool; +import me.hatter.tools.commons.log.LogTools; import me.hatter.tools.commons.misc.Base64s; -import java.nio.charset.StandardCharsets; import java.util.Optional; public class AgeCliUtil { + private static final LogTool log = LogTools.getLogTool(AgeCliUtil.class); - public static void main(String[] args) { + public static void main(String[] args) throws Exception { System.out.println( - encryptBytes( - "age", - "age1yubikey1qtwna67eqmyu7q9s3mpf7lkkrqzdrnqazdfdjftmv2qercy0cdchc7jcpu5", - "hello world".getBytes(StandardCharsets.UTF_8) - ) + decryptBytes("age", "age1yubikey1qtwna67eqmyu7q9s3mpf7lkkrqzdrnqazdfdjftmv2qercy0cdchc7jcpu5", "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHBpdi1wMjU2IHFNWDVVUSBBNGMwMTl3\neXhQSXQ1dnhxSzFUNk4rNlJ6QkJCcnJqazZzTVAzcS9sSktyRQpBOXNQSVFudDBQ\nd2kyMFpySGxIUS8yemVwdkJxdEpoZHl3NTJuUzFaTjJJCi0tLSBwbW45LzUyem9j\nZWpROUFRT2huVDArS2hjaWFBeVd4S0xaTzh5eFo5QlhRCo2zDaVK/7YuOShVT0iw\n43LFOEP3T9v53YKhuUqjmm+6af93U2H/ppZVZluXrYPpR6+WHL4vdxflExP4yH1e\n8C+hFKSSdvxQe7cE8lHG\n-----END AGE ENCRYPTED FILE-----") ); } + public static Optional decryptBytes(String ageCli, String ageRecipient, String ageEnvelop) { + AssertUtil.notEmpty(ageCli, "Age-cli cannot be empty"); + AssertUtil.notEmpty(ageRecipient, "Age-recipient cannot be empty"); + AssertUtil.isTrue(ageRecipient.matches("^[a-zA-Z0-9]+$"), "Age-recipient illegal"); + AssertUtil.notEmpty(ageEnvelop, "Age-envelop cannot be empty"); + AssertUtil.isFalse(ageEnvelop.contains("'"), "Age-envelop cannot contains `'`"); + AssertUtil.isTrue(ageEnvelop.matches("^[\\-/+=\\n\\r\\sa-zA-Z0-9]+$"), "Age-envelop illegal"); + + final RFile recipientAgeFile = RFile.fromUserHome(".tinyencrypt/" + ageRecipient + ".age"); + AssertUtil.isTrue(recipientAgeFile.isFile(), "Age key file required"); + + final ProcessBuilder pb = new ProcessBuilder( + "sh", + "-c", + "echo '" + ageEnvelop + "' | " + ageCli + " -d -i " + recipientAgeFile.file().getAbsolutePath()); + + pb.redirectInput(ProcessBuilder.Redirect.PIPE); + pb.redirectError(ProcessBuilder.Redirect.PIPE); + + return CardCliUtil.runProcess(pb).map(b -> Base64s.normal().decode(b.trim())); + } + public static String encryptBytes(String ageCli, String ageRecipient, byte[] bytes) { AssertUtil.notEmpty(ageCli, "Age-cli cannot be empty"); AssertUtil.notEmpty(ageRecipient, "Age-recipient cannot be empty"); - AssertUtil.notNull(bytes, "Bytes cannot null"); + AssertUtil.notNull(bytes, "Bytes cannot be null"); final ProcessBuilder pb = new ProcessBuilder( "sh", - "-c", "echo " + Base64s.normal().encode(bytes) + " | " + ageCli + " -e -r " + ageRecipient + " -a"); + "-c", + "echo " + Base64s.normal().encode(bytes) + " | " + ageCli + " -e -r " + ageRecipient + " -a"); final Optional outputsOpt = CardCliUtil.runProcess(pb); if (!outputsOpt.isPresent()) { throw new RuntimeException("Encrypt use age failed!"); } return outputsOpt.get(); } + + + public static Optional readUserPin() { + System.out.print("Input PIV user PIN: "); + final char[] pin = System.console().readPassword(); + if (pin.length < 6) { + log.error("User PIN must have 6 letters"); + return Optional.empty(); + } + return Optional.of(new String(pin)); + } }