最近又被【現場破解共享單車系統】刷了一臉,不得不開始后怕:如何防止類似的情況發生?
想來想去,始終覺得將程序加密是最簡單的做法。但是摩拜、ofo也有加密,為什么仍然被破解?那是因為請求在傳輸過程中被篡改了關鍵參數,從而導致服務器做出了錯誤的響應。
如何防止篡改?自然是使用加密,加密方式的不同會決定篡改的成本(開頭故事里的哥們大概用了2天),目前大多數的加密方法是暴露其他參數,只加密一個token,然后服務端驗證token是否有效。從我的角度來講,這種加密方式雖然破解難度很高,但多半是用MD5、SHA1、hmacMd5或AES等方式進行加密,結果往往是不可逆的,並且固定內容加密——不管多少次結果總是相同的。
對於部分人來講,只要你暴露出來的數據就可以做很多事情了,而且如果使用這種加密手段,那么服務端和客戶端必須約定好加密手段和參與加密的字段(規則),這就相當於:我(服務端)和你(客戶端)一起設計了鑰匙,然后在需要的時候(發起請求)你就往指定地方(服務器)丟一個上了鎖的箱子(加密后的數據),然后我制作鑰匙打開鎖。我也可以丟箱子,然后你用同樣方式去打開。
大家可以打開的鎖安全么?想想都不靠譜。
更好的是什么?我們設想一下:我(服務端)給你(客戶端)制造鎖的圖紙,我有制造鑰匙的圖紙,在你需要的時候(發起請求)就做一把鎖,然后把貨物(原始數據)放在箱子里鎖好(加密完成)丟在指定的地方(服務器),然后我制作出鑰匙,開鎖拿走貨物(解密)。
現在感覺如何?就像情報機構,偵察員只負責按照規則傳遞密碼,而總部按照另一套規則解讀,這個過程中就算被監聽到,第三方也無法解讀——這種方式就是今天要說的RSA。
該怎么做呢?很簡單:基於現在基本都是用http交互數據,相互傳輸數據用json再好不過,所以我會要求客戶端將所有參數以json格式全部拼裝起來,然后整串加密后傳輸到服務器,請求內容看上去是這樣的:
完全不知道這是一串什么鬼(如果我不是服務器的話):實際上這是一次登陸請求,里面包含賬戶、密碼、時間戳、執行方法等,在服務端解密后是這樣的:
{"msg":{"method":"log_in","time":"2017-09-28 10:30:27","user_name":"test","user_pwd":"test"}}
這就完成了一次正確的數據提交。並且如果有人修改了密文都會導致服務端解密失敗從而駁回請求。這種情況下,就算你破譯了整套客戶端,也僅僅能拿到一個公鑰,而遠在服務端的私鑰還是好好的(除非你已經攻破了人家的服務器),從而阻止參數在傳遞過程中被篡改(下面上點干貨)。
生成公私鑰:
/// <summary> /// RSA 的密鑰產生 產生私鑰 和公鑰 /// </summary> /// <param name="priKeys">私鑰</param> /// <param name="pubKey">公鑰</param> public static void rsaKey(out string priKeys, out string pubKey) { RSACryptoServiceProvider rsa = new RSACryptoServiceProvider(); priKeys = rsa.ToXmlString(true); pubKey = rsa.ToXmlString(false); }
RSA加密:
/// <summary> /// RSA加密(不限長度) /// </summary> /// <param name="publicKey">公鑰</param> /// <param name="text">要加密的文本</param> /// <param name="enc">編碼方式</param> /// <returns>密文</returns> public static string rsaEncrypt(string pubKey, string text, Encoding enc) { using (var rsaProvider = new RSACryptoServiceProvider()) { var inputBytes = enc.GetBytes(text); rsaProvider.FromXmlString(pubKey); int bufferSize = (rsaProvider.KeySize / 8) - 11; var buffer = new byte[bufferSize]; using (MemoryStream inputStream = new MemoryStream(inputBytes), outputStream = new MemoryStream()) { while (true) { int readSize = inputStream.Read(buffer, 0, bufferSize); if (readSize <= 0) { break; } var temp = new byte[readSize]; Array.Copy(buffer, 0, temp, 0, readSize); var encryptedBytes = rsaProvider.Encrypt(temp, false); outputStream.Write(encryptedBytes, 0, encryptedBytes.Length); } return Convert.ToBase64String(outputStream.ToArray()); } } }
RSA解密:
/// <summary> /// rsa解密 /// </summary> /// <param name="priKey">私鑰</param> /// <param name="encrypted">要解密的字符串</param> /// <param name="enc">編碼方式</param> /// <returns>明文</returns> public static string rsaDecrypt(string priKey, string encrypted, Encoding enc) { if (string.IsNullOrEmpty(encrypted)) { return string.Empty; } if (string.IsNullOrWhiteSpace(priKey)) { throw new ArgumentException("Invalid Private Key"); } using (var rsaProvider = new RSACryptoServiceProvider()) { var inputBytes = Convert.FromBase64String(encrypted); rsaProvider.FromXmlString(priKey); int bufferSize = rsaProvider.KeySize / 8; var buffer = new byte[bufferSize]; using (MemoryStream inputStream = new MemoryStream(inputBytes), outputStream = new MemoryStream()) { while (true) { int readSize = inputStream.Read(buffer, 0, bufferSize); if (readSize <= 0) { break; } var temp = new byte[readSize]; Array.Copy(buffer, 0, temp, 0, readSize); var rawBytes = rsaProvider.Decrypt(temp, false); outputStream.Write(rawBytes, 0, rawBytes.Length); } return enc.GetString(outputStream.ToArray()); } } }
因為本人對js也有一些興趣,所以嘗試着利用js與服務器交互,然鵝發現js需要java格式的公私鑰,於是找到了轉換公私鑰的代碼,親測有效,在這里搬運下,利己利他:
/// <summary> /// RSA公鑰格式轉換(net轉java) /// </summary> /// <param name="pubKey">net格式公鑰</param> /// <returns>java格式的公鑰</returns> public static string rsaPubKeyNet2Java(string pubKey) { XmlDocument doc = new XmlDocument(); doc.LoadXml(pubKey); BigInteger m = new BigInteger(1, Convert.FromBase64String(doc.DocumentElement.GetElementsByTagName("Modulus")[0].InnerText)); BigInteger p = new BigInteger(1, Convert.FromBase64String(doc.DocumentElement.GetElementsByTagName("Exponent")[0].InnerText)); RsaKeyParameters pub = new RsaKeyParameters(false, m, p); SubjectPublicKeyInfo publicKeyInfo = SubjectPublicKeyInfoFactory.CreateSubjectPublicKeyInfo(pub); byte[] serializedPublicBytes = publicKeyInfo.ToAsn1Object().GetDerEncoded(); return Convert.ToBase64String(serializedPublicBytes); } /// <summary> /// RSA私鑰格式轉換(net轉java) /// </summary> /// <param name="priKey">net格式私鑰</param> /// <returns>java格式的私鑰</returns> public static string rsaPriKeyNet2Java(string priKey) { XmlDocument doc = new XmlDocument(); doc.LoadXml(priKey); BigInteger m = new BigInteger(1, Convert.FromBase64String(doc.DocumentElement.GetElementsByTagName("Modulus")[0].InnerText)); BigInteger exp = new BigInteger(1, Convert.FromBase64String(doc.DocumentElement.GetElementsByTagName("Exponent")[0].InnerText)); BigInteger d = new BigInteger(1, Convert.FromBase64String(doc.DocumentElement.GetElementsByTagName("D")[0].InnerText)); BigInteger p = new BigInteger(1, Convert.FromBase64String(doc.DocumentElement.GetElementsByTagName("P")[0].InnerText)); BigInteger q = new BigInteger(1, Convert.FromBase64String(doc.DocumentElement.GetElementsByTagName("Q")[0].InnerText)); BigInteger dp = new BigInteger(1, Convert.FromBase64String(doc.DocumentElement.GetElementsByTagName("DP")[0].InnerText)); BigInteger dq = new BigInteger(1, Convert.FromBase64String(doc.DocumentElement.GetElementsByTagName("DQ")[0].InnerText)); BigInteger qinv = new BigInteger(1, Convert.FromBase64String(doc.DocumentElement.GetElementsByTagName("InverseQ")[0].InnerText)); RsaPrivateCrtKeyParameters privateKeyParam = new RsaPrivateCrtKeyParameters(m, exp, d, p, q, dp, dq, qinv); PrivateKeyInfo privateKeyInfo = PrivateKeyInfoFactory.CreatePrivateKeyInfo(privateKeyParam); byte[] serializedPrivateBytes = privateKeyInfo.ToAsn1Object().GetEncoded(); return Convert.ToBase64String(serializedPrivateBytes); }
進行轉換需要用到一個三方類庫,我的百度網盤里可以下載 http://pan.baidu.com/s/1b9Wy42,可以直接使用。
js的RSA加密也要用到一個庫,在這里捎帶貼一下:
<script type="text/javascript" src="https://passport.cnblogs.com/scripts/jsencrypt.min.js"></script> <script> //實例化對象 var crypt = new JSEncrypt(); //設置私鑰 crypt.setPrivateKey('你的RSA公鑰'); $('#log_in').click(function () { //拼裝發送數據 var post_data = crypt.encrypt('{"msg":{"method":"log_in","time":"' + Tool.getNowTime() + '","user_name":"' + $('.name').val() + '","user_pwd":"' + $('.pwd').val() + '"}}'); //在這里發起你的請求 }); </script>
因為js可以被看到,所以你更需要RSA,因為全部被看到也無所謂了。希望對大家有幫助~