JAVA ECC 橢圓曲線密碼學

Java : 橢圓曲線加密演算法

謝騏澤 / 謝騏澤 Kevin Hsieh 2021/12/22 18:34:25
842

前言

橢圓曲線密碼學(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達到資訊安全對於資料的保護以及簡單的程式範例。
  • 開發環境

 


 

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

謝騏澤