[VB.net][VB6][VBA]客製化 Base64 編碼/解碼 的技巧(三)建立自己的演算法 - 對資料加解密

[VB.net][VB6][VBA]客製化 Base64 編碼/解碼 的技巧(三)建立自己的演算法 - 對資料加解密

 

前言


前文說明了 Base64 的原理、索引字串的關鍵性角色、用固定種子亂數為基因演化出一整串的亂數集合,現在再試著實作一個包含前述手法的客製化資料加解密系統。

 

程式功能需求及基本做法


 

參考比較標準 Base64 的編碼及輸出格式後,決定只在文字輸出時採用 Base64 字串的 64 個字元,其他的都調整一下:

  1. 程式可對任何形式的資料進行加密、解密。例如執行檔、影音檔,或程式中以 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
       
  2. 以字串輸出時,所使用的字元和 Base64 同為 A-Za-z0-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
       
  3. 編碼時可選擇性使用金鑰上鎖,金鑰可為任何長度的字串。
    • 需重新撰寫 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
      
      
  4. 有混淆功能,於相同編碼條件之下每次都輸出不同密文,且確定都能解密還原。

    • 每次加密編碼時都重建亂數種子,據以產生不特定數量的亂數集合元素,合併到輸出密文內再於解密時引用。
    • 程式碼已附在前面「用自己的演算法對 Binary() 陣列加密」、「用自己的演算法解密 Binary() 陣列

       

  5. 有密文自我驗證能力,密文產生後若曾被篡改,解密程序會拒絕解密並送出提示訊息。
    • 加密後陣列用 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

       
  6. 要能防逆向工程(這部分的程式碼都在前面了)。
    • 加密原則採位元組運算,將來源資料逐一和亂數陣列相對應之位元組做 XOR 運算,週而復始直到來源資料用盡。
    • 亂數陣列採動態長度,每次編碼使用的亂數量都不一致,較難反向運算破解。
    • 密文並不包含亂數集合實際內容,亂數全由基因自行在解碼程序中演化、展開。
    • 解密加上延時程序,防暴力式破解。 

 

比較一下


 

  • 用簡單的原始字串(A - AAAAA)以標準 Base64 和自製的 ku64 各輸出兩次,比較一下輸出的字串就可以推估一下破解的難易度。
    1. 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==
    2. 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==

         
  • 再直接對 Byte() 陣列做加密看看:  
  1. 用 ku64 加密Binary 陣列並觀察內在的變化:
  2. 結果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   

測試程式畫面:

    • image image

 

結語


  1. 有時候也不要太過於妄自菲薄,一些高知名度的專業級加密演算法、驗證演算法、或者是排序方法其實也都是人寫出來的。
  2. 把寫程當做是一種 藝術 看待,用自己的見解和想法加上擁有的技術能力實作一下,讓生活多采多姿也是不錯的。
  3. 有興趣的朋友不妨幫我看看有哪些缺失或漏洞,如果還有閒情逸緻的話。
  4. 下回有空再談一下「產品序號機制」「產品網路啟用」「自動偵測新版本」「產品自動下載更新」等等技巧。

 

Demo 程式下載



ku3