資料加密-國密SM2對資料進行加密

R-B發表於2021-09-09

1 什麼是SM2

RSA演算法的危機在於其存在亞指數演算法,對ECC演算法而言一般沒有亞指數攻擊演算法。

SM2橢圓曲線公鑰密碼演算法:我國自主智慧財產權的商用密碼演算法,是ECC(Elliptic Curve Cryptosystem)演算法的一種,基於橢圓曲線離散對數問題,計算複雜度是指數級,求解難度較大,同等安全程度要求下,橢圓曲線密碼較其他公鑰演算法所需金鑰長度小很多。

1.1 ECC演算法簡述

ECC的全稱是Error Checking and Correction,是一種用於Nand的差錯檢測和修正演算法。如果操作時序和電路穩定性不存在問題的話,NAND Flash出錯的時候一般不會造成整個Block或是Page不能讀取或是全部出錯,而是整個Page(例如512Bytes)中只有一個或幾個bit出錯。ECC能糾正1個位元錯誤和檢測2個位元錯誤,而且計算速度很快,但對1位元以上的錯誤無法糾正,對2位元以上的錯誤不保證能檢測。

以上部分就是簡單介紹一下什麼是SM2,具體的理論請自行百度(太難了。。。。我這個小菜雞也看不懂)。本篇主要是SM2方法的工具類以及簡單的demo,工具程式碼已經經過實際測驗可直接使用,涉及到業務相關的自行處理。

2 SM2工具類實現

首先定義一個公私鑰對實體類,實體類很簡單,就兩個欄位:

@Data
public class SM2KeyPair {
    public SM2KeyPair(String publicKey, String privateKey) {
        this.publicKey = publicKey;
        this.privateKey = privateKey;
    }

    /**公鑰*/
    private String publicKey;
    /**私鑰*/
    private String privateKey;

}

SM2演算法處理類:

