一文帶你學會國產加密演算法SM4的java實現方案

經典雞翅發表於2020-05-18

前言

今天給大家帶來一個國產SM4加密解密演算法的java後端解決方案,程式碼完整,可以直接使用,希望給大家帶來幫助,尤其是做政府系統的開發人員,可以直接應用到專案中進行加密解密。
畫重點!是SM4哦,不是SM。哈哈,各位要在知識裡遨遊,不要想歪。正文開始~

國產SM4加密解密演算法概念介紹

SMS4演算法是在國內廣泛使用的WAPI無線網路標準中使用的加密演算法,是一種32輪的迭代非平衡Feistel結構的分組加密演算法,其金鑰長度和分組長度均為128。SMS4演算法的加解密過程中使用的演算法是完全相同的,唯一不同點在於該演算法的解密金鑰是由它的加密金鑰進行逆序變換後得到的。
SMS4分組加密演算法是中國無線標準中使用的分組加密演算法,在2012年已經被國家商用密碼管理局確定為國家密碼行業標準,標準編號GM/T 0002-2012並且改名為SM4演算法,與SM2橢圓曲線公鑰密碼演算法,SM3密碼雜湊演算法共同作為國家密碼的行業標準,在我國密碼行業中有著極其重要的位置。
SMS4演算法的分組長度為128bit,金鑰長度也是128bit。加解密演算法均採用32輪非平衡Feistel迭代結構,該結構最先出現在分組密碼LOKI的金鑰擴充套件演算法中。SMS4通過32輪非線性迭代後加上一個反序變換,這樣只需要解密金鑰是加密金鑰的逆序,就能使得解密演算法與加密演算法保持一致。SMS4加解密演算法的結構完全相同,只是在使用輪金鑰時解密金鑰是加密金鑰的逆序。
S盒是一種利用非線性變換構造的分組密碼的一個元件,主要是為了實現分組密碼過程中的混淆的特性和設計的。SMS4演算法中的S盒在設計之初完全按照歐美分組密碼的設計標準進行,它採用的方法是能夠很好抵抗差值攻擊的仿射函式逆對映覆合法。

SM4加密演算法應用場景

SM4常用於政府系統的資料傳輸加密,比如當我們前端向後臺傳引數的時候,可以使用此演算法。對引數的資料進行加密,然後後臺對加密的資料進行解密再儲存到資料庫中,保證資料傳輸過程中,不受洩露。
本次提供的方案不僅提供sm4的加密解密,還提供了md5演算法的完整性防篡改校驗。

Java端解決方案

對於java端,我們使用的基於spring的aop切面和自定義註解來實現。整體思路為,當後臺開啟加密解密的時候,針對於打上註解的方法,尋找實體類中打上註解的欄位進行加密和解密。再從前端傳遞請求的request中取出md5的header,進行md5的完整性,防篡改校驗。

首先我們必須說的是兩個工具類,一個是SM4Utils工具類,另一個則是md5工具類。
下面先來說一下SM4Utils。這個工具類用於SM4演算法的加密和解密及密碼校驗。我們先直接看程式碼,然後後面對此進行解釋。

SM4Utils

public class Sm4Utils {

    private static final String ENCODING = "UTF-8";
    public static final String ALGORIGTHM_NAME = "SM4";
    public static final String ALGORITHM_NAME_ECB_PADDING = "SM4/ECB/PKCS7Padding";
    public static final int DEFAULT_KEY_SIZE = 128;

    public Sm4Utils() {
    }

    static {
        Security.addProvider(new BouncyCastleProvider());
    }

    /**
     *  @Description:生成ecb暗號
     */
    private static Cipher generateEcbCipher(String algorithmName, int mode, byte[] key) throws Exception {
        Cipher cipher = Cipher.getInstance(algorithmName,BouncyCastleProvider.PROVIDER_NAME);
        Key sm4Key = new SecretKeySpec(key, ALGORIGTHM_NAME);
        cipher.init(mode, sm4Key);
        return cipher;
    }

    /**
     *  @Description:自動生成金鑰
     */
    public static byte[] generateKey() throws Exception {
        return generateKey(DEFAULT_KEY_SIZE);
    }

    public static byte[] generateKey(int keySize) throws Exception {
        KeyGenerator kg = KeyGenerator.getInstance(ALGORIGTHM_NAME, BouncyCastleProvider.PROVIDER_NAME);
        kg.init(keySize, new SecureRandom());
        return kg.generateKey().getEncoded();
    }


    /**
     *  @Description:加密
     */
    public static String encryptEcb(String hexKey, String paramStr, String charset) throws Exception {
        String cipherText = "";
        if (null != paramStr && !"".equals(paramStr)) {
            byte[] keyData = ByteUtils.fromHexString(hexKey);
            charset = charset.trim();
            if (charset.length() <= 0) {
                charset = ENCODING;
            }
            byte[] srcData = paramStr.getBytes(charset);
            byte[] cipherArray = encrypt_Ecb_Padding(keyData, srcData);
            cipherText = ByteUtils.toHexString(cipherArray);
        }
        return cipherText;
    }

