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

@@ -24,6 +24,8 @@ Meta format:
| pgpFingerprint | String | `deprecated` Hex(Sha256(PGP Publickey)) | | pgpFingerprint | String | `deprecated` Hex(Sha256(PGP Publickey)) |
| ageEnvelop | String | `deprecated` PGP Publickey Encrypted DataKey | | ageEnvelop | String | `deprecated` PGP Publickey Encrypted DataKey |
| ageRecipient | String | `deprecated` age1*** | | ageRecipient | String | `deprecated` age1*** |
| ecdhEnvelop | String | `deprecated` KW:*** |
| ecdhPoint | String | `deprecated` 02*** |
| envelop | String | `deprecated` KMS Encrypted DataKey | | envelop | String | `deprecated` KMS Encrypted DataKey |
| envelops | Envelop[] | Envelop Array | | envelops | Envelop[] | Envelop Array |
| nonce | String | `base64` GCM Nonce | | nonce | String | `base64` GCM Nonce |

View File

@@ -12,7 +12,7 @@
}, },
"repo": { "repo": {
"dependencies": [ "dependencies": [
"me.hatter:commons:3.36", "me.hatter:commons:3.56",
"info.picocli:picocli:4.6.1", "info.picocli:picocli:4.6.1",
"org.bouncycastle:bcprov-jdk15on:1.69" "org.bouncycastle:bcprov-jdk15on:1.69"
], ],

View File

@@ -70,6 +70,9 @@ public class TinyEncryptArgs {
@CommandLine.Option(names = {"-A", "--age"}, description = "Decrypt use Age") @CommandLine.Option(names = {"-A", "--age"}, description = "Decrypt use Age")
boolean age = false; boolean age = false;
@CommandLine.Option(names = {"--ecdh"}, description = "Decrypt use ECDH")
boolean ecdh = false;
@CommandLine.Option(names = {"--use-jce"}, description = "Use JCE") @CommandLine.Option(names = {"--use-jce"}, description = "Use JCE")
boolean useJce = false; boolean useJce = false;

View File

@@ -90,10 +90,10 @@ public class TinyEncryptMain {
tinyEncryptArgs.comment, tinyEncryptArgs.encryptedComment); tinyEncryptArgs.comment, tinyEncryptArgs.encryptedComment);
} else { } else {
if (tinyEncryptArgs.showInWindow || tinyEncryptArgs.editInWindow) { 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 encryptOrDecryptSuccess = false; // do not delete file
} else if (tinyEncryptArgs.digest) { } 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) { if (sha256 != null) {
log.info(sha256.asHex() + " - " + f); log.info(sha256.asHex() + " - " + f);
final File clearTextFile = EncryptedFileUtil.getDecryptFile(f); final File clearTextFile = EncryptedFileUtil.getDecryptFile(f);
@@ -109,7 +109,7 @@ public class TinyEncryptMain {
} }
encryptOrDecryptSuccess = false; // do not delete file encryptOrDecryptSuccess = false; // do not delete file
} else { } 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) { if (encryptOrDecryptSuccess && tinyEncryptArgs.removeFile) {

View File

@@ -186,6 +186,7 @@ public class TinyEncryptMainUtil {
sb.append(header("Envelops")).append("KMS: ").append(toYesOrNo(meta.getEnvelop())) sb.append(header("Envelops")).append("KMS: ").append(toYesOrNo(meta.getEnvelop()))
.append(", PGP: ").append(toYesOrNo(meta.getPgpEnvelop())) .append(", PGP: ").append(toYesOrNo(meta.getPgpEnvelop()))
.append(", Age: ").append(toYesOrNo(meta.getAgeEnvelop())) .append(", Age: ").append(toYesOrNo(meta.getAgeEnvelop()))
.append(", ECDH: ").append(toYesOrNo(meta.getEcdhEnvelop()))
.append("\n"); .append("\n");
if (StringUtil.isNotBlank(meta.getPgpFingerprint())) { if (StringUtil.isNotBlank(meta.getPgpFingerprint())) {
sb.append(header("PGP fingerprint")).append(meta.getPgpFingerprint()).append("\n"); sb.append(header("PGP fingerprint")).append(meta.getPgpFingerprint()).append("\n");
@@ -193,6 +194,9 @@ public class TinyEncryptMainUtil {
if (StringUtil.isNotBlank(meta.getAgeRecipient())) { if (StringUtil.isNotBlank(meta.getAgeRecipient())) {
sb.append(header("Age recipient")).append(meta.getAgeRecipient()).append("\n"); 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())) { if (StringUtil.isNotBlank(meta.getComment())) {
sb.append(header("Comment")).append(meta.getComment()).append("\n"); sb.append(header("Comment")).append(meta.getComment()).append("\n");
} }

View File

@@ -10,6 +10,7 @@ public class TinyEncryptConfig {
private String localPrivateKeyPemChallenge; private String localPrivateKeyPemChallenge;
private Boolean turnOffEnvelop; private Boolean turnOffEnvelop;
private String pgpEncryptPublicKeyPem; private String pgpEncryptPublicKeyPem;
private String ecdhPublicKeyPoint;
@Deprecated @Deprecated
private String pgpDecryptCmd; private String pgpDecryptCmd;
private String cardCli; private String cardCli;
@@ -73,6 +74,14 @@ public class TinyEncryptConfig {
this.pgpEncryptPublicKeyPem = pgpEncryptPublicKeyPem; this.pgpEncryptPublicKeyPem = pgpEncryptPublicKeyPem;
} }
public String getEcdhPublicKeyPoint() {
return ecdhPublicKeyPoint;
}
public void setEcdhPublicKeyPoint(String ecdhPublicKeyPoint) {
this.ecdhPublicKeyPoint = ecdhPublicKeyPoint;
}
public String getPgpDecryptCmd() { public String getPgpDecryptCmd() {
return pgpDecryptCmd; return pgpDecryptCmd;
} }

View File

@@ -1,7 +1,7 @@
package me.hatter.tools.tinyencrypt.config; package me.hatter.tools.tinyencrypt.config;
public class TinyEncryptConstant { 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"; 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.io.RFile;
import me.hatter.tools.commons.log.LogTool; import me.hatter.tools.commons.log.LogTool;
import me.hatter.tools.commons.log.LogTools; 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.AESCryptTool;
import me.hatter.tools.commons.security.crypt.CryptInputStream; import me.hatter.tools.commons.security.crypt.CryptInputStream;
import me.hatter.tools.commons.security.crypt.CryptOutputStream; 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.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.string.StringUtil;
import me.hatter.tools.commons.tlv.Tlv; import me.hatter.tools.commons.tlv.Tlv;
import me.hatter.tools.commons.tlv.TlvUtil; import me.hatter.tools.commons.tlv.TlvUtil;
@@ -24,6 +28,9 @@ import me.hatter.tools.tinyencrypt.util.SwingWindow;
import java.io.*; import java.io.*;
import java.nio.charset.StandardCharsets; 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.Date;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
@@ -36,6 +43,7 @@ public class EncryptedFileUtil {
public static boolean decryptToOutputStream(TinyEncryptConfig config, File file, OutputStream os, public static boolean decryptToOutputStream(TinyEncryptConfig config, File file, OutputStream os,
boolean pgp, boolean pgp,
boolean age, boolean age,
boolean ecdh,
AtomicReference<TinyEncryptMeta> metaRef) { AtomicReference<TinyEncryptMeta> metaRef) {
if (getDecryptFile(file) == null) { if (getDecryptFile(file) == null) {
log.warn("File is not tinyenc file, skip: " + file); log.warn("File is not tinyenc file, skip: " + file);
@@ -86,6 +94,27 @@ public class EncryptedFileUtil {
return false; return false;
} }
dataKey = dataKeyOpt.get(); 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 { } else {
dataKey = TinyEncryptMetaUtil.decryptDataKey(config, meta); 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()); 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 null;
} }
return outputStream.digest(); 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 ByteArrayOutputStream baos = new ByteArrayOutputStream();
final AtomicReference<TinyEncryptMeta> metaRef = new AtomicReference<>(); 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) { if (!decryptSuccess) {
return; 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); final File decFile = getDecryptFile(file);
if (decFile == null) { if (decFile == null) {
log.warn("File is not tinyenc file, skip: " + decFile); log.warn("File is not tinyenc file, skip: " + decFile);
@@ -176,7 +205,7 @@ public class EncryptedFileUtil {
final AtomicReference<TinyEncryptMeta> meta = new AtomicReference<>(); final AtomicReference<TinyEncryptMeta> meta = new AtomicReference<>();
final boolean decryptResult; final boolean decryptResult;
try (FileOutputStream fos = new FileOutputStream(decFile)) { 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 (!decryptResult) {
if (decFile.length() == 0) { if (decFile.length() == 0) {

View File

@@ -10,6 +10,8 @@ public class TinyEncryptMeta {
private String encryptedComment; private String encryptedComment;
private String pgpEnvelop; private String pgpEnvelop;
private String pgpFingerprint; private String pgpFingerprint;
private String ecdhEnvelop;
private String ecdhPoint;
private String ageEnvelop; private String ageEnvelop;
private String ageRecipient; private String ageRecipient;
private String envelop; private String envelop;
@@ -76,6 +78,22 @@ public class TinyEncryptMeta {
this.pgpFingerprint = pgpFingerprint; 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() { public String getAgeEnvelop() {
return ageEnvelop; 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.network.HttpRequest;
import me.hatter.tools.commons.os.OSUtil; import me.hatter.tools.commons.os.OSUtil;
import me.hatter.tools.commons.security.crypt.AESCryptTool; 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.digest.Digests;
import me.hatter.tools.commons.security.ec.ECUtil;
import me.hatter.tools.commons.security.key.KeyUtil; import me.hatter.tools.commons.security.key.KeyUtil;
import me.hatter.tools.commons.security.random.RandomTool; import me.hatter.tools.commons.security.random.RandomTool;
import me.hatter.tools.commons.security.rsa.RSAUtil; 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.PrivateKey;
import java.security.PublicKey; import java.security.PublicKey;
import java.security.interfaces.ECPublicKey;
import java.security.interfaces.RSAPublicKey; import java.security.interfaces.RSAPublicKey;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Base64; import java.util.Base64;
@@ -138,6 +141,15 @@ public class TinyEncryptMetaUtil {
tinyEncryptMeta.setAgeEnvelop(ageEnvelop.trim()); tinyEncryptMeta.setAgeEnvelop(ageEnvelop.trim());
tinyEncryptMeta.setAgeRecipient(config.getAgeRecipient()); 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.setNonce(RandomTool.secureRandom().nextbytes(12));
tinyEncryptMeta.setUserAgent("TinyEncrypt v" + TinyEncryptConstant.VERSION + "@" + OSUtil.getCurrentOS().name()); tinyEncryptMeta.setUserAgent("TinyEncrypt v" + TinyEncryptConstant.VERSION + "@" + OSUtil.getCurrentOS().name());
tinyEncryptMeta.setComment(comment); tinyEncryptMeta.setComment(comment);

View File

@@ -10,16 +10,17 @@ import me.hatter.tools.commons.log.LogTools;
import me.hatter.tools.commons.string.StringUtil; import me.hatter.tools.commons.string.StringUtil;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Optional; import java.util.Optional;
public class CardCliUtil { public class CardCliUtil {
private static final LogTool log = LogTools.getLogTool(CardCliUtil.class); private static final LogTool log = LogTools.getLogTool(CardCliUtil.class);
public static Optional<String> readUserPin() { 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(); final char[] pin = System.console().readPassword();
if (pin.length < 6) { 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.empty();
} }
return Optional.of(new String(pin)); return Optional.of(new String(pin));
@@ -80,6 +81,26 @@ public class CardCliUtil {
return Optional.of(Bytes.fromHex(jo.getString("text_hex")).bytes()); 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) { public static Optional<String> runProcess(ProcessBuilder pb) {
Process p = null; Process p = null;
try { try {