s(new SM3Digest());
    }

    public SM2EngineExtend(Digest digest) {
        this.digest = digest;
    }

    /**
     * 設定密文排序方式
     * @param cipherMode
     */
    public void setCipherMode(int cipherMode){
        this.cipherMode = cipherMode;
    }

    /**
     * 預設初始化方法,使用國密排序標準
     * @param forEncryption - 是否以加密模式初始化
     * @param param - 曲線引數
     */
    public void init(boolean forEncryption, CipherParameters param) {
        init(forEncryption, CIPHERMODE_NORM, param);
    }

    /**
     * 預設初始化方法,使用國密排序標準
     * @param forEncryption 是否以加密模式初始化
     * @param cipherMode 加密資料排列模式:1-標準排序;0-BC預設排序
     * @param param 曲線引數
     */
    public void init(boolean forEncryption, int cipherMode, CipherParameters param) {
        this.forEncryption = forEncryption;
        this.cipherMode = cipherMode;
        if (forEncryption) {
            ParametersWithRandom rParam = (ParametersWithRandom) param;

            ecKey = (ECKeyParameters) rParam.getParameters();
            ecParams = ecKey.getParameters();

            ECPoint s = ((ECPublicKeyParameters) ecKey).getQ().multiply(ecParams.getH());
            if (s.isInfinity()) {
                throw new IllegalArgumentException("invalid key: [h]Q at infinity");
            }

            random = rParam.getRandom();
        } else {
            ecKey = (ECKeyParameters) param;
            ecParams = ecKey.getParameters();
        }

        curveLength = (ecParams.getCurve().getFieldSize() + 7) / 8;
    }

    /**
     * 加密或解密輸入資料
     * @param in
     * @param inOff
     * @param inLen
     * @return
     * @throws InvalidCipherTextException
     */
    public byte[] processBlock( byte[] in, int inOff, int inLen) throws InvalidCipherTextException {
        if (forEncryption) {
            // 加密
            return encrypt(in, inOff, inLen);
        } else {
            return decrypt(in, inOff, inLen);
        }
    }

    /**
     * 加密實現,根據cipherMode輸出指定排列的結果,預設按標準方式排列
     * @param in
     * @param inOff
     * @param inLen
     * @return
     * @throws InvalidCipherTextException
     */
    private byte[] encrypt(byte[] in, int inOff, int inLen)
            throws InvalidCipherTextException {
        byte[] c2 = new byte[inLen];

        System.arraycopy(in, inOff, c2, 0, c2.length);

        byte[] c1;
        ECPoint kPB;
        do {
            BigInteger k = nextK();

            ECPoint c1P = ecParams.getG().multiply(k).normalize();

            c1 = c1P.getEncoded(false);

            kPB = ((ECPublicKeyParameters) ecKey).getQ().multiply(k).normalize();

            kdf(digest, kPB, c2);
        }
        while (notEncrypted(c2, in, inOff));

        byte[] c3 = new byte[digest.getDigestSize()];

        addFieldElement(digest, kPB.getAffineXCoord());
        digest.update(in, inOff, inLen);
        addFieldElement(digest, kPB.getAffineYCoord());

        digest.doFinal(c3, 0);
        if (cipherMode == CIPHERMODE_NORM){
            return Arrays.concatenate(c1, c3, c2);
        }
        return Arrays.concatenate(c1, c2, c3);
    }

    /**
     * 解密實現,預設按標準排列方式解密,解密時解出c2部分原文並校驗c3部分
     * @param in
     * @param inOff
     * @param inLen
     * @return
     * @throws InvalidCipherTextException
     */
    private byte[] decrypt(byte[] in, int inOff, int inLen)
            throws InvalidCipherTextException {
        byte[] c1 = new byte[curveLength * 2 + 1];

        System.arraycopy(in, inOff, c1, 0, c1.length);

        ECPoint c1P = ecParams.getCurve().decodePoint(c1);

        ECPoint s = c1P.multiply(ecParams.getH());
        if (s.isInfinity()) {
            throw new InvalidCipherTextException("[h]C1 at infinity");
        }

        c1P = c1P.multiply(((ECPrivateKeyParameters) ecKey).getD()).normalize();

        byte[] c2 = new byte[inLen - c1.length - digest.getDigestSize()];
        if (cipherMode == CIPHERMODE_BC) {
            System.arraycopy(in, inOff + c1.length, c2, 0, c2.length);
        }else{
            // C1 C3 C2
            System.arraycopy(in, inOff + c1.length + digest.getDigestSize(), c2, 0, c2.length);
        }

        kdf(digest, c1P, c2);

        byte[] c3 = new byte[digest.getDigestSize()];

        addFieldElement(digest, c1P.getAffineXCoord());
        digest.update(c2, 0, c2.length);
        addFieldElement(digest, c1P.getAffineYCoord());

        digest.doFinal(c3, 0);

        int check = 0;
        // 檢查密文輸入值C3部分和由摘要生成的C3是否一致
        if (cipherMode == CIPHERMODE_BC) {
            for (int i = 0; i != c3.length; i++) {
                check |= c3[i] ^ in[c1.length + c2.length + i];
            }
        }else{
            for (int i = 0; i != c3.length; i++) {
                check |= c3[i] ^ in[c1.length + i];
            }
        }

        clearBlock(c1);
        clearBlock(c3);

        if (check != 0) {
            clearBlock(c2);
            throw new InvalidCipherTextException("invalid cipher text");
        }

        return c2;
    }

    private boolean notEncrypted(byte[] encData, byte[] in, int inOff) {
        for (int i = 0; i != encData.length; i++) {
            if (encData[i] != in[inOff]) {
                return false;
            }
        }

        return true;
    }

    private void kdf(Digest digest, ECPoint c1, byte[] encData) {
        int ct = 1;
        int v = digest.getDigestSize();

        byte[] buf = new byte[digest.getDigestSize()];
        int off = 0;

        for (int i = 1; i > 24));
            digest.update((byte) (ct >> 16));
            digest.update((byte) (ct >> 8));
            digest.update((byte) ct);

            digest.doFinal(buf, 0);

            if (off + buf.length = 0);

        return k;
    }

    private void addFieldElement(Digest digest, ECFieldElement v) {
        byte[] p = BigIntegers.asUnsignedByteArray(curveLength, v.toBigInteger());

        digest.update(p, 0, p.length);
    }

    /**
     * clear possible sensitive data
     */
    private void clearBlock(
            byte[] block) {
        for (int i = 0; i != block.length; i++) {
            block[i] = 0;
        }
    }


}

最後就是SM2工具類,工具類中提供獲取公私鑰對,加密解密三個方法。

@Slf4j
public class SM2Utils {