    /**
     *  @Description:加密模式之ecb
     */
    public static byte[] encrypt_Ecb_Padding(byte[] key, byte[] data) throws Exception {
        Cipher cipher = generateEcbCipher(ALGORITHM_NAME_ECB_PADDING, Cipher.ENCRYPT_MODE, key);
        byte[] bs = cipher.doFinal(data);
        return bs;
    }

    /**
     *  @Description:sm4解密
     */
    public static String decryptEcb(String hexKey, String cipherText, String charset) throws Exception {
        String decryptStr = "";
        byte[] keyData = ByteUtils.fromHexString(hexKey);
        byte[] cipherData = ByteUtils.fromHexString(cipherText);
        byte[] srcData = decrypt_Ecb_Padding(keyData, cipherData);
        charset = charset.trim();
        if (charset.length() <= 0) {
            charset = ENCODING;
        }
        decryptStr = new String(srcData, charset);
        return decryptStr;
    }

    /**
     *  @Description:解密
     */
    public static byte[] decrypt_Ecb_Padding(byte[] key, byte[] cipherText) throws Exception {
        Cipher cipher = generateEcbCipher(ALGORITHM_NAME_ECB_PADDING, Cipher.DECRYPT_MODE, key);
        return cipher.doFinal(cipherText);
    }

    /**
     *  @Description:密碼校驗
     */
    public static boolean verifyEcb(String hexKey,String cipherText,String paramStr) throws Exception {
        boolean flag = false;
        byte[] keyData = ByteUtils.fromHexString(hexKey);
        byte[] cipherData = ByteUtils.fromHexString(cipherText);
        byte[] decryptData = decrypt_Ecb_Padding(keyData,cipherData);
        byte[] srcData = paramStr.getBytes(ENCODING);
        flag = Arrays.equals(decryptData,srcData);
        return flag;
    }

