1.情景展示
Java提供的密鑰,C#無法解密。
2.原因分析
在Java中,AES的實際密鑰需要用到KeyGenerator 和 SecureRandom,但是C#和.NET 里面沒有這2個類,
所以,無法使用安全隨機數生成KEY,進而導致解密失敗。
Java對密鑰做的進一步處理:
參數說明:
加密模式:ECB(默認值)、CBC
填充模式:PKCS5Padding(java只有這一種,其它語言使用PKCS7Padding即可,5和7沒有區別)
數據塊:128位(java只有這一種)
3.解決方案
超級簡單的方法見最后(20190921)
方案一:推薦使用
思路:
將由Java生成的AES所需要的實際密鑰,提供給C#,然后C#用這個實際的key去解密。
由於C#中byte范圍是[0,255],而Java中的byte范圍是[-128,127],所以,我們需要對生成的二進制密鑰進行處理。
因此,Java作為密鑰的提供方,需要將二進制轉成16進制,C#將接收到的16進制密鑰轉換成二進制即可。
流程圖:
java AES 加密
import java.security.SecureRandom; import javax.crypto.Cipher; import javax.crypto.KeyGenerator; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; import org.apache.log4j.Logger; /** * AES加密算法工具類 * @explain 可逆算法:加密、解密 * AES/ECB/PKCS5Padding * @author Marydon * @creationTime 2018年7月7日下午2:17:43 * @version 3.0 * @since 2.0 * @email marydon20170307@163.com */ public class AESUtils { private static Logger log = Logger.getLogger(AESUtils.class); // 定義字符集 private static final String ENCODING = "UTF-8"; /** * 根據提供的密鑰生成AES專用密鑰 * @explain * @param password * 可以是中文、英文、16進制字符串 * @return AES密鑰 * @throws Exception */ public static byte[] generateKey(String password) throws Exception { byte[] keyByteArray = null; // 創建AES的Key生產者 KeyGenerator kgen = KeyGenerator.getInstance("AES"); // 利用用戶密碼作為隨機數初始化 // 指定強隨機數的生成方式 // 兼容linux SecureRandom random = SecureRandom.getInstance("SHA1PRNG"); random.setSeed(password.getBytes(ENCODING)); kgen.init(128, random);// 只能是128位 // 根據用戶密碼,生成一個密鑰 SecretKey secretKey = kgen.generateKey(); // 返回基本編碼格式的密鑰,如果此密鑰不支持編碼,則返回null。 keyByteArray = secretKey.getEncoded(); return keyByteArray; } /** * AES加密字符串 * @param content * 需要被加密的字符串 * @param password * 加密需要的密碼 * @return 16進制的密文(密文的長度隨着待加密字符串的長度變化而變化,至少32位) */ public static String encrypt(String content, String password) { String cipherHexString = "";// 返回字符串 try { // 轉換為AES專用密鑰 byte[] keyBytes = generateKey(password); SecretKeySpec sks = new SecretKeySpec(keyBytes, "AES"); // 將待加密字符串轉二進制 byte[] clearTextBytes = content.getBytes(ENCODING); // 創建密碼器,默認參數:AES/EBC/PKCS5Padding Cipher cipher = Cipher.getInstance("AES"); // 初始化為加密模式的密碼器 cipher.init(Cipher.ENCRYPT_MODE, sks); // 加密結果 byte[] cipherTextBytes = cipher.doFinal(clearTextBytes); // byte[]-->hexString cipherHexString = ByteUtils.toHex(cipherTextBytes); } catch (Exception e) { e.printStackTrace(); log.error("AES加密失敗:" + e.getMessage()); } log.info("AES加密結果:" + cipherHexString); return cipherHexString; } }
先調用generateKey()方法,然后將二進制轉換成16進制(如果byte[]轉16進制不會,見文末鏈接)。
C# AES 解密
/// <summary> /// AES 解密 /// </summary> /// <param name="toDecrypt">密文(待解密)</param> /// <param name="hexKey">密鑰(16進制)</param> /// <returns></returns> public static string AesDecrypt(string toDecrypt, string hexKey) { if (string.IsNullOrEmpty(toDecrypt)) return null; //將16進制的密文轉為字節數組 var toDecryptArray = new byte[toDecrypt.Length / 2]; for (var x = 0; x < toDecryptArray.Length; x++) { var i = Convert.ToInt32(toDecrypt.Substring(x * 2, 2), 16); toDecryptArray[x] = (byte)i; } //將16進制的秘鑰轉成字節數組 var keyArray = new byte[hexKey.Length / 2]; for (var x = 0; x < keyArray.Length; x++) { var i = Convert.ToInt32(hexKey.Substring(x * 2, 2), 16); keyArray[x] = (byte)i; } RijndaelManaged rm = new RijndaelManaged { Key = keyArray, Mode = CipherMode.ECB,//必須設置為ECB Padding = PaddingMode.PKCS7//必須設置為PKCS7 }; ICryptoTransform cTransform = rm.CreateDecryptor(); Byte[] resultArray = cTransform.TransformFinalBlock(toDecryptArray, 0, toDecryptArray.Length); return Encoding.UTF8.GetString(resultArray); }
測試
public static void main(String[] args) throws Exception { String text = "Marydon"; String password = "521"; System.out.println(ByteUtils.toHex(generateKey(password)));// FB511ED54B1B3D71093309D4F6DEBD61 // 加密 String encrypt = encrypt(text, password);// 7468F296C547B321AE1086741BAC13C4 }
方案二:密鑰使用16位的,自行百度。
方案三: 改變填充模式
Java默認的填充模式為PKCS5Padding,可以將Java和C#統一采用NoPadding,需要自己定義這種填充模式。
方案四:使用dll動態庫實現。
2019/05/08
.NET的解決方案與C#一樣。
/// <summary> /// 將16進制字符串轉二進制 /// </summary> /// <param name="hexString">需要進行解碼的字符串</param> /// <returns></returns> public static byte[] HexStrToByte(string hexString) { hexString = hexString.Replace(" ", ""); if ((hexString.Length % 2) != 0) hexString += " "; byte[] returnBytes = new byte[hexString.Length / 2]; for (int i = 0; i < returnBytes.Length; i++) returnBytes[i] = Convert.ToByte(hexString.Substring(i * 2, 2), 16); return returnBytes; } /// <summary> /// AES 解密 /// </summary> /// <param name="str">密文(待解密)</param> /// <param name="key">密鑰</param> /// <returns></returns> public static string AesDecrypt(string str, string key) { if (string.IsNullOrEmpty(str)) return null; //將16進制密文轉為字節數組 var toEncryptArray = new byte[str.Length / 2]; for (var x = 0; x < toEncryptArray.Length; x++) { var i = Convert.ToInt32(str.Substring(x * 2, 2), 16); toEncryptArray[x] = (byte)i; } //將16進制秘鑰轉成字節數組 var inputByteArray = HexStrToByte(key); RijndaelManaged rm = new RijndaelManaged { Key = inputByteArray, Mode = CipherMode.ECB,//必須設置為ECB Padding = PaddingMode.PKCS7//必須設置為PKCS7 }; ICryptoTransform cTransform = rm.CreateDecryptor(); Byte[] resultArray = cTransform.TransformFinalBlock(toEncryptArray, 0, toEncryptArray.Length); return Encoding.UTF8.GetString(resultArray); }
2019/08/28
C# AES加密
/// <summary> /// AES 加密 /// </summary> /// <param name="toEncrypt">明文(待加密)</param> /// <param name="hexKey">密鑰(確保java提供給你的是16進制密鑰,不是十進制!)</param> /// <returns>AES加密結果</returns> public static string AesEncrypt(string toEncrypt, string hexKey) { //將16進制秘鑰轉成字節數組 var keyArray = new byte[hexKey.Length / 2]; for (var x = 0; x < keyArray.Length; x++) { var i = Convert.ToInt32(hexKey.Substring(x * 2, 2), 16); keyArray[x] = (byte)i; } byte[] toEncryptArray = Encoding.UTF8.GetBytes(toEncrypt); RijndaelManaged rDel = new RijndaelManaged(); rDel.Key = keyArray; rDel.Mode = CipherMode.ECB; rDel.Padding = PaddingMode.PKCS7; ICryptoTransform cTransform = rDel.CreateEncryptor(); byte[] resultArray = cTransform.TransformFinalBlock(toEncryptArray, 0, toEncryptArray.Length); return ByteArrayToHexString(resultArray); } /// <summary> /// 將一個byte數組轉換成一個格式化的16進制字符串 /// </summary> /// <param name="data">byte數組</param> /// <returns>格式化的16進制字符串</returns> public static string ByteArrayToHexString(byte[] data) { StringBuilder sb = new StringBuilder(data.Length * 3); foreach (byte b in data) { sb.Append(Convert.ToString(b, 16).PadLeft(2, '0')); } return sb.ToString().ToUpper(); }
解決方案五:20190921 最省心
在實際對接過程中,每次都要和對方解釋半天才能搞定,真是浪費時間,我們不妨這樣想一想:
由於密碼生成器是java所獨有的,其它語言都不支持(IOS,ANDROID,C#,.NET等),既然java這么特立獨行,我們是不是可以不使用這個密碼生成器呢?
經實踐發現,只要不用java特有的密鑰生成器對初始密鑰做進一步處理,不論是哪一種語言,其加密結果都是一致的。
/** * AES加密字符串(兼容任何語言) * @explain 沒有使用java獨有的密碼生成器 * @param content * 需要被加密的字符串 * @param password * 加密需要的密碼 * @return 16進制的密文(密文的長度隨着待加密字符串的長度變化而變化,至少32位) */ public static String encrypt(String content, String password) { String cipherHexString = "";// 返回字符串 try { // 轉換為AES專用密鑰(這一步直接廢棄) // byte[] keyBytes = generateKey(password); // 直接當做密鑰使用(除java以外的語言,其它語言都是直接把它當做密鑰) byte[] keyBytes = password.getBytes(ENCODING); SecretKeySpec sks = new SecretKeySpec(keyBytes, "AES"); // 將待加密字符串轉byte[] byte[] clearTextBytes = content.getBytes(ENCODING); // 創建密碼器 Cipher cipher = Cipher.getInstance("AES"); // 初始化為加密模式的密碼器 cipher.init(Cipher.ENCRYPT_MODE, sks); // 加密結果 byte[] cipherTextBytes = cipher.doFinal(clearTextBytes); // byte[]-->hexString cipherHexString = ByteUtils.toHex(cipherTextBytes); } catch (Exception e) { e.printStackTrace(); log.error("AES加密失敗:" + e.getMessage()); } log.info("AES加密結果:" + cipherHexString); return cipherHexString; }
2019/10/14
php加密,解密
/** * 加密 * * @param $str * @return string */ function encrypt($str){ $secret = $this->getSecret(); $data = openssl_encrypt($str, 'aes-128-ecb', $secret, OPENSSL_PKCS1_PADDING); $data = bin2hex($data) return $data; } /** * 解密 * * @param $str */ function decrypt($str){ $data = hex2bin($str); $secret = $this->getSecret(); $data = openssl_decrypt($data, 'aes-128-ecb', $secret, OPENSSL_PKCS1_PADDING); return $data; } /** * 密鑰處理 */ function getSecret(){ // 16進制密鑰 $hex = 'F07D896FD9098039D0F666525FD9EDE2'; // 轉二進制 $hex = pack('H*', $hex); return $hex; }
2019/12/27
oracle加密
CREATE OR REPLACE FUNCTION FUN_ENCRYPT_AES(V_STR VARCHAR2, V_KEY VARCHAR2) RETURN VARCHAR2 AS V_KEY_RAW RAW(32); V_STR_RAW RAW(2000); V_RETURN_STR VARCHAR2(2000); V_TYPE PLS_INTEGER; BEGIN /************************************************* 加密函數 FUN_ENCRYPT_AES 入參: V_STR 待加密字符串 V_KEY 密鑰 返回值: V_RETURN_STR 返回密文字符串,約定返回為 16進制密文字符串 加密方式 128/ebc/pkcs5 密鑰位數:AES128 DBMS_CRYPTO.ENCRYPT_AES128 連接方式:EBC DBMS_CRYPTO.CHAIN_EBC 填充方式:PKCS5 DBMS_CRYPTO.PAD_PKCS5 **************************************************/ --將字符串varchar2轉換成位串raw,並按照utf-8格式進行解析 V_KEY_RAW := UTL_I18N.STRING_TO_RAW(V_KEY, 'AL32UTF8'); /*V_KEY_RAW := '40146E5CA7AF57D01959C0FAFB7B7330';*/ V_STR_RAW := UTL_I18N.STRING_TO_RAW(V_STR, 'AL32UTF8'); /*注意:需保證當前登錄用戶有調用包DBMS_CRYPTO的權限*/ -- 指定:密鑰算法、工作模式、填充方式 V_TYPE := DBMS_CRYPTO.ENCRYPT_AES128 + DBMS_CRYPTO.CHAIN_ECB + DBMS_CRYPTO.PAD_PKCS5; V_STR_RAW := DBMS_CRYPTO.ENCRYPT(SRC => V_STR_RAW, TYP => V_TYPE, KEY => V_KEY_RAW); V_RETURN_STR := RAWTOHEX(V_STR_RAW); RETURN V_RETURN_STR; END;
注意:這里的Key也是需要Java提前轉換成16進制的密鑰。
2020/01/02
powerbuilder(PB)
//AES/ECB/PKCS5Padding 加密 以 Hex 編碼返回 blob lbb_iv ls_param_encrypt = _codec.HexEncode(_crypto.SymEncrypt(1, _codec.ToUTF8(ls_param_set), _codec.HexDecode(is_aes_key), _crypto.CIPHER_MODE_ECB, lbb_iv))
2020/01/08
javascript
前提:引入cryptojs文件
<script type="text/javascript" src="crypt/crypto-js.js"></script>
aes加密、解密
/** * aes加密 * @param clearText 待加密字符串 * @param hexKey 加密密鑰,由java提供(16進制) * @explain AES/ECB/PKCS7Padding * 偏移量(填充方式):PKCS7Padding,對應java的PKCS5Padding * 加密模式:ECB * return 加密結果(16進制) */ function encrypt(clearText, hexKey) { var key = CryptoJS.enc.Hex.parse(hexKey); var encrytedData = CryptoJS.AES.encrypt(clearText, key, { mode : CryptoJS.mode.ECB, padding : CryptoJS.pad.Pkcs7 }); return encrytedData.ciphertext.toString().toUpperCase(); } /** * aes解密 * @param cipherText 待解密字符串(16進制) * @param hexKey 解密密鑰,由java提供(16進制) * return 解密結果(以utf-8進行編碼) */ function decrypt(cipherText, hexKey) { // 1.將16進制轉換成數組 var hexArray = CryptoJS.enc.Hex.parse(cipherText); // 2.將數組轉換成base64字符串 var base64Str = CryptoJS.enc.Base64.stringify(hexArray); // 3.將密鑰轉換成數組 var key = CryptoJS.enc.Hex.parse(hexKey); // 4.解密 var decryptedData = CryptoJS.AES.decrypt(base64Str, key, { mode : CryptoJS.mode.ECB, padding : CryptoJS.pad.Pkcs7 }); // 5.以utf-8進行編碼解密結果 return decryptedData.toString(CryptoJS.enc.Utf8); }
測試
window.onload = function() { var clearText = "張三"; var hexKey = "E341BACB74574E03051D2BB1FD48BD99"; var cipherText = encrypt(clearText, hexKey); console.log(cipherText);//6E3AC3434E1F8C371EA81DDB124AA5D7 clearText = decrypt(cipherText,hexKey); console.log(clearText);//張三 }