    /**
     * SM2加密演算法
     * @param publicKey 公鑰
     * @param data 待加密的資料
     * @return 密文,BC庫產生的密文帶由04識別符號,與非BC庫對接時需要去掉開頭的04
     */
    public static String encrypt(String publicKey, String data){
        // 按國密排序標準加密
        return encrypt(publicKey, data, SM2EngineExtend.CIPHERMODE_NORM);
    }

    /**
     * SM2加密演算法
     * @param publicKey 公鑰
     * @param data 待加密的資料
     * @param cipherMode 密文排列方式0-C1C2C3;1-C1C3C2;
     * @return 密文,BC庫產生的密文帶由04識別符號,與非BC庫對接時需要去掉開頭的04
     */
    public static String encrypt(String publicKey, String data, int cipherMode){
        // 獲取一條SM2曲線引數
        X9ECParameters sm2ECParameters = GMNamedCurves.getByName("sm2p256v1");
        // 構造ECC演算法引數,曲線方程、橢圓曲線G點、大整數N
        ECDomainParameters domainParameters = new ECDomainParameters(sm2ECParameters.getCurve(), sm2ECParameters.getG(), sm2ECParameters.getN());
        //提取公鑰點
        ECPoint pukPoint = sm2ECParameters.getCurve().decodePoint(Hex.decode(publicKey));
        // 公鑰前面的02或者03表示是壓縮公鑰,04表示未壓縮公鑰, 04的時候,可以去掉前面的04
        ECPublicKeyParameters publicKeyParameters = new ECPublicKeyParameters(pukPoint, domainParameters);

        SM2EngineExtend sm2Engine = new SM2EngineExtend();
        // 設定sm2為加密模式
        sm2Engine.init(true, cipherMode, new ParametersWithRandom(publicKeyParameters, new SecureRandom()));

        byte[] arrayOfBytes = null;
        try {
            byte[] in = data.getBytes();
            arrayOfBytes = sm2Engine.processBlock(in, 0, in.length);
        } catch (Exception e) {
            log.error("SM2加密時出現異常:{}", e.getMessage(), e);
        }
        return Hex.toHexString(arrayOfBytes);
    }