    /**
     *  @Description:測試類
    */
    public static void main(String[] args) {
        try {
            String json = "{\"name\":\"color\",\"sex\":\"man\"}";
            // 自定義的32位16進位制金鑰
            String key = "cc9368581322479ebf3e79348a2757d9";
            String cipher = Sm4Utils.encryptEcb(key, json,ENCODING);
            System.out.println(cipher);
            System.out.println(Sm4Utils.verifyEcb(key, cipher, json));
            json = Sm4Utils.decryptEcb(key, cipher,ENCODING);
            System.out.println(json);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

我們來仔細看一下上面的程式碼,首先定義了個4個靜態不可修改都變數,用於下方方法的使用。包括編碼utf-8,密碼名稱sm4,密碼的分組方式SM4/ECB/PKCS7Padding和預設的key值長度128。
整體的方法我們分為生成ecb暗號,自動生成金鑰,加密,解密,密碼校驗的演算法。
以上為sm4utils的核心程式碼。

MD5Utils


/**
 *  @Description:md5加密工具
 */
public class Md5Utils {

    /**
     *  @Description:獲得md5加密串
     */
    public static String getMD5String(String str) {
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            md.update(str.getBytes());
            return new BigInteger(1, md.digest()).toString(16);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
}

MD5Utils主要用於對引數的完整性校驗,防止篡改。

此時我們已經實現了主要的加密解密工具,接下來實現spring的aop自定義註解,自定義註解我們要實現三個。
第一個是加密註解,用於方法上的,表示該方法的引數需要被加密。
第二個是解密註解,用於方法上的,表示該方法的引數需要被解密。
第三個是欄位加密解密註解,用於標識實體類的欄位是否需要被加密和解密。

加密的自定義註解

@Documented
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Order(-2147483648)
public @interface EncryptMethod {
}

可以看到此時打了一個order註解,-2147483648用於標識優先順序最高。

解密的自定義註解

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Order(-2147483648)
@Inherited
public @interface DecryptMethod {
}

加密解密欄位的自定義註解

@Documented
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Order(-2147483648)
public @interface EncryptField {
}

註解的準備工作已經做完,接下來就是實現sm4的切面方法。不瞭解spring的aop的實現方法,可以去先補一下spring的相關知識,這裡不做贅述。我們這裡採用的切面都是環繞通知,切面的切點是加密解密的註解。

加密解密切面的實現

說一下切面的答題實現思路。我們可以看到@ConditionalOnProperty(prefix = "sm4", value = "enable", matchIfMissing = false),這個我們做成了根據配置檔案的配置進行動態的開關。
我們在appication.yml檔案中進行如下的配置。這樣切面是否生效就取決於配置。然後我們捕捉加密的註解和解密的註解,然後對加了註解的方法中的邏輯進行加密和解密。
下方程式碼的切點方法為EncryptAOPCut和DecryptAOPCut。隨後通過around對切點進行捕捉。分別呼叫的核心的加密演算法encryptMethodAop和解密演算法decryptMethodAop。

sm4:
  enable: true

核心的加密解密演算法都是使用環繞通知的ProceedingJoinPoint類,從他的物件中我們可以取到spring的各種引數,包括request請求,請求的引數和response物件。


/**
 *  @Description:sm4加密解密切面
 */
@Order(-2147483648)
@Aspect
@Component
@ConditionalOnProperty(prefix = "sm4", value = "enable", matchIfMissing = false)
public class Sm4Aspect {

    private Logger log = LoggerFactory.getLogger(Sm4Aspect.class);
    private static final String DEFAULT_KEY = "cc9368581322479ebf3e79348a2757d9";

    public Sm4Aspect() {
    }

    @Pointcut("@annotation(com.jichi.aop.sm4.EncryptMethod)")
    public void EncryptAOPCut() {
    }

    @Pointcut("@annotation(com.jichi.aop.sm4.DecryptMethod)")
    public void DecryptAOPCut() {
    }

    @Around("EncryptAOPCut()")
    public Object encryptMethodAop(ProceedingJoinPoint joinPoint) {
        Object responseObj = null;
        try {
            responseObj = joinPoint.proceed();
            this.handleEncrypt(responseObj);
            //md5加密
            String md5Data = Md5Utils.getMD5String(new Gson().toJson(responseObj));
            SpringContextUtil.getHttpServletResponse().setHeader("md5",md5Data);
        } catch (Throwable throwable) {
            throwable.printStackTrace();
            this.log.error("encryptMethodAop處理出現異常{}", throwable);
        }
        return responseObj;
    }

    @Around("DecryptAOPCut()")
    public Object decryptMethodAop(ProceedingJoinPoint joinPoint) {
        Object responseObj = null;
        try {
            responseObj = joinPoint.getArgs()[0];
            //throw new RuntimeException("md5校驗失敗");
            this.handleDecrypt(responseObj);
            String md5 = "";
            md5 = Md5Utils.getMD5String(new Gson().toJson(responseObj));
            System.out.println(md5);
            String origianlMd5 = "";
            origianlMd5 = SpringContextUtil.getHttpServletRequest().getHeader("md5");
            if(origianlMd5.equals(md5)){
                responseObj = joinPoint.proceed();
            }else{
                this.log.error("引數的md5校驗不同,可能存在篡改行為,請檢查!");
                throw new Exception("引數的md5校驗不同,可能存在篡改行為,請檢查!");
            }
        } catch (Throwable throwable) {
            throwable.printStackTrace();
            this.log.error("decryptMethodAop處理出現異常{}", throwable);
        }
        return responseObj;
    }

    private void handleEncrypt(Object requestObj) throws Exception {
        if (!Objects.isNull(requestObj)) {
            Field[] fields = requestObj.getClass().getDeclaredFields();
            Field[] fieldsCopy = fields;
            int fieldLength = fields.length;
            for(int i = 0; i < fieldLength; ++i) {
                Field field = fieldsCopy[i];
                boolean hasSecureField = field.isAnnotationPresent(EncryptField.class);
                if (hasSecureField) {
                    field.setAccessible(true);
                    String plaintextValue = (String)field.get(requestObj);
                    String encryptValue = Sm4Utils.encryptEcb(DEFAULT_KEY, plaintextValue, "");
                    field.set(requestObj, encryptValue);
                }
            }
        }
    }

    private Object handleDecrypt(Object responseObj) throws Exception {
        if (Objects.isNull(responseObj)) {
            return null;
        } else {
            Field[] fields = responseObj.getClass().getDeclaredFields();
            Field[] fieldsCopy = fields;
            int fieldLength = fields.length;
            for(int i = 0; i < fieldLength; ++i) {
                Field field = fieldsCopy[i];
                boolean hasSecureField = field.isAnnotationPresent(EncryptField.class);
                if (hasSecureField) {
                    field.setAccessible(true);
                    String encryptValue = (String)field.get(responseObj);
                    String plaintextValue = Sm4Utils.decryptEcb(DEFAULT_KEY, encryptValue, "");
                    field.set(responseObj, plaintextValue);
                }
            }
            return responseObj;
        }
    }
}

程式碼實際應用

首先我們可以定義一個實體類,對實體類的欄位進行加密或解密的標識。我們這裡建立了一個Info實體類,對於其中的name屬性,我們加了註解加密解密欄位,對於sex屬性我們不做任何處理。

@Data
public class Info {

    @EncryptField
    private String name;

    private String sex;

}

然後我們對於controller方法打上加密的方法或解密的方法。

@RestController
@RequestMapping("/demo/test")
public class TestController {


    @PostMapping("/saveInfo")
    @DecryptMethod
    public HashMap<String,String> saveInfo(@RequestBody Info info) {
        HashMap<String,String> result = new HashMap<String,String>();
        String name = info.getName();
        System.out.println(name);
        String sex= info.getSex();
        System.out.println(sex);
        result.put("flag","1");
        result.put("msg","操作成功");
        return result;
    }

}

注意到方法上的註解@DecryptMethod,以為這著我們的這個方法將會進行解密。如果是@EncryptMethod,則代表對方法進行加密。

總結

到此為止,涉及到java後端的程式碼解決方案已經完畢。示例程式碼已經給出,大家可以直接使用,本人親測有效。文中難免有不足,歡迎大家批評指正。

相關文章