前言
前文說明了 Base64 的原理、索引字串的關鍵性角色、用固定種子亂數為基因演化出一整串的亂數集合,現在再試著實作一個包含前述手法的客製化資料加解密系統。
程式功能需求及基本做法
參考比較標準 Base64 的編碼及輸出格式後,決定只在文字輸出時採用 Base64 字串的 64 個字元,其他的都調整一下:
- 程式可對任何形式的資料進行加密、解密。例如執行檔、影音檔,或程式中以 Binary 序列化的物件等等。
- 支援字串操作的輸入輸出的介面。
- 程式碼如下:
'---以 Unicode 編碼為 Ku64Ux 字串--- Function Ku64_Ux_Encode(ByVal 字串 As String, Optional ByVal 金鑰 As String = "", Optional ByVal l As Integer = 76) As String Dim 編碼方式 As New UnicodeEncoding Dim src() = 陣列加密(編碼方式.GetBytes(字串)) '---把來源字串轉為 Unicode byte() 陣列再加密 Dim 結果 = 字串寬度修飾(Array_To_ku64(src, 金鑰), l) '---進行加密運算後以固定寬度輸出編碼後的字串 Return 結果 '---進行加密運算後以固定寬度輸出編碼後的字串 End Function '---解碼 Ku64Ux 為 Unicode 字串--- Public Function Ku64_Ux_Decode(ByVal 字串 As String, Optional ByVal 金鑰 As String = "") As String Ku64_Ux_Decode = "" If 字串 = "" Then Exit Function Dim tmp() As Byte = 陣列解密(Ku64_To_Array(字串, 金鑰)) If tmp Is Nothing Then MsgBox(解碼錯誤訊息 & IIf(金鑰 = "", "", ",或金鑰不正確") & "。") : Return "" Dim code As New UnicodeEncoding Return code.GetChars(tmp) End Function
- 核心動作都在二進位的 Byte() 陣列中進行
- 程式碼如下:
'-------------------------------------------------------------- ' 用自己的演算法對 Binary() 陣列加密 '-------------------------------------------------------------- Function 陣列加密(ByVal 原始陣列() As Byte) As Byte() Dim 亂數基因 As Double = 基礎亂數產生器.NextDouble '---先產生一個 Double 型別的亂數基因 Dim 基因陣列() As Byte = BitConverter.GetBytes(亂數基因) '---轉 Double 為 Byte 陣列 Dim 亂數陣列 = 由基因演化出陣列(亂數基因) '---建立一組亂數 Dim tmp(UBound(原始陣列) + 基因陣列.Length + 16) As Byte '---準備輸出用的陣列空間 基因陣列.CopyTo(tmp, 0) '--- Copy 基因到輸出陣列 For i = 0 To UBound(原始陣列) '---依序和亂數陣列做運算 tmp(i + 基因陣列.Length) = 原始陣列(i) Xor 亂數陣列(i Mod 亂數陣列.Length) Next Dim MD5陣列 = MD5_Binary(tmp) '---附加 MD5 驗證碼到輸出陣列--- MD5陣列.CopyTo(tmp, UBound(tmp) - 15) Return tmp End Function '-------------------------------------------------------------- ' 用自己的演算法解密 Binary() 陣列 '-------------------------------------------------------------- Function 陣列解密(ByVal 密文陣列() As Byte) As Byte() 陣列解密 = Nothing Try Dim 亂數基因 As Double = BitConverter.ToDouble(密文陣列, 0) '---從陣列最前取出基因 Dim 亂數陣列 = 由基因演化出陣列(亂數基因) '---解回亂數陣列 Dim 編碼時的驗證值(15) As Byte '---建立陣列存放 MD5 CheckSum If 密文陣列.Length >= 16 Then Array.Copy(密文陣列, 密文陣列.Length - 16, 編碼時的驗證值, 0, 16) '---從密文陣列取出夾帶的 MD5 驗證值 Array.Clear(密文陣列, 密文陣列.Length - 16, 16) '---清空密文最後 16 個元素 Dim 當下資料驗證值() = MD5_Binary(密文陣列) '---重新計算 MD5 驗證值 If MD5_String(編碼時的驗證值) <> MD5_String(當下資料驗證值) Then Exit Function '---比對若不正確就傳回 Nothing Dim tmp(密文陣列.Length - 1 - 8 - 16) As Byte '---修正為原始陣列長度(扣減亂數基因和驗證區的長度) For i = 0 To UBound(tmp) tmp(i) = 密文陣列(i + 8) Xor 亂數陣列(i Mod 亂數陣列.Length) Next Return tmp End If Catch ex As Exception End Try End Function
- 以字串輸出時,所使用的字元和 Base64 同為 A-Z,a-z,0-9 及 +/。並可設定輸出寬度(Default=76)。
- 仍使用數底轉換為基礎。
- 寫一個字串切割函式處理固定寬度,程式碼如下:
Function 字串寬度修飾(ByVal ret As String, ByVal Width As Integer) '---以固定寬度輸出編碼結果--- Dim tmp As String = "" While Len(ret) > Width tmp = tmp & Left(ret, Width) & vbCrLf ret = Mid(ret, Width + 1) End While If Len(ret) > 0 Then tmp = tmp & ret End If Return tmp End Function
- 編碼時可選擇性使用金鑰上鎖,金鑰可為任何長度的字串。
- 需重新撰寫 Base64 的編解碼函式,數底轉換自己來做,程式碼如下:
'-------------------------------------------------------------- '函數功能:編碼 Byte() 陣列為 Ku64 字串 '-------------------------------------------------------------- Function Array_To_ku64(ByVal 原始陣列() As Byte, ByVal 金鑰 As String) As String Dim 索引字串 As String = Base64_索引字串 If 金鑰 <> "" Then 索引字串 = 重建索引字串(金鑰) '---若使用金鑰則產生一組非標準的字元順序 Dim 位置, 餘數, skip As Integer 餘數 = (UBound(原始陣列) + 1) Mod 3 If 餘數 Then '---修正來源陣列長度為3的倍數,不足則補0 skip = 3 - 餘數 '---計算需補幾個0 ReDim Preserve 原始陣列(UBound(原始陣列) + skip) '---重新配置原始陣列大小(保留原資料) End If Dim out As New StringBuilder '---準備建立輸出字串 Dim Data8bit = 0, Data6bit = 0 位置 = UBound(原始陣列) For i = 0 To 位置 Step 3 '---從來源陣列一次轉3個 Bytes(8*3=24 To 6*4=24) Dim Value = 0 '---預存值為0 For j = 0 To 2 '---從來源陣列的 i 位置取3個 8bit(0-255) Data8bit = 原始陣列(i + j) '---取出第 i 組第 j 個 8bit 資料 Value = 256 * Value + Data8bit '---求值 Next For j = 0 To 3 '---轉為4個 6bit(0-63)--- Data6bit = Value \ (64 ^ (3 - j)) Mod 64 '---求值 out.Append(索引字串.Substring(Data6bit, 1)) '---對照索引字串轉換為 ASCII 字元 Next Next Return out.ToString.Substring(0, out.Length - skip) & StrDup(skip, "=") End Function '-------------------------------------------------------------- '函數功能:解碼字串至 Byte 陣列 '-------------------------------------------------------------- Function Ku64_To_Array(ByVal 字串 As String, Optional ByVal 金鑰 As String = "") As Array Dim kuKey As String = Base64_索引字串 If 金鑰 <> "" Then kuKey = 重建索引字串(金鑰) '---若使用金鑰則產生一組非標準的字元順序 Dim src = Replace(Replace(Replace(字串, vbCrLf, ""), vbTab, ""), " ", "") '---忽略斷行字元 Dim 餘數 = Len(src) Mod 4 If 餘數 Then '---修正字串長度為4的倍數(補 '=') src &= StrDup(4 - 餘數, "=") End If Dim 字元數 = Len(src) Try Dim 輸出陣列(字元數 / 4 * 3 - 1) As Byte Dim id As Integer = 0, skip As Integer = 0 Dim Data8bit = 0, Data6bit = 0 For i = 1 To 字元數 Step 4 '---處理來源陣列 Dim Value = 0 '---預存值為0 For j = 0 To 3 '---從來源陣列一次取4個 Data6bit(6*4=24)為 Token Dim Ch = Mid(src, i + j, 1) If Ch = "=" Then skip = skip + 1 Data6bit = 0 Else Data6bit = InStr(kuKey, Ch) - 1 End If Value = 64 * Value + Data6bit '---求 Token 值 Next For j = 0 To 2 '---轉為3個 Data8bit 值(8*3=24) Data8bit = Value \ (256 ^ (2 - j)) Mod 256 輸出陣列(id + j) = Data8bit Next id = id + 3 '---輸出陣列處埋位置指標加3 Next ReDim Preserve 輸出陣列(UBound(輸出陣列) - skip) Return 輸出陣列 Catch ex As Exception Return Nothing End Try End Function
- 寫一個經由金鑰(key)來產生非正規字元順序的索引字串。
Function 重建索引字串(ByVal key As String) As String Dim 基因 = key.GetHashCode '---以 key 的雜湊碼為亂數種子把字串弄亂--- Dim X = Rnd(-1) : Randomize(基因) '---用 Rnd(-1) 維持後續亂數的一致性--- Dim 前段字串 As String = Base64_索引字串, 後段字串 As String = "", tmp As Char For i = 1 To 32 '---隨機抽出32個字母排在最前面, 剩餘的排在後面--- tmp = Mid(前段字串, Int(Rnd() * Len(前段字串) + 1), 1) 後段字串 &= tmp : 前段字串 = Replace(前段字串, tmp, "") Next Return 後段字串 & 前段字串 End Function
- 需重新撰寫 Base64 的編解碼函式,數底轉換自己來做,程式碼如下:
- 有混淆功能,於相同編碼條件之下每次都輸出不同密文,且確定都能解密還原。
- 每次加密編碼時都重建亂數種子,據以產生不特定數量的亂數集合元素,合併到輸出密文內再於解密時引用。
- 程式碼已附在前面「用自己的演算法對 Binary() 陣列加密」、「用自己的演算法解密 Binary() 陣列」
- 有密文自我驗證能力,密文產生後若曾被篡改,解密程序會拒絕解密並送出提示訊息。
- 加密後陣列用 MD5 採值並附在輸出內容,解密時隔離掉 MD5 區塊後再採一次 MD5 值與先前比對,若不符合立即停止解碼傳回 nNothing。
'---計算 MD5--- Public Function MD5_Binary(ByVal src() As Byte) As Byte() Dim kumd5 As New MD5CryptoServiceProvider() Dim data As Byte() = kumd5.ComputeHash(src) Return data End Function Public Function MD5_String(ByVal src() As Byte) As String Dim kumd5 As New MD5CryptoServiceProvider() Dim sBuilder As New StringBuilder() Dim data As Byte() = kumd5.ComputeHash(src) For i = 0 To data.Length - 1 sBuilder.Append(UCase(data(i).ToString("x2"))) Next i Return sBuilder.ToString() End Function Public Function MD5(ByVal src As String) As String Dim kumd5 As New MD5CryptoServiceProvider() Dim sBuilder As New StringBuilder() Dim data As Byte() = kumd5.ComputeHash(Encoding.Default.GetBytes(src)) Dim i As Integer For i = 0 To data.Length - 1 sBuilder.Append(UCase(data(i).ToString("x2"))) Next i Return sBuilder.ToString() End Function
- 加密後陣列用 MD5 採值並附在輸出內容,解密時隔離掉 MD5 區塊後再採一次 MD5 值與先前比對,若不符合立即停止解碼傳回 nNothing。
- 要能防逆向工程(這部分的程式碼都在前面了)。
- 加密原則採位元組運算,將來源資料逐一和亂數陣列相對應之位元組做 XOR 運算,週而復始直到來源資料用盡。
- 亂數陣列採動態長度,每次編碼使用的亂數量都不一致,較難反向運算破解。
- 密文並不包含亂數集合實際內容,亂數全由基因自行在解碼程序中演化、展開。
- 解密加上延時程序,防暴力式破解。
比較一下
- 用簡單的原始字串(A - AAAAA)以標準 Base64 和自製的 ku64 各輸出兩次,比較一下輸出的字串就可以推估一下破解的難易度。
- Base64 :
標準 Base64 編碼輸出
--------------------------------------
A→Base64 編碼→QQA=
A→Base64 編碼→QQA=
AA→Base64 編碼→QQBBAA==
AA→Base64 編碼→QQBBAA==
AAA→Base64 編碼→QQBBAEEA
AAA→Base64 編碼→QQBBAEEA
AAAA→Base64 編碼→QQBBAEEAQQA=
AAAA→Base64 編碼→QQBBAEEAQQA=
AAAAA→Base64 編碼→QQBBAEEAQQBBAA==
AAAAA→Base64 編碼→QQBBAEEAQQBBAA==
- ku64:
- 自製 ku64 編碼輸出
--------------------------------------
A→ku64 編碼→3u20GO922j8CbLUMjbxqKffigA736cTqqac=
A→ku64 編碼→nPB7zE347T8BVvS0g6sw5YhNVrUpqH5nlco=
AA→ku64 編碼→0QQgV2gC0D9+PXKPojBohp4VaulhWr2UlyQ4zQ==
AA→ku64 編碼→33qwbm896D9rQEeFbA4rXkhWMML2/DeSa4sbdA==
AAA→ku64 編碼→Jfm9i5L87j8ekIYjA8KijLP0NAbh9SAWO7OSnCTX
AAA→ku64 編碼→+lFty/yo5j9/Mdx3++9fdigeoH3e/rMWA1YLaj5K
AAAA→ku64 編碼→d3EuQrs4xz9q9Nqm9GYrD5qIlc6HyCs4VUJ2gl8XwYM=
AAAA→ku64 編碼→7FMjMfapwT9q8fBAF0Hn9WUgKOT0b1cbOY5R7NbfRHE=
AAAAA→ku64 編碼→gIbyMUBD6T8QW7rJwWtuRVPoBsmXMFjpm0Gh8+I0T67+yA==
AAAAA→ku64 編碼→ORujlpyN4T99YP8SthVEVHaJl+Z1ACr9r5wcWplubuvs4g==
- 自製 ku64 編碼輸出
- Base64 :
- 再直接對 Byte() 陣列做加密看看:
- 用 ku64 加密Binary 陣列並觀察內在的變化:
- 結果5次加密後 Byte() 陣列內容都不同,但都可正確解密:
Private Sub Button4_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button4.Click
'---建立一個供測試用的 Byte() 陣列-
Dim b() As Byte = {&H11, &H22, &H33, &H44, &H55, &H66, &H77, &H88, &H99}
'---連續做5次加密、解密--
For n = 1 To 5
Dim a() As Byte = ku.陣列加密(b) : show陣列(a) : Debug.Write(" → ")
Dim c() As Byte = ku.陣列解密(a) : show陣列(c) : Debug.Write(vbNewLine)
Next
End Sub
Sub show陣列(ByVal b() As Byte)
For Each i In b
Debug.Write(i.ToString("x2") & " ")
Next
End Sub
- 05 a5 38 45 82 52 cc 3f 43 fa c5 3a 22 2f 3e 52 3e c7 1c 73 3d cb 26 41 b6 28 50 05 62 79 28 8d 51 → 11 22 33 44 55 66 77 88 99
- eb 98 2b 5f 75 cc d5 3f 4e c3 d0 da 35 df f5 1d a4 0d 47 0b 6d 01 62 cd 3b 2f f2 a9 15 74 d4 2e f4 → 11 22 33 44 55 66 77 88 99
- fc 57 22 f1 fd 2b e1 3f 53 44 f5 e1 6f 3c 78 17 96 f1 23 e7 f1 2b d1 d7 7d 6c 5f d8 ef 0d 20 8b 50 → 11 22 33 44 55 66 77 88 99
- e9 21 2c 49 f4 10 e6 3f 55 6b 04 49 8f 96 94 d2 ae ee 5e e9 43 ab b5 8b 35 ac 0b e8 6a 32 a0 e0 a2 → 11 22 33 44 55 66 77 88 99
- 7e 27 ac 3b bf 13 d6 3f 5c ab 41 21 f9 83 b6 05 a2 73 f5 03 9e 5c b2 d6 b3 4a 27 4b 2b 31 5a 84 b0 → 11 22 33 44 55 66 77 88 99
測試程式畫面:
結語
- 有時候也不要太過於妄自菲薄,一些高知名度的專業級加密演算法、驗證演算法、或者是排序方法其實也都是人寫出來的。
- 把寫程當做是一種 藝術 看待,用自己的見解和想法加上擁有的技術能力實作一下,讓生活多采多姿也是不錯的。
- 有興趣的朋友不妨幫我看看有哪些缺失或漏洞,如果還有閒情逸緻的話。
- 下回有空再談一下「產品序號機制」「產品網路啟用」「自動偵測新版本」「產品自動下載更新」等等技巧。