    /**
     * 獲取sm2金鑰對
     * BC庫使用的公鑰=64個位元組+1個位元組(04標誌位),BC庫使用的私鑰=32個位元組
     * SM2秘鑰的組成部分有 私鑰D 、公鑰X 、 公鑰Y , 他們都可以用長度為64的16進位制的HEX串表示,
     * 
SM2公鑰並不是直接由X+Y表示 , 而是額外新增了一個頭,當啟用壓縮時:公鑰=有頭+公鑰X ,即省略了公鑰Y的部分 * @param compressed 是否壓縮公鑰(加密解密都使用BC庫才能使用壓縮) * @return */ public static SM2KeyPair getSm2Keys(boolean compressed){ //獲取一條SM2曲線引數 X9ECParameters sm2ECParameters = GMNamedCurves.getByName("sm2p256v1"); //構造domain引數 ECDomainParameters domainParameters = new ECDomainParameters(sm2ECParameters.getCurve(), sm2ECParameters.getG(), sm2ECParameters.getN()); //1.建立金鑰生成器 ECKeyPairGenerator keyPairGenerator = new ECKeyPairGenerator(); //2.初始化生成器,帶上隨機數 try { keyPairGenerator.init(new ECKeyGenerationParameters(domainParameters, SecureRandom.getInstance("SHA1PRNG"))); } catch (NoSuchAlgorithmException e) { log.error("生成公私鑰對時出現異常:", e); } //3.生成金鑰對 AsymmetricCipherKeyPair asymmetricCipherKeyPair = keyPairGenerator.generateKeyPair(); ECPublicKeyParameters publicKeyParameters = (ECPublicKeyParameters)asymmetricCipherKeyPair.getPublic(); ECPoint ecPoint = publicKeyParameters.getQ(); // 把公鑰放入map中,預設壓縮公鑰 // 公鑰前面的02或者03表示是壓縮公鑰,04表示未壓縮公鑰,04的時候,可以去掉前面的04 String publicKey = Hex.toHexString(ecPoint.getEncoded(compressed)); ECPrivateKeyParameters privateKeyParameters = (ECPrivateKeyParameters) asymmetricCipherKeyPair.getPrivate(); BigInteger intPrivateKey = privateKeyParameters.getD(); // 把私鑰放入map中 String privateKey = intPrivateKey.toString(16); return new SM2KeyPair(publicKey, privateKey); } /** * SM2解密演算法 * @param privateKey 私鑰 * @param cipherData 密文資料 * @return */ public static String decrypt(String privateKey, String cipherData) { // // 按國密排序標準解密 return decrypt(privateKey, cipherData, SM2EngineExtend.CIPHERMODE_NORM); } /** * SM2解密演算法 * @param privateKey 私鑰 * @param cipherData 密文資料 * @param cipherMode 密文排列方式0-C1C2C3;1-C1C3C2; * @return */ public static String decrypt(String privateKey, String cipherData, int cipherMode) { // 使用BC庫加解密時密文以04開頭,傳入的密文前面沒有04則補上 if (!cipherData.startsWith("04")){ cipherData = "04" + cipherData; } byte[] cipherDataByte = Hex.decode(cipherData); //獲取一條SM2曲線引數 X9ECParameters sm2ECParameters = GMNamedCurves.getByName("sm2p256v1"); //構造domain引數 ECDomainParameters domainParameters = new ECDomainParameters(sm2ECParameters.getCurve(), sm2ECParameters.getG(), sm2ECParameters.getN()); BigInteger privateKeyD = new BigInteger(privateKey, 16); ECPrivateKeyParameters privateKeyParameters = new ECPrivateKeyParameters(privateKeyD, domainParameters); SM2EngineExtend sm2Engine = new SM2EngineExtend(); // 設定sm2為解密模式 sm2Engine.init(false, cipherMode, privateKeyParameters); String result = ""; try { byte[] arrayOfBytes = sm2Engine.processBlock(cipherDataByte, 0, cipherDataByte.length); return new String(arrayOfBytes); } catch (Exception e) { log.error("SM2解密時出現異常:{}", e.getMessage(), e); } return result; } }

2 SM2工具類測試

新建一個demo,對剛才的工具類進行測試

public static void main(String[] args) {
        //獲取公私鑰
        SM2KeyPair sm2Keys = SM2Utils.getSm2Keys(false);
        System.out.println("公鑰 :" + sm2Keys.getPublicKey());
        System.out.println("私鑰 :" + sm2Keys.getPrivateKey());

        //需要加密的資料
        String data = "9ol.0p;/?$^MJU&";
        //公鑰加密,獲取密文
        String encrypt = SM2Utils.encrypt(sm2Keys.getPublicKey(), data);
        System.out.println("密文 :" + encrypt);

        //私鑰解密
        String decrypt = SM2Utils.decrypt(sm2Keys.getPrivateKey(), encrypt);
        System.out.println("解密資料 : " + decrypt);

        System.out.println("明文密文是否相同 :" + data.equals(decrypt));
    }

控制檯輸出:

公鑰 :04088b8bc1191ad2bc72ef17db5e22f128ac4621442c060c5685512f6bb8b06594d7fd8027ef38d74351012a2c3794be811c2789fa13ae0c016b6f151c53706a9c
私鑰 :76b8c2f70bef82029ebba73d02c0fcc53208948897a4e223869485ad36e12034
密文 :04cec3b6ed6c6d5ac30f7ed2b260add3e1c9f1406be18bc9c74b631756b67a98d86df8903a54e6f932a9138bbb1c4de6818ae2b5b1f75aea0c995ed341f0b0cd65359c3f00cc0865e640669f19cd21eb820faf13ba1c671bc883c1d30cb7bb556a6b3ec54c186032087a35fd01d63ecfec13
解密資料 : 9ol.0p;/?$^MJU&
明文密文是否相同 :true

到此SM2加解密實現就算完成,再實際應用中通常再前端先獲取到公鑰,然後透過公鑰對請求資料進行加密,將加密後的密文串和公鑰(加密)傳遞給後端,後端先透過公鑰獲取到對應私鑰再將密文解密成明文,最後給到controller中處理。
後面還會增加簽名加密和實際業務場景中前後端互動的加密解密處理。
我是王同學,喜歡這篇文章的夥伴可以點贊關注,我會經常分享技術相關部落格,共同進步。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/4560/viewspace-2797578/,如需轉載,請註明出處,否則將追究法律責任。

相關文章