feat: v0.7.0, add ecdh envelop

This commit is contained in:
2023-04-01 16:27:24 +08:00
parent 3f58992680
commit e3eb182873
11 changed files with 111 additions and 13 deletions

View File

@@ -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;

View File

@@ -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) {

View File

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

View File

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

View File

@@ -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";
}

View File

@@ -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<TinyEncryptMeta> 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<String> 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<byte[]> 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<TinyEncryptMeta> 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<TinyEncryptMeta> 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) {

View File

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

View File

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

View File

@@ -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<String> 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<byte[]> 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<String> 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<String> runProcess(ProcessBuilder pb) {
Process p = null;
try {