Java : 橢圓曲線加密演算法
前言
橢圓曲線密碼學(Elliptic Curve Cryptography,簡稱:ECC)是一種基於橢圓曲線數學的公開密鑰加密演算法。
本篇簡單介紹關於運用ECC達到資訊安全對於資料的保護,安全密鑰的交換算法(ECDH)、數位簽章算法(ECDSA)以及由不同加密方式混合而成的加密方式(ECIES),並使用簡單程式範例演示,文中並沒有橢圓曲線演算法理論與說明,如有興趣可以搜尋相關文章詳細閱讀。
資訊安全(Information Security)
- 資訊安全的內容可以簡化為三個基本點,稱為CIA三要素。
- CIA Triad:
- 機密性 (Confidentiality):確保資料傳遞與儲存的隱密性,避免未經授權的使用者有意或無意的揭露資料內容。
- 完整性 (Integrity):確保資料在傳輸或儲存時,保有正確性與一致性。
- 可用性 (Availability):使用者需透過資訊系統進行操作時,資料與服務須要保持可用的狀況,並且能滿足使用的需求。
- 其他的特性:
- 可鑑別性 (Authenticity):證明資源的識別就是所聲明者的特性。
- 可歸責性 (Accountability):確保可以唯一追溯到該實體的性質。
- 不可否認性 (Non-repudiation):對已經發生的動作或事件的證明,使發起該動作或事件的實體,往後不可以否認其行為。基本上是採用數位簽章技術。
對稱式加密(Symmetric Encryption)
- 對稱式加密是傳送方(sender)與接收方(recipient)的加解密皆使用同一把密鑰(secret key),只要雙方都擁有這把鑰匙,傳送方用這把鑰匙將本文訊息(plain text)進行加密傳資料,接收方收到加密過後的加密訊息(cipher text)後,再用同一把鑰匙解密,就能得到本文訊息。
非對稱式加密(Asymmetric Encryption)
- 需要兩把金鑰,一個是公開密鑰(public key),另一個是私有密鑰(private key);公鑰用作加密,私鑰則用作解密,鑰匙要是成雙成對(key pair)。傳送方(sender)使用接收方(recipient)的公鑰把本文訊息(plain text)加密後所得的加密訊息(cipher text),只能用相對應接收方(recipient)的私鑰才能解密並得到原本的本文訊息。公鑰可以公開,可任意向外發布;私鑰不可以公開,必須由使用者自行嚴格秘密保管,絕不透過任何途徑向任何人提供,也不會透露給被信任的要通訊的另一方。
- 需接收方(recipient)要如何確認訊息真的是由傳送方(sender)傳送的,如有心人士取得接收方的公鑰(public key),偽造或夾帶病毒的訊息,再利用接收方的公鑰加密傳送,接收方就會取得非預期的訊息或檔案,可能會導致資料被盜竊、或者被利用做其他不法用途。這時就可以使用數位簽章(Digital Signature)來確保是接收方(recipient)傳送過來的資料。
數位簽章(Digital Signature)
- 是一種電子式的簽名機制,有效的數位簽章接收端有理由相信該加密訊息是由發送端發送的,因此接收端不能否認已發送的訊息(不可否認性和可鑑別性),檔案或訊息會經過雜湊(hash)運算出一個雜湊值(hash value),再透過金鑰對雜湊值進行加密,接收端則需要金鑰進行解密取得雜湊後,得到的檔案或訊息雜湊,再將兩個雜湊值進行比對(compare)確保檔案或訊息的內容有完整傳遞且沒有被經過攥改(完整性)。
² 簽名加密
² 簽名驗證
ECC橢圓曲線加密演算法
- 由接下來介紹運用ECC達到資訊安全對於資料的保護以及簡單的程式範例。
- 開發環境:
- Java版本:Java11
- 第三方套件:Bouncy Castle、Apache Commons Codec
ECC安全密鑰的交換算法(ECDH)
- 由於通過ECDH,雙方在交換各自的公開金鑰(Public Key),將自己的私密金鑰(Private Key)與對方的公開金鑰,在不共享任何祕密的前提下,經過進行密鑰協商(KA)後,可以得到一個共同的密鑰(Share Secret),之後可以用這把密鑰去做後需的加密演算,在不傳遞真正密鑰的情況下只有雙方可以解密。
² 程式範例
public class ECDH {
// EC
private static final String ALGORITHM_EC = "EC";
// ECDH
private static final String ALGORITHM_ECDH = "ECDH";
public static void main(String[] args) {
try {
// Generate key pair
KeyPair aliceKeyPair = ECCUtil.genKeyPair(256, ALGORITHM_EC);
KeyPair bobKeyPair = ECCUtil.genKeyPair(256, ALGORITHM_EC);
// Get Alice's public key and private key
String alicePublicKeyWithHex = Hex.encodeHexString(aliceKeyPair.getPublic().getEncoded());
String alicePrivateKeyWithHex = Hex.encodeHexString(aliceKeyPair.getPrivate().getEncoded());
// Get Bob's public key and private key
String bobPublicKeyWithHex = Hex.encodeHexString(bobKeyPair.getPublic().getEncoded());
String bobPrivateKeyWithHex = Hex.encodeHexString(bobKeyPair.getPrivate().getEncoded());
System.out.println("Alice PublicKey = " + alicePublicKeyWithHex);
System.out.println("Alice PrivateKey = " + alicePrivateKeyWithHex);
System.out.println("---------------------------------------------------------------------------------------------------------------");
System.out.println("Bob PublicKey = " + bobPublicKeyWithHex);
System.out.println("Bob PrivateKey = " + bobPrivateKeyWithHex);
System.out.println("---------------------------------------------------------------------------------------------------------------");
// Get secret key
byte[] secretKey1 = ECCUtil.getSecretKey(aliceKeyPair.getPublic(), bobKeyPair.getPrivate(), ALGORITHM_ECDH);
byte[] secretKey2 = ECCUtil.getSecretKey(bobKeyPair.getPublic(), aliceKeyPair.getPrivate(), ALGORITHM_ECDH);
System.out.println("Secret Key 1 = " + Hex.encodeHexString(secretKey1));
System.out.println("Secret Key 2 = " + Hex.encodeHexString(secretKey2));
} catch (InvalidKeyException | NoSuchAlgorithmException ex) {
ex.printStackTrace();
}
}
}
public static KeyPair genKeyPair(int keySize, String algorithm) throws NoSuchAlgorithmException {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(algorithm);
keyPairGenerator.initialize(keySize);
return keyPairGenerator.generateKeyPair();
}
public static byte[] getSecretKey(PublicKey publicKey, PrivateKey privateKey, String algorithm) throws NoSuchAlgorithmException, InvalidKeyException {
KeyAgreement keyAgreement = KeyAgreement.getInstance(algorithm);
keyAgreement.init(privateKey);
keyAgreement.doPhase(publicKey, true);
return keyAgreement.generateSecret();
}
ECC數位簽章算法(ECDSA)
- 是一種電子式的簽名機制,有效的數位簽章接收端有理由相信該加密訊息是由發送端發送的,因此接收端不能否認已發送的訊息(不可否認性和可鑑別性),檔案或訊息會經過雜湊(hash)運算出一個雜湊值(hash value),再透過金鑰對雜湊值進行加密,接收端則需要金鑰進行解密取得雜湊後,得到的檔案或訊息雜湊,再將兩個雜湊值進行比對(compare)確保檔案或訊息的內容有完整傳遞且沒有被經過攥改(完整性)。
² 程式範例
public class ECDSA {
// EC
private static final String ALGORITHM_EC = "EC";
// SHA256 with ECDSA
private static final String ALGORITHM_SHA256_WITH_ECDSA = "SHA256withECDSA";
public static void main(String[] args) {
try {
String plaintext = "訊息本文測試";
KeyPair keyPair = ECCUtil.genKeyPair(256, ALGORITHM_EC);
// Get signature
byte[] signature = DigitalSignatureUtil.sign(
ALGORITHM_SHA256_WITH_ECDSA,
keyPair.getPrivate(),
plaintext.getBytes());
// Do verify
boolean certified = DigitalSignatureUtil.verify(
ALGORITHM_SHA256_WITH_ECDSA,
keyPair.getPublic(),
plaintext.getBytes(),
signature);
System.out.println("Certified = " + certified);
} catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException ex) {
ex.printStackTrace();
}
}
}
public class DigitalSignatureUtil {
public static byte[] sign(String algorithm, PrivateKey privateKey, byte[] plaintext) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
Signature signature = Signature.getInstance(algorithm);
signature.initSign(privateKey);
signature.update(plaintext);
return signature.sign();
}
public static boolean verify(String algorithm, PublicKey publicKey, byte[] plaintext, byte[] receivedSignature) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
Signature signature = Signature.getInstance(algorithm);
signature.initVerify(publicKey);
signature.update(plaintext);
return signature.verify(receivedSignature);
}
}
ECC混合式加密(ECIES)
- 由不同加密方式混合而成的加密方式,ECIES使用以下包含不同類型的函數,
- 密鑰協商功能(Key Agreement)
- 金鑰衍生函式(Key Derivation Function)
- 對稱式加密(Symmetric Encryption)
- 雜湊函式(Hash Function)
² 加密流程
1. 傳送方(Sender)取得接收方(Recipient)的公開金鑰(Recipient Public Key)後,使用傳送方的私密金鑰(Sender Private Key)與其進行密鑰協商(KA)取得分享金鑰(Share Secret)。
2. 分享金鑰進行金鑰衍生函式(KDF),將其進行SHA512雜湊(Hash)。
3. 將雜湊後分享金鑰的值分成一半,前半為使用對稱式加密的金鑰(ENC Key),後半為產生訊息鑑別碼的金鑰(MAC Key)。
4. 訊息本文使用ENC Key進行AES對稱式加密,並取得加密後的加密訊息(cipher text)。
5. 加密訊息再使用MAC Key進行金鑰雜湊訊息鑑別碼(HMAC),並取得可識別的鑑別碼(Authentication Tag)。
² 解密流程
1. 接收方(Recipient)取得傳送方(Sender)的公開金鑰(Sender Public Key)、加密訊息(cipher text)、鑑別碼(Authentication Tag)後,進行解密流程。
2. 使用傳送方的公開金鑰(Sender Public Key)與其進行密鑰協商(KA)取得分享金鑰(Share Secret)。
3. 分享金鑰進行金鑰衍生函式(KDF),將其進行SHA512雜湊(Hash)。分享金鑰進行金鑰衍生函式(KDF),將其進行SHA512雜湊(Hash)。
4. 將雜湊後分享金鑰的值分成一半,前半為使用對稱式加密的金鑰(ENC Key),後半為產生訊息鑑別碼的金鑰(MAC Key)。
5. 加密訊息(cipher text)使用ENC Key進行AES對稱式解密,並取得解密後的訊息本文(plain text)。
6. 加密訊息使用MAC Key進行金鑰雜湊訊息鑑別碼(HMAC)後,與傳送方的鑑別碼進行比對,確保在傳輸的過程當中訊息未被遭到攥改。
² 程式範例
public class ECIES {
// EC
private static final String ALGORITHM_EC = "EC";
// ECDH
private static final String ALGORITHM_ECDH = "ECDH";
// ECDSA
private static final String ALGORITHM_ECDSA = "ECDSA";
// Algorithm/Mode/Padding
private static final String ALGORITHM_AES_CBC_PKCS5PADDING = "AES/CBC/PKCS5Padding";
// 算法/加密模式/數據填充方式
private static final String ALGORITHM_HMAC_256 = "HmacSHA256";
// IV
private static final String IV = "0000000000000000";
public static void main(String[] args) {
String plainText = "訊息本文測試";
try {
// 產生金鑰對
KeyPair senderKeyPair = ECCUtil.genKeyPair(256, ALGORITHM_EC);
KeyPair recipientKeyPair = ECCUtil.genKeyPair(256, ALGORITHM_EC);
String senderPublicKeyStr = Hex.encodeHexString(senderKeyPair.getPublic().getEncoded());
String senderPrivateKeyStr = Hex.encodeHexString(senderKeyPair.getPrivate().getEncoded());
String recipientPublicKeyStr = Hex.encodeHexString(recipientKeyPair.getPublic().getEncoded());
String recipientPrivateKeyStr = Hex.encodeHexString(recipientKeyPair.getPrivate().getEncoded());
PublicKey senderPublicKey = ECCUtil.getPublicKey(ALGORITHM_ECDSA, Hex.decodeHex(senderPublicKeyStr));
PrivateKey senderPrivateKey = ECCUtil.getPrivateKey(ALGORITHM_ECDSA, Hex.decodeHex(senderPrivateKeyStr));
PublicKey recipientPublicKey = ECCUtil.getPublicKey(ALGORITHM_ECDSA, Hex.decodeHex(recipientPublicKeyStr));
PrivateKey recipientPrivateKey = ECCUtil.getPrivateKey(ALGORITHM_ECDSA, Hex.decodeHex(recipientPrivateKeyStr));
Map<String, Object> encryptedMap = encrypt(plainText, senderPublicKey, recipientPrivateKey);
Map<String, Object> decryptedMap = decrypt(Hex.decodeHex((String) encryptedMap.get("EncryptValue")), recipientPublicKey, senderPrivateKey);
System.out.println("Encrypted Value = " + encryptedMap.get("EncryptValue"));
System.out.println("Encrypted HMAC Value = " + encryptedMap.get("HMACValue"));
System.out.println("Decrypted Value = " + decryptedMap.get("PlainText"));
System.out.println("Decrypted HMAC Value = " + decryptedMap.get("HMACValue"));
} catch (NoSuchAlgorithmException | DecoderException | NoSuchProviderException | InvalidKeySpecException e) {
e.printStackTrace();
}
}
private static Map<String, Object> encrypt(String plainText, PublicKey publicKey, PrivateKey privateKey) {
Map<String, Object> map = new HashMap<>();
try {
// Get share secret key
byte[] shareSecretKey = ECCUtil.getSecretKey(publicKey, privateKey, "ECDH");
// Share secret key with Sha512
byte[] shareSecretKeyWithSha512 = HashUtil.SHA512(shareSecretKey);
// Get macKey
byte[] macKey = Arrays.copyOfRange(shareSecretKeyWithSha512, 0, 16);
// Get encKey
byte[] encKey = Arrays.copyOfRange(shareSecretKeyWithSha512, 16, 32);
// PlainText encrypt with AES
byte[] encryptPlainTextWithAES = AESUtil.encrypt(ALGORITHM_AES_CBC_PKCS5PADDING, plainText.getBytes(), encKey, IV);
String aesWithHex = Hex.encodeHexString(encryptPlainTextWithAES);
// CipherText encrypt with HMAC
byte[] encryptCipherTextWithHMAC = HMACUtil.hmac(ALGORITHM_HMAC_256, macKey, aesWithHex);
String hmacWithHex = Hex.encodeHexString(encryptCipherTextWithHMAC);
map.put("PublicKey", Hex.encodeHexString(publicKey.getEncoded()));
map.put("EncryptValue", aesWithHex);
map.put("HMACValue", hmacWithHex);
} catch (NoSuchAlgorithmException | InvalidKeyException | NoSuchPaddingException | BadPaddingException | IllegalBlockSizeException | InvalidAlgorithmParameterException ex) {
ex.printStackTrace();
}
return map;
}
private static Map<String, Object> decrypt(byte[] encryptData, PublicKey publicKey, PrivateKey privateKey) {
Map<String, Object> map = new HashMap<>();
try {
// Get share secret key
byte[] shareSecretKey = ECCUtil.getSecretKey(publicKey, privateKey, ALGORITHM_ECDH);
// Share secret key with Sha512
byte[] shareSecretKeyWithSha512 = HashUtil.SHA512(shareSecretKey);
// Get macKey
byte[] macKey = Arrays.copyOfRange(shareSecretKeyWithSha512, 0, 16);
// Get encKey
byte[] encKey = Arrays.copyOfRange(shareSecretKeyWithSha512, 16, 32);
// Encrypt data decrypt with AES
byte[] decryptData = AESUtil.decrypt(ALGORITHM_AES_CBC_PKCS5PADDING, encryptData, encKey, IV);
// CipherText encrypt with HMAC
byte[] encryptCipherTextWithHMAC = HMACUtil.hmac(ALGORITHM_HMAC_256, macKey, Hex.encodeHexString(encryptData));
String hmacWithHex = Hex.encodeHexString(encryptCipherTextWithHMAC);
map.put("PlainText", new String(decryptData));
map.put("HMACValue", hmacWithHex);
} catch (NoSuchAlgorithmException | InvalidKeyException | NoSuchPaddingException | BadPaddingException | IllegalBlockSizeException | InvalidAlgorithmParameterException ex) {
ex.printStackTrace();
}
return map;
}
}
public class ECCUtil {
private static final BouncyCastleProvider bouncyCastleProvider = new BouncyCastleProvider();
public static PublicKey getPublicKey(String algorithm, byte[] publicKey) throws NoSuchAlgorithmException, NoSuchProviderException, InvalidKeySpecException {
Security.addProvider(bouncyCastleProvider);
KeyFactory factory = KeyFactory.getInstance(algorithm, "BC");
return factory.generatePublic(new X509EncodedKeySpec(publicKey));
}
public static PrivateKey getPrivateKey(String algorithm, byte[] privateKey) throws NoSuchAlgorithmException, NoSuchProviderException, InvalidKeySpecException {
Security.addProvider(bouncyCastleProvider);
KeyFactory factory = KeyFactory.getInstance(algorithm, "BC");
return factory.generatePrivate(new PKCS8EncodedKeySpec(privateKey));
}
}
References