diff --git a/TinyEncryptSpecV1.1.md b/TinyEncryptSpecV1.1.md index 16e9669..071c392 100644 --- a/TinyEncryptSpecV1.1.md +++ b/TinyEncryptSpecV1.1.md @@ -24,6 +24,8 @@ Meta format: | pgpFingerprint | String | `deprecated` Hex(Sha256(PGP Publickey)) | | ageEnvelop | String | `deprecated` PGP Publickey Encrypted DataKey | | ageRecipient | String | `deprecated` age1*** | +| ecdhEnvelop | String | `deprecated` KW:*** | +| ecdhPoint | String | `deprecated` 02*** | | envelop | String | `deprecated` KMS Encrypted DataKey | | envelops | Envelop[] | Envelop Array | | nonce | String | `base64` GCM Nonce | diff --git a/build.json b/build.json index 3ff95c0..8a27e87 100644 --- a/build.json +++ b/build.json @@ -12,7 +12,7 @@ }, "repo": { "dependencies": [ - "me.hatter:commons:3.36", + "me.hatter:commons:3.56", "info.picocli:picocli:4.6.1", "org.bouncycastle:bcprov-jdk15on:1.69" ], diff --git a/src/main/java/me/hatter/tools/tinyencrypt/TinyEncryptArgs.java b/src/main/java/me/hatter/tools/tinyencrypt/TinyEncryptArgs.java index 35290a9..5568cc7 100644 --- a/src/main/java/me/hatter/tools/tinyencrypt/TinyEncryptArgs.java +++ b/src/main/java/me/hatter/tools/tinyencrypt/TinyEncryptArgs.java @@ -70,6 +70,9 @@ public class TinyEncryptArgs { @CommandLine.Option(names = {"-A", "--age"}, description = "Decrypt use Age") boolean age = false; + @CommandLine.Option(names = {"--ecdh"}, description = "Decrypt use ECDH") + boolean ecdh = false; + @CommandLine.Option(names = {"--use-jce"}, description = "Use JCE") boolean useJce = false; diff --git a/src/main/java/me/hatter/tools/tinyencrypt/TinyEncryptMain.java b/src/main/java/me/hatter/tools/tinyencrypt/TinyEncryptMain.java index 6fe051f..4023a52 100644 --- a/src/main/java/me/hatter/tools/tinyencrypt/TinyEncryptMain.java +++ b/src/main/java/me/hatter/tools/tinyencrypt/TinyEncryptMain.java @@ -90,10 +90,10 @@ public class TinyEncryptMain { tinyEncryptArgs.comment, tinyEncryptArgs.encryptedComment); } else { if (tinyEncryptArgs.showInWindow || tinyEncryptArgs.editInWindow) { - EncryptedFileUtil.decryptInWindow(config, f, tinyEncryptArgs.pgp, tinyEncryptArgs.age, tinyEncryptArgs.editInWindow); + EncryptedFileUtil.decryptInWindow(config, f, tinyEncryptArgs.pgp, tinyEncryptArgs.age, tinyEncryptArgs.ecdh, tinyEncryptArgs.editInWindow); encryptOrDecryptSuccess = false; // do not delete file } else if (tinyEncryptArgs.digest) { - final Bytes sha256 = EncryptedFileUtil.decryptAndDigest(config, f, tinyEncryptArgs.pgp, tinyEncryptArgs.age); + final Bytes sha256 = EncryptedFileUtil.decryptAndDigest(config, f, tinyEncryptArgs.pgp, tinyEncryptArgs.age, tinyEncryptArgs.ecdh); if (sha256 != null) { log.info(sha256.asHex() + " - " + f); final File clearTextFile = EncryptedFileUtil.getDecryptFile(f); @@ -109,7 +109,7 @@ public class TinyEncryptMain { } encryptOrDecryptSuccess = false; // do not delete file } else { - encryptOrDecryptSuccess = EncryptedFileUtil.decryptFile(config, f, tinyEncryptArgs.pgp, tinyEncryptArgs.age); + encryptOrDecryptSuccess = EncryptedFileUtil.decryptFile(config, f, tinyEncryptArgs.pgp, tinyEncryptArgs.age, tinyEncryptArgs.ecdh); } } if (encryptOrDecryptSuccess && tinyEncryptArgs.removeFile) { diff --git a/src/main/java/me/hatter/tools/tinyencrypt/TinyEncryptMainUtil.java b/src/main/java/me/hatter/tools/tinyencrypt/TinyEncryptMainUtil.java index d97f155..23a0580 100644 --- a/src/main/java/me/hatter/tools/tinyencrypt/TinyEncryptMainUtil.java +++ b/src/main/java/me/hatter/tools/tinyencrypt/TinyEncryptMainUtil.java @@ -186,6 +186,7 @@ public class TinyEncryptMainUtil { sb.append(header("Envelops")).append("KMS: ").append(toYesOrNo(meta.getEnvelop())) .append(", PGP: ").append(toYesOrNo(meta.getPgpEnvelop())) .append(", Age: ").append(toYesOrNo(meta.getAgeEnvelop())) + .append(", ECDH: ").append(toYesOrNo(meta.getEcdhEnvelop())) .append("\n"); if (StringUtil.isNotBlank(meta.getPgpFingerprint())) { sb.append(header("PGP fingerprint")).append(meta.getPgpFingerprint()).append("\n"); @@ -193,6 +194,9 @@ public class TinyEncryptMainUtil { if (StringUtil.isNotBlank(meta.getAgeRecipient())) { sb.append(header("Age recipient")).append(meta.getAgeRecipient()).append("\n"); } + if (StringUtil.isNotBlank(meta.getEcdhPoint())) { + sb.append(header("ECDH point")).append(meta.getEcdhPoint()).append("\n"); + } if (StringUtil.isNotBlank(meta.getComment())) { sb.append(header("Comment")).append(meta.getComment()).append("\n"); } diff --git a/src/main/java/me/hatter/tools/tinyencrypt/config/TinyEncryptConfig.java b/src/main/java/me/hatter/tools/tinyencrypt/config/TinyEncryptConfig.java index 01173ec..630b61b 100644 --- a/src/main/java/me/hatter/tools/tinyencrypt/config/TinyEncryptConfig.java +++ b/src/main/java/me/hatter/tools/tinyencrypt/config/TinyEncryptConfig.java @@ -10,6 +10,7 @@ public class TinyEncryptConfig { private String localPrivateKeyPemChallenge; private Boolean turnOffEnvelop; private String pgpEncryptPublicKeyPem; + private String ecdhPublicKeyPoint; @Deprecated private String pgpDecryptCmd; private String cardCli; @@ -73,6 +74,14 @@ public class TinyEncryptConfig { this.pgpEncryptPublicKeyPem = pgpEncryptPublicKeyPem; } + public String getEcdhPublicKeyPoint() { + return ecdhPublicKeyPoint; + } + + public void setEcdhPublicKeyPoint(String ecdhPublicKeyPoint) { + this.ecdhPublicKeyPoint = ecdhPublicKeyPoint; + } + public String getPgpDecryptCmd() { return pgpDecryptCmd; } 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 4d34b1f..4fca812 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.4"; + public static final String VERSION = "0.7.0"; 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 312540d..526318c 100644 --- a/src/main/java/me/hatter/tools/tinyencrypt/encrypt/EncryptedFileUtil.java +++ b/src/main/java/me/hatter/tools/tinyencrypt/encrypt/EncryptedFileUtil.java @@ -8,10 +8,14 @@ import me.hatter.tools.commons.io.IOUtil; 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 me.hatter.tools.commons.security.crypt.AESCryptTool; import me.hatter.tools.commons.security.crypt.CryptInputStream; import me.hatter.tools.commons.security.crypt.CryptOutputStream; +import me.hatter.tools.commons.security.crypt.WrapKeyUtil; import me.hatter.tools.commons.security.digest.Digests; +import me.hatter.tools.commons.security.key.KdfUtil; +import me.hatter.tools.commons.security.key.KeyUtil; import me.hatter.tools.commons.string.StringUtil; import me.hatter.tools.commons.tlv.Tlv; import me.hatter.tools.commons.tlv.TlvUtil; @@ -24,6 +28,9 @@ import me.hatter.tools.tinyencrypt.util.SwingWindow; import java.io.*; import java.nio.charset.StandardCharsets; +import java.security.PublicKey; +import java.security.interfaces.ECPublicKey; +import java.security.spec.ECPoint; import java.util.Date; import java.util.Optional; import java.util.concurrent.atomic.AtomicReference; @@ -36,6 +43,7 @@ public class EncryptedFileUtil { public static boolean decryptToOutputStream(TinyEncryptConfig config, File file, OutputStream os, boolean pgp, boolean age, + boolean ecdh, AtomicReference metaRef) { if (getDecryptFile(file) == null) { log.warn("File is not tinyenc file, skip: " + file); @@ -86,6 +94,27 @@ public class EncryptedFileUtil { return false; } dataKey = dataKeyOpt.get(); + } else if (ecdh) { + if (StringUtil.isBlank(meta.getEcdhEnvelop())) { + log.error("File is not encrypted with ECDH envelop"); + return false; + } + final Optional pinOpt = CardCliUtil.readUserPin(); + final String pin = pinOpt.orElse(null); + final WrapKeyUtil.WrapKey wrapKey = WrapKeyUtil.WrapKey.parse(meta.getEcdhEnvelop()); + final PublicKey ecPublicKey = KeyUtil.parsePublicKeyBytes(Base64s.uriCompatible().decode(wrapKey.getHeader().getePubKey())); + final ECPoint ecPoint = ((ECPublicKey) ecPublicKey).getW(); + final byte[] ecPointXBytes = ecPoint.getAffineX().toByteArray(); + final byte[] ecPointYBytes = ecPoint.getAffineY().toByteArray(); + final String ePublicKeyHex = "04" + + Bytes.from(ecPointXBytes).subBytes((ecPointXBytes.length == 33 && ecPointXBytes[0] == 0x00) ? 1 : 0).asHex() + + Bytes.from(ecPointYBytes).subBytes((ecPointYBytes.length == 33 && ecPointYBytes[0] == 0x00) ? 1 : 0).asHex(); + final Optional sharedSecretBytesOpt = CardCliUtil.ecdhSharedSecret(config.getCardCli(), pin, ePublicKeyHex); + if (!sharedSecretBytesOpt.isPresent()) { + return false; + } + final byte[] sharedSecretBytes = sharedSecretBytesOpt.get(); + dataKey = WrapKeyUtil.decryptEcdhP256((header) -> sharedSecretBytes, wrapKey); } else { dataKey = TinyEncryptMetaUtil.decryptDataKey(config, meta); } @@ -118,18 +147,18 @@ public class EncryptedFileUtil { } } - public static Bytes decryptAndDigest(TinyEncryptConfig config, File file, boolean pgp, boolean age) { + public static Bytes decryptAndDigest(TinyEncryptConfig config, File file, boolean pgp, boolean age, boolean ecdh) { final DigestOutputStream outputStream = new DigestOutputStream(new NilOutputStream(), Digests.sha256()); - if (!decryptToOutputStream(config, file, outputStream, pgp, age, null)) { + if (!decryptToOutputStream(config, file, outputStream, pgp, age, ecdh, null)) { return null; } return outputStream.digest(); } - public static void decryptInWindow(TinyEncryptConfig config, File file, boolean pgp, boolean age, boolean editable) { + public static void decryptInWindow(TinyEncryptConfig config, File file, boolean pgp, boolean age, boolean ecdh, boolean editable) { final ByteArrayOutputStream baos = new ByteArrayOutputStream(); final AtomicReference metaRef = new AtomicReference<>(); - final boolean decryptSuccess = decryptToOutputStream(config, file, baos, pgp, age, metaRef); + final boolean decryptSuccess = decryptToOutputStream(config, file, baos, pgp, age, ecdh, metaRef); if (!decryptSuccess) { return; } @@ -162,7 +191,7 @@ public class EncryptedFileUtil { } } - public static boolean decryptFile(TinyEncryptConfig config, File file, boolean pgp, boolean age) { + public static boolean decryptFile(TinyEncryptConfig config, File file, boolean pgp, boolean age, boolean ecdh) { final File decFile = getDecryptFile(file); if (decFile == null) { log.warn("File is not tinyenc file, skip: " + decFile); @@ -176,7 +205,7 @@ public class EncryptedFileUtil { final AtomicReference meta = new AtomicReference<>(); final boolean decryptResult; try (FileOutputStream fos = new FileOutputStream(decFile)) { - decryptResult = decryptToOutputStream(config, file, fos, pgp, age, meta); + decryptResult = decryptToOutputStream(config, file, fos, pgp, age, ecdh, meta); } if (!decryptResult) { if (decFile.length() == 0) { diff --git a/src/main/java/me/hatter/tools/tinyencrypt/encrypt/TinyEncryptMeta.java b/src/main/java/me/hatter/tools/tinyencrypt/encrypt/TinyEncryptMeta.java index 612c87f..c865178 100644 --- a/src/main/java/me/hatter/tools/tinyencrypt/encrypt/TinyEncryptMeta.java +++ b/src/main/java/me/hatter/tools/tinyencrypt/encrypt/TinyEncryptMeta.java @@ -10,6 +10,8 @@ public class TinyEncryptMeta { private String encryptedComment; private String pgpEnvelop; private String pgpFingerprint; + private String ecdhEnvelop; + private String ecdhPoint; private String ageEnvelop; private String ageRecipient; private String envelop; @@ -76,6 +78,22 @@ public class TinyEncryptMeta { this.pgpFingerprint = pgpFingerprint; } + public String getEcdhEnvelop() { + return ecdhEnvelop; + } + + public void setEcdhEnvelop(String ecdhEnvelop) { + this.ecdhEnvelop = ecdhEnvelop; + } + + public String getEcdhPoint() { + return ecdhPoint; + } + + public void setEcdhPoint(String ecdhPoint) { + this.ecdhPoint = ecdhPoint; + } + public String getAgeEnvelop() { return ageEnvelop; } diff --git a/src/main/java/me/hatter/tools/tinyencrypt/encrypt/TinyEncryptMetaUtil.java b/src/main/java/me/hatter/tools/tinyencrypt/encrypt/TinyEncryptMetaUtil.java index 495125e..8dc7eb7 100644 --- a/src/main/java/me/hatter/tools/tinyencrypt/encrypt/TinyEncryptMetaUtil.java +++ b/src/main/java/me/hatter/tools/tinyencrypt/encrypt/TinyEncryptMetaUtil.java @@ -9,7 +9,9 @@ import me.hatter.tools.commons.log.LogTools; import me.hatter.tools.commons.network.HttpRequest; import me.hatter.tools.commons.os.OSUtil; import me.hatter.tools.commons.security.crypt.AESCryptTool; +import me.hatter.tools.commons.security.crypt.WrapKeyUtil; import me.hatter.tools.commons.security.digest.Digests; +import me.hatter.tools.commons.security.ec.ECUtil; import me.hatter.tools.commons.security.key.KeyUtil; import me.hatter.tools.commons.security.random.RandomTool; import me.hatter.tools.commons.security.rsa.RSAUtil; @@ -22,6 +24,7 @@ import me.hatter.tools.tinyencrypt.util.CardCliUtil; import java.security.PrivateKey; import java.security.PublicKey; +import java.security.interfaces.ECPublicKey; import java.security.interfaces.RSAPublicKey; import java.util.ArrayList; import java.util.Base64; @@ -138,6 +141,15 @@ public class TinyEncryptMetaUtil { tinyEncryptMeta.setAgeEnvelop(ageEnvelop.trim()); tinyEncryptMeta.setAgeRecipient(config.getAgeRecipient()); } + if (StringUtil.isNotBlank(config.getEcdhPublicKeyPoint())) { + final PublicKey ecPubliecKey = ECUtil.getEcPublicKey(ECUtil.CURVE_SECP256R1, Bytes.fromHex(config.getEcdhPublicKeyPoint()).bytes()); + final byte[] ecPointXBytes = ((ECPublicKey) ecPubliecKey).getW().getAffineX().toByteArray(); + final boolean startsWith00AndLen33 = (ecPointXBytes.length == 33 && ecPointXBytes[0] == 0x00); + final String ecdhPoint = "02" + Bytes.from(ecPointXBytes).subBytes(startsWith00AndLen33 ? 1 : 0).asHex(); + final String ecdhEnvelop = WrapKeyUtil.encryptEcdhP256(null, ecPubliecKey, dataKey).toString(); + tinyEncryptMeta.setEcdhEnvelop(ecdhEnvelop); + tinyEncryptMeta.setEcdhPoint(ecdhPoint); + } tinyEncryptMeta.setNonce(RandomTool.secureRandom().nextbytes(12)); tinyEncryptMeta.setUserAgent("TinyEncrypt v" + TinyEncryptConstant.VERSION + "@" + OSUtil.getCurrentOS().name()); tinyEncryptMeta.setComment(comment); diff --git a/src/main/java/me/hatter/tools/tinyencrypt/util/CardCliUtil.java b/src/main/java/me/hatter/tools/tinyencrypt/util/CardCliUtil.java index 672c444..dc49edb 100644 --- a/src/main/java/me/hatter/tools/tinyencrypt/util/CardCliUtil.java +++ b/src/main/java/me/hatter/tools/tinyencrypt/util/CardCliUtil.java @@ -10,16 +10,17 @@ import me.hatter.tools.commons.log.LogTools; import me.hatter.tools.commons.string.StringUtil; import java.nio.charset.StandardCharsets; +import java.util.Arrays; import java.util.Optional; public class CardCliUtil { private static final LogTool log = LogTools.getLogTool(CardCliUtil.class); public static Optional readUserPin() { - System.out.print("Input PGP user PIN: "); + System.out.print("Please input user PIN: "); final char[] pin = System.console().readPassword(); if (pin.length < 6) { - log.error("User PIN must have 6 letters"); + log.warn("Input user PIN has " + pin.length + " char(s)"); return Optional.empty(); } return Optional.of(new String(pin)); @@ -80,6 +81,26 @@ public class CardCliUtil { return Optional.of(Bytes.fromHex(jo.getString("text_hex")).bytes()); } + public static Optional ecdhSharedSecret(String cardCli, String pin, String ePublicKeyPointHex) { + final ProcessBuilder pb = new ProcessBuilder( + cardCli, + "piv-ecdh", + "--private", + "--slot", "82", + "--epk", ePublicKeyPointHex, + "--json"); + if (StringUtil.isNotBlank(pin)) { + pb.command().addAll(Arrays.asList("--pin", pin)); + } + log.info("Start: " + cardCli); + final Optional outputsOpt = runProcess(pb); + if (!outputsOpt.isPresent()) { + return Optional.empty(); + } + final JSONObject jo = JSON.parseObject(outputsOpt.get()); + return Optional.of(Bytes.fromHex(jo.getString("shared_secret_hex")).bytes()); + } + public static Optional runProcess(ProcessBuilder pb) { Process p = null; try {