前言
老張和Apollo分處中美兩國,是生意上的合作伙伴。Apollo在美國經營一家商業軟件設計公司,他會根據最新的市場需求進行軟件產品設計,然后將詳細設計方案交由老張的軟件外包公司完成軟件開發。最初他們是這樣交流的:
- Apollo通過郵件/IM工具將具有商業秘密的詳細設計方案直接發送給老張;
- 老張根據方案完成軟件開發,並將源碼以同樣的方式發送給Apollo。
這種方式方便快捷,Apollo既可以在美國這樣一個處於軟件業流行前沿的國家,更好地把握行業發展方向;又可以利用發展中國家的廉價勞動力,降低軟件產品成本。
但是,不好的事情很快發生了,每當Apollo設計出一款新產品,馬上就會被人山寨出來,有時甚至在老張還沒有開發完成前,市面上就已經有雷同的產品在售了。Apollo逐漸意識到互聯網在提供方便快捷的同時,也蘊藏着巨大的信息安全風險。他覺得是時候為此做些什么了。
V1.0 - 簡單編碼
Apollo認為免費的郵箱和IM工具不靠譜,得自己做一個通訊工具,才能按需對信息進行保護。這個東西很簡單嘛,就是最基礎的網絡通訊,Apollo花了一周的業余時間就搞定了。他還對數據做了簡單的倒序編碼保護,算法如下:
1 /// <summary> 2 /// 簡單編碼 3 /// </summary> 4 /// <param name="plainText">原文</param> 5 /// <returns>密文</returns> 6 public static string SimpleEncoding(string plainText) 7 { 8 var array = plainText.ToCharArray(); 9 Array.Reverse(array); 10 return new string(array); 11 } 12 13 /// <summary> 14 /// 簡單解碼 15 /// </summary> 16 /// <param name="cipherText">密文</param> 17 /// <returns>原文</returns> 18 public static string SimpleDecoding(string cipherText) 19 { 20 var array = cipherText.ToCharArray(); 21 Array.Reverse(array); 22 return new string(array); 23 }
當Apollo有新方案要給老張時,他這樣對方案信息進行編碼:
CryptoUtil.SimpleEncoding("方案");
老張拿到方案后,這樣進行解碼:
CryptoUtil.SimpleDecoding("案方");
經過這樣的編碼加密處理后,信息的流轉如下圖所示:
圖1簡單編碼保護
簡單對方案內容倒序編碼保護,一開始尚能瞞天過海,但由於編碼規則過於簡單,很快便被識破了。
V1.1 - 復雜編碼
Apollo決定對編碼算法進行改善,改用Base64編碼保護,算法如下:
1 /// <summary> 2 /// 編碼 3 /// </summary> 4 /// <param name="plainText">原文</param> 5 /// <returns>密文</returns> 6 public static string Encoding(string plainText) 7 { 8 var data = System.Text.Encoding.UTF8.GetBytes(plainText); 9 var cipherText = System.Convert.ToBase64String(data); 10 return cipherText; 11 } 12 13 /// <summary> 14 /// 解碼 15 /// </summary> 16 /// <param name="cipherText">密文</param> 17 /// <returns>原文</returns> 18 public static string Decoding(string cipherText) 19 { 20 var data = System.Convert.FromBase64String(cipherText); 21 var plainText = System.Text.Encoding.UTF8.GetString(data); 22 return plainText; 23 }
當Apollo有方案要發送給老張時,他這樣對信息進行編碼:
CryptoUtil.Encoding("方案");
老張拿到方案后,這樣進行解碼:
CryptoUtil.Decoding("5pa55qGI");
經過這樣的編碼加密處理后,信息的流轉如下圖所示:
圖2復雜編碼保護
傳輸的信息不僅內容與原文不同,而且長度也不一樣,看起來貌似還挺安全的。於是,Apollo與老張決定采用自主研發的這個通訊工具進行信息交互。試運行3個月效果不錯,Apollo沒有再被山寨問題所煩惱,從而能將精力更多地投入到新產品設計上。但好景不長,不久,問題似乎又有所反復。
V2.0 - 對稱加密
Apollo又對問題進行了深入的分析,覺得問題出在每次都按同樣的規律進行編碼,次數越多,越容易被人猜到編碼規則。就好像下一局象棋,馬后炮足以一招致勝,但如果每局都用馬后炮,那注定不能常勝。
怎么辦呢,不斷更換規則嗎?似乎后期維護太麻煩傷不起;增加編碼規則復雜度嗎?似乎治標不治本。Apollo清楚問題的實質在於編碼技術是基於規則的安全技術,一旦規則曝光,誰都可以解密。因此,他選擇了更好的解決方案:使用基於密鑰的安全技術——對稱加密。
所謂基於密鑰的安全,就是在密碼算法中引入了密鑰因子,使用加密密鑰加密的數據,只有提供唯一對應的解密密鑰才能解密。對稱加密即加密密鑰和解密密鑰是同一個密鑰。Apollo使用對稱加密技術后的算法大致如下:
1 /// <summary> 2 /// 對稱加密 3 /// </summary> 4 /// <param name="plainText">原文</param> 5 /// <param name="symmetricKey">對稱密鑰</param> 6 /// <returns>密文</returns> 7 public static byte[] SymmetricEncrypt(byte[] plainText, byte[] symmetricKey) 8 { 9 var cipher = SecurityContext.Current.CipherProvider; 10 using (var encryptor = cipher.CreateEncryptor()) 11 { 12 return encryptor.SymmetricEncrypt(plainText, symmetricKey); 13 } 14 } 15 16 /// <summary> 17 /// 對稱解密 18 /// </summary> 19 /// <param name="cipherText">密文</param> 20 /// <param name="symmetricKey">對稱密鑰</param> 21 /// <returns>原文</returns> 22 public static byte[] SymmetricDecrypt(byte[] cipherText, byte[] symmetricKey) 23 { 24 var cipher = SecurityContext.Current.CipherProvider; 25 using (var encryptor = cipher.CreateEncryptor()) 26 { 27 return encryptor.SymmetricDecrypt(cipherText, symmetricKey); 28 } 29 }
這樣,Apollo和老張約定一個對稱密鑰,當Apollo有新方案要發送給老張時,他就用這個固定的對稱密鑰進行加密:
1 var plainText = Encoding.UTF8.GetBytes("方案"); 2 var symmetricKey = Apollo.Common.Utils.FileUtil.ReadFile("SymmetricKey.dat"); 3 var cipherText = CryptoUtil.SymmetricEncrypt(plainText, symmetricKey);
Apollo將加密后的方案文件傳輸給老張,老張拿到密文和密鑰后,這樣解密:
1 var symmetricKey = Apollo.Common.Utils.FileUtil.ReadFile("SymmetricKey.dat"); 2 var plainText = CryptoUtil.SymmetricDecrypt(cipherText, symmetricKey);
經過對稱加密后的方案傳輸情況如下圖所示:
圖3對稱加密保護
經過對稱加密后,密文表現為一系列毫無意義的二進制數,想來應該是相當安全了。事實證明了這點,Apollo又安身了大半年。但最終還是道高一尺魔高一丈,方案在一段時間后又出現了泄露問題,這是為什么呢?
V2.1 - 動態密鑰對稱加密
任何事情一旦重復多次,都會被人找到規律,這是目前問題的關鍵。Apollo每次都用相同的對稱密鑰加密數據,次數越多越容易被他人發現規律,從而破解。Apollo想出了一個巧妙的解決辦法,他每次加密都對當日的日期作SHA1摘要運算,得到20字節的摘要值,並用0在其后填充12個字節,最終得到32字節的對稱密鑰。對稱加解密算法調整為:
1 /// <summary> 2 /// 根據當前日期產生對稱密鑰 3 /// </summary> 4 /// <returns></returns> 5 private static byte[] GenerateSymmetricKey() 6 { 7 var sha1 = SHA1.Create(); 8 var data = Encoding.Default.GetBytes(DateTime.Now.ToShortDateString()); 9 var symmetricKey = sha1.ComputeHash(data); 10 Apollo.Common.Utils.ByteUtil.Append(ref symmetricKey, new byte[12]); 11 12 return symmetricKey; 13 } 14 15 /// <summary> 16 /// 對稱加密 17 /// </summary> 18 /// <param name="plainText">原文</param> 19 /// <returns>密文</returns> 20 public static byte[] SymmetricEncrypt(byte[] plainText) 21 { 22 var symmetricKey = GenerateSymmetricKey(); 23 return SymmetricEncrypt(plainText, symmetricKey); 24 } 25 26 /// <summary> 27 /// 對稱解密 28 /// </summary> 29 /// <param name="cipherText">密文</param> 30 /// <returns>原文</returns> 31 public static byte[] SymmetricDecrypt(byte[] cipherText) 32 { 33 var symmetricKey = GenerateSymmetricKey(); 34 return SymmetricDecrypt(cipherText, symmetricKey); 35 }
相應地,Apollo發送方案時的做法調整為:
1 var plainText = Encoding.UTF8.GetBytes("方案"); 2 var cipherText = CryptoUtil.SymmetricEncrypt(plainText);
Apollo將加密后的方案文件傳輸給老張,然后電話告訴老張密鑰。老張拿到密文和密鑰后,這樣解密:
var plainText = CryptoUtil.SymmetricDecrypt(cipherText);
這種方式實現了更為安全的一次一密,而且Apollo和老張都不用再關心密鑰的事情了。但這個算法並不是無懈可擊的,它的短板就在密鑰本身——密鑰的產生是有規律性的(就是本節提到的密鑰產生算法),這實際是把基於規則的原文安全轉移為了基於規則的密鑰安全,只是密鑰不需要分發。
Apollo也想過用隨機數作為密鑰,這樣就能補上這塊短板,但隨之而來的是繁瑣的密鑰分發管理工作(每次加密使用的對稱密鑰都要告知老張)。
V3.0 - 對稱+非對稱加密
Apollo現在是左右為難。一方面,如果沿用目前的算法,密鑰的機密性短板問題可能讓他所有的努力功虧一簣;另一方面,如果改為隨機的一次一密,密鑰分發問題同樣令他頭大。Apollo決定迎難而上,使用對稱+非對稱加密技術來解決對稱密鑰的分發問題。
在開始對稱+非對稱加密技術之前,讓我們先來了解下非對稱加密。所謂非對稱加密,是指加密和解密運算所使用的密鑰是不相同,且總是成對的。其中一個密鑰叫私鑰,由密鑰所有人唯一持有;另一個密鑰叫公鑰,對所有人公開。使用其中一個密鑰加密的數據,僅能由配對的另一個密鑰解密。
非對稱加密的以上特性給Apollo提供了強有力的技術支持。他產生了兩對RSA1024密鑰對,一對自己留用,一對給老張,並且把老張的公鑰保留了一份,把自己的公鑰也一並給了老張。Apollo將加解密密算法調整為:
1 /// <summary> 2 /// 對稱加密 3 /// </summary> 4 /// <param name="plainText">原文</param> 5 /// <param name="symmetricKey">對稱密鑰</param> 6 /// <returns>密文</returns> 7 public static byte[] SymmetricEncrypt(byte[] plainText, out byte[] symmetricKey) 8 { 9 var cipher = SecurityContext.Current.CipherProvider; 10 using (var keyGenerator = cipher.CreateKeyGenerator()) 11 { 12 symmetricKey = keyGenerator.GenerateRandom(32); 13 } 14 15 return SymmetricEncrypt(plainText, symmetricKey); 16 } 17 18 /// <summary> 19 /// 對稱解密 20 /// </summary> 21 /// <param name="cipherText">密文</param> 22 /// <param name="symmetricKey">對稱密鑰</param> 23 /// <returns>原文</returns> 24 public static byte[] SymmetricDecrypt(byte[] cipherText, byte[] symmetricKey) 25 { 26 var cipher = SecurityContext.Current.CipherProvider; 27 28 using (var encryptor = cipher.CreateEncryptor()) 29 { 30 return encryptor.SymmetricDecrypt(cipherText, symmetricKey); 31 } 32 } 33 34 /// <summary> 35 /// 非對稱加密 36 /// </summary> 37 /// <param name="plainText">原文</param> 38 /// <param name="publicKey">加密公鑰</param> 39 /// <returns>密文</returns> 40 public static byte[] AsymmetricEncrypt(byte[] plainText, PublicKey publicKey) 41 { 42 var cipher = SecurityContext.Current.CipherProvider; 43 44 using (var encryptor = cipher.CreateEncryptor()) 45 { 46 return encryptor.AsymmetricEncrypt(plainText, publicKey); 47 } 48 } 49 50 /// <summary> 51 /// 非對稱解密 52 /// </summary> 53 /// <param name="cipherText">密文</param> 54 /// <returns>明文</returns> 55 public static byte[] AsymmetricDecrypt(byte[] cipherText) 56 { 57 var cipher = SecurityContext.Current.CipherProvider; 58 using (var encryptor = cipher.CreateEncryptor()) 59 { 60 return encryptor.AsymmetricDecrypt(cipherText); 61 } 62 }
這樣,當他有新方案要發送給老張時,使用上面的對稱加密算法加密方案文件,同時獲得一個隨機對稱密鑰:
1 var data = Encoding.UTF8.GetBytes(plainText); 2 byte[] symmetricKey = null; 3 var cipherText = CryptoUtil.SymmetricEncrypt(data, out symmetricKey);
然后用老張的公鑰加密該對稱密鑰:
var encryptedSymmetricKey = CryptoUtil.AsymmetricEncrypt(symmetricKey, publicKey);
完成后,將方案文件密文和對稱密鑰密文發給老張。老張拿到這兩件東西后,首先用自己的私鑰解密對稱密鑰密文,獲得對稱密鑰原文:
var symmetricKey = CryptoUtil.AsymmetricDecrypt(encryptedSymmetricKey);
然后用對稱密鑰解密方案密文,得到方案原文:
var plainText = CryptoUtil.SymmetricDecrypt(cipherText, decryptedSymmetricKey);
這套對稱+非對稱加密保護方案的信息傳遞過程如下圖所示:
圖4對稱+非對稱加密保護
V3.1 - 數字信封
對稱+非對稱加密保護方案技術上已經能保證方案的機密性了,但數據的發送仍略顯繁瑣(需要將對稱密鑰密文和方案文件密文分別發給老張,並且要告知老張哪個是對稱密鑰密文,哪個是方案文件密文)。Apollo是一個怕麻煩的人,所以他決定努力解決這個問題。
經過一翻研究,Apollo了解到PKCS #7(RFC2315)標准中已經定義了數字信封的ASN1數據結構,其主要結構定義如下:
EnvelopedData ::= SEQUENCE { version Version, // 語法版本 recipientInfos RecipientInfos, // 接收者信息 encryptedContentInfo EncryptedContentInfo, // 數據密文 }
EnvelopedData數據的生成與解析需要數字證書(參見X.509標准)支持,為此,Apollo向第三方運營CA申請購買了兩份證書及私鑰(參見PKCS#12標准)。Apollo將加解密密算法調整為:
1 /// <summary> 2 /// 封裝數字信封 3 /// </summary> 4 /// <param name="plainText">原文</param> 5 /// <param name="cert">接收方證書</param> 6 /// <returns>數字信封</returns> 7 public static byte[] ToEnvelopedData(byte[] plainText, X509Certificate cert) 8 { 9 var cipher = SecurityContext.Current.CipherProvider; 10 11 using (var encryptor = cipher.CreateEncryptor()) 12 { 13 var envelopedData = encryptor.ToEnvelopedData(plainText, cert); 14 return envelopedData.GetDerEncoded(); 15 } 16 } 17 18 /// <summary> 19 /// 拆開數字信封 20 /// </summary> 21 /// <param name="cipherText">數字信封</param> 22 /// <returns>原文</returns> 23 public static byte[] FromEnvelopedData(byte[] cipherText) 24 { 25 var cipher = SecurityContext.Current.CipherProvider; 26 var envelopedData = EnvelopedData.GetInstance(Asn1Object.FromByteArray(cipherText)); 27 28 using (var encryptor = cipher.CreateEncryptor()) 29 { 30 return encryptor.FromEnvelopedData(envelopedData); 31 } 32 }
這樣,當他有新方案要發送給老張時,使用上面的算法將方案文件封裝為數字信封:
1 var data = Encoding.UTF8.GetBytes(plainText); 2 var cert = X509Certificate.Parse(FileUtil.ReadFile(@"..\..\..\Reference\老張.cer")); 3 var cipherText = CryptoUtil.ToEnvelopedData(data, cert);
老張拿到數字信封后,使用上面的算法拆開數字信封,得到方案原文:
CryptoUtil.FromEnvelopedData(cipherText);
數字信封加密保護方案的信息傳遞過程如下圖所示:
圖5數字信封加密保護
走到這一步,Apollo表示非常滿意了,既實現了一次一密,使數據的機密性得到保障;又不用為密鑰的分發問題操心,真的是一勞永逸了。
V4.0 - 未完待續
雖然Apollo的通訊工具已更新到了V3.x版本,但問題只解決了一半。截至目前,他所做的所有努力只解決了方案明文不被非法獲取,即保證了方案數據的機密性。除此之外,其實還存在以下安全需求:
一、真實性
真實性也叫不可抵賴性,指的是從數據本身就能識別出該數據源於何人。保證數據的真實性很重要,首先,老張在收到一個方案數據時,需要確認是否來自Apollo,而不是被假冒的;其次,一旦發生糾紛,Apollo不能否認該方案不是來自他的。
二、完整性
數據的完整性保護指的是,接收方獲取數據后,可以驗證數據是否和源數據一致,從而確定數據是否被篡改。
數據的真實性和完整性保護一般采用數字簽名技術來實現。PKCS #7(RFC2315)中定義的SignedData類型可用於封裝數字簽名數據;另外,PKCS #7(RFC2315)中還定義了SignedAndEnvelopedData類型,可用於封裝數字簽名和數字信封結構數據。
三、時效性
再想多點的話,數據還需要保證時效性,即需要證明該數據是在哪個時間產生的。這樣,一旦發生糾紛,Apollo便不能否認他發送方案數據給老張的時間。
除此之外,該通訊工具還有一個短板,即證書及私鑰是以文件的形式存在通訊雙方的電腦中的,一旦文件被非法竊取,便無安全可言。為此,可以考慮將證書及私鑰文件改為使用USBKey硬件,這樣便可杜絕私鑰被復制的風險,做到絕對安全。
總結
本文以敘事的形式,循序漸進的講述了密碼應用技術的發展過程,其中包含了古典密碼學和現代密碼學的典型密碼技術,也包含了一些信息安全技術。如果你沒有這方面的基礎,可能會看得有點暈。不過沒有關系,本文只是對上述各種技術的概要論述,其中許多細節均未涉及,后續將針對這些技術作專題論述,並在結束篇中實現本文最終設計的通訊工具(包括未完待續部分)。希望本文對你了解密碼應用技術有一定的幫助。