18. 安全性

 

軟體合法性及軟體版權都是相當重要的課題,要證明某個應用軟體的原作者是誰,我們可以採行兩種方式。第一,使用所謂的"復活節彩蛋"也就是一個隱藏的對話方塊,在軟體版權有所爭議時,復活節彩蛋可以證明誰是原作者;第二,在應用程式中內藏加密後的訊息(Encrypted Messages),以這個訊息來證明應用程式的原作者是誰。

對於講求安全性的應用程式,密碼是應用程式不可或缺的一部份,在本章中我們會介紹如何在Visual Basic中建立一個輸入密碼時所需用到的對話方塊。

如何在應用程式中加入一個"復活節彩蛋"?
 

復活節彩蛋的用途在提供法律上的保障給應用程式的原作者,當兩個以上的作者爭議誰是軟體的原作者時,真正的作者可以在只有他自己知道的地方按下滑鼠鍵叫出復活節彩蛋,以證明他才是真正的原作者。許多應用程式中都隱藏著這種未在應用程式文件中記載的功能。復活節彩蛋應用程式是作者發揮創造力的地方,有些彩蛋設計得很有趣,如圖18-1即是一個彩蛋的範例。

設計一個復活節彩蛋,最簡單的方式就是建立一張具特殊用途的表單,表單上可以放置任何你要表達的訊息。復活節彩蛋設計的關鍵不在表單本身,而是讓表單顯示出來的方法。筆者在此提供一個通用的概念和一個例子,你可以發揮創造力去想一些好點子,讓表單經由不同的方法顯示出來。

應用程式可以檢查變換鍵 (Shift Ctrl and ALT) 及滑鼠鍵狀態,進而決定是否要呼叫復活彩蛋,例如,只有當Shift鍵被按住,並移動滑鼠至某個圖片方塊的右上角一公分範圍內,按下滑鼠右鍵,才能叫出復活節彩蛋。

偵測滑鼠在表單或控制項上被點選的位置是一件很容易的事。MouseDown和MouseUp事件提供了x和y參數,這組參數告訴你當滑鼠按鈕被按下時滑鼠游標所在的位置,同樣的,偵測變換鍵(Shift,Ctrl,和Alt)以及滑鼠按鍵的狀態也很容易,所有這些資訊都會以參數的形式傳入到你的MouseUp或MouseDown事件程序中。你也可以在MouseUp或MouseDown事件程序中使用靜態變數來偵測並且記錄滑鼠左鍵及右鍵被按下的順序,檢查這個順序是否為一特定的順序,或者偵測在固定時間內,如2秒內,滑鼠鍵是否被連續按下了特定的次數。


 

 圖18-1 復活節彩蛋

以類似的方法,你可以設定表單的KeyPreview屬性為True,並且用KeyDown事件記錄最近被按下的字鍵,對字鍵中被按下的順序做記錄監測。現在讓我們來看看如何在程式中偵測按鍵順序EGG。

筆者建立了一個簡單的物件類別模組Egg來檢查輸入之字元,Egg物件只有一個唯寫屬性Char,如果Char屬性的內容恰為EGG時Egg物件會顯示出一個訊息方塊。

Option Explicit

Private mstrKeyPhrase As String * 3

`~~~.Char
Property Let Char(intKey As Integer)
    Select Case intKey
        Case vbKeyE: mstrKeyPhrase = Mid$(mstrKeyPhrase, 2) & "E"
        Case vbKeyG: mstrKeyPhrase = Mid$(mstrKeyPhrase, 2) & "G"
        Case Else: mstrKeyPhrase = ""
    End Select
    If mstrKeyPhrase = "EGG" Then EasterEgg
End Property

Private Sub EasterEgg()
    MsgBox "JC and JW were here!!!"
End Sub

請在主表單中加入以下的程式碼以便觀察Egg物件執行的結果。

Option Explicit

Dim eggTest As New Egg

Sub Form_Load()
    Me.KeyPreview = True
End Sub

Sub Form_KeyDown(intKeyCode As Integer, intShift As Integer)
    eggTest.Char = intKeyCode
End Sub

Form_Load事件程序設定表單的KeyPreview屬性為True,因此不管表單上的哪一個控制項取得了駐點(Focus)所有被按下的字鍵都會被檢查。當EGG三個字鍵被按下後,私有程序EasterEgg會顯示出一個簡單的訊息方塊,每當表單或表單上的控制項取得駐點而且字鍵被按下時,Form_KeyDown事件程序就會被呼叫,此時物件執行實體eggTest會檢查Char屬性中所累積最近被按下的三個字鍵,如果累積的字元按順序恰好為EGG,那麼Property Let Char屬性程序就會呼叫私有程序EasterEgg。


參考資料:

請參閱 三十四章"進階應用程式" 的Dialogs應用程式,這個應用程式介紹了一個隱藏的訊息畫面範例,這個範例以辨別滑鼠按鍵的方式來呼叫復活節彩蛋。


如何建立一個密碼輸入對話方塊?
 

用早期版本的Visual Basic建立密碼輸入對話方塊是一件困難重重的苦工,到了最近幾版的Visual Basic以後,事情就變得容易多了。

我們要建立的對話方塊,必須能在使用者輸入密碼時顯示星號或其他符號,顯示星號或其他符號的作用在於讓使用者知道他輸入了幾個字,又不會讓旁人看見使用者輸入了什麼。另一方面,程式必須能記錄使用者實際上輸入的所有字元,以便加以辨證。

圖18-2顯示的是一個典型的密碼輸入對話方塊。


 

 圖18-2 一個典型的密碼輸入對話方塊

密碼輸入對話方塊通常需要一個文字方塊控制項。文字方塊控制項有一個屬性PasswordChar,你可以設定這個屬性以隱藏輸入的密碼。儘管輸入密碼時密碼不會被顯示,但Text屬性中仍然能記錄被輸入的密碼。

以下這個程式在說明這基本技巧。表單上有一個文字方塊控制項txtPassword以及一個指令按鈕控制項cmdOK,我們用cmdOK_Click事件程序檢查輸入的密碼是否為"sesame",如果密碼符合則顯示一個表示輸入正確的訊息給使用者。

Option Explicit

Private Sub cmdOK_Click()
    If txtPassword.Text <> "sesame" Then
        MsgBox "Incorrect password", vbCritical
    Else
        MsgBox "Okay!Dear John, How Do I... Correct password"
    End If
End Sub

Private Sub Form_Load()
    txtPassword.PasswordChar = "*"
    txtPassword.Text=""
End Sub

參考資料:

 第三十四章"進階應用程式" 對於這個技巧有更完整深入的探討


如何將密碼或文字予以加密編碼?
 

加密編碼(Cipher)的技術可以很簡單也可以極度的複雜嚴謹,在大部份情況下,你只需要防止使用者在執行檔或資料檔中找到機密的資訊,不需要做到符合國安局的最高保密程度。以下我們所提供的ASCII-to-ASCII加密技巧可以防止大部份的使用者窺視到機密的資料。當然,面對那些一心要破解機密的"駭客"(Hacker)而言,這仍不算是百分之百安全的保密措施。

Cipher物件類別
 

Cipher物件類別模組所定義的物件可以對一個字串做加密與解密的動作,以下就是對這些物件屬性和方法的說明:

  •  KeyString屬性: 這個唯寫屬性的用途為設定加密或解密時所需的鍵值,當這個屬性被設定之後,Visual Basic會根據屬性值中的每個字元和字元的排列順序,設定一個獨一無二的起始種子值給內部的亂數產生器。
     

    注意:

    如果要使Visual Basic的亂數產生器重複產生相同的亂數序列,你必須用一個負的引數呼叫Rnd及函式,然後再呼叫Randomize陳述式,例如,Randomize (Rnd (-1.23))將亂數產生器初始化之後,每次呼叫Rnd都會產生相同的亂數序列。


  •  Text屬性︰ 這個屬性存放將被加密或解密的文字。例如,你可以指定一個字串給Text屬性,呼叫DoXor方法和Stretch方法對字串進行加密及延展處理,然後把結果存入一個檔案中(從檔案中你讀不到原來的字串),稍後,你再設定KeyString的值為原來加密時給的值,呼叫Shrink方法和DoXor方法對字串做回縮和解密處理,最後將還原後的字串列印出來。
     
  •  DoXor方法︰ 亂數產生器利用KeyString產生了一個固定的亂數序列,DoXor方法將Text屬性值中的每一個位元組與亂數序列中的亂數循序地做Xor運算,然後將處理過的字串放回Text屬性中。DoXor是一個可逆運算,也就是說,把DorXor處理過的字串用DorXor再處理一次,便可以回復原來字串的本貌。DoXor的技巧在許多加密解密的運算法中,通常扮演著核心的角色。
     
  •  Stretch方法︰ 這個方法主要在將任何字串(不管是否可以列印)轉換成可列印、可顯示的字串,換句話說,如果某個字串中含有不可印列或顯示的二進位位元組,Strech方法會把它轉換為可以列印顯示但長度較長的字串。例如,當你在列印或顯示一個含有10個Tab字元的字串時,可能什麼都看不到,這時候如果用Stretch方法便可將Tab轉成可列印的字元。為什麼需要Stretch方法呢?因為DoXor方法很可能會產生這些Tab、Return或其他奇奇怪怪的字元。Strech方法的運作方式是以每三個字元為一組,從每一組中取得每一個字元的每個位元,用取得的位元去形成第四個字元,而且這四個字元都被映對處理為可列印的字元。透過Stretch處理過的字串可以被存放在系統登錄或INI檔中,可以被列印在報表上,也可以包含在電子郵件中透過Internet傳送。
     
  •  Shrink方法︰ 這個方法是Stretch方法的逆向處理,它把包含可列印字元的字串轉換成可能含有任何256字元位元組的字串。
     

    注意:

    筆者"幾乎"是用Unicode中的演算法來設計Strech和Shrink方法;之所謂"幾乎"是因為筆者用了Unicode中的演算法但不同的offset值來對映延伸的字串,以避免對映到空白字元。


讓Cipher物件運作
 

下列這段程式告訴你如何使用前面介紹過的屬性和方法。要看到Clipher物件實際上執行的結果,請建立一張含有兩個文字方塊控制項、兩個標籤控制項、兩個指令按鈕控制項的表單,然後在表單中加入這段程式︰

Option Explicit

Private Sub cmdEncrypt_Click()
    Dim cipherTest As New Cipher
    cipherTest.KeyString = txtKey.Text
    cipherTest.Text = txtClear.Text
    cipherTest.DoXor
    cipherTest.Stretch
    txtEncrypted.Text = cipherTest.Text
End Sub

Private Sub cmdDecrypt_Click()
    Dim cipherTest As New Cipher
    cipherTest.KeyString = txtKey.Text
    cipherTest.Text = txtEncrypted.Text
    cipherTest.Shrink
    cipherTest.DoXor
    txtDecrypted.Text = cipherTest.Text
End Sub

這段程式假設txtClearText存放原始而未加密的文字,txtKeyString存放鍵值,lblEncryptedText存放原始文字加密後的結果。按下cmdEncrypt指令按鈕,程式將會進行加密的動作,而按下cmdDecrypt則進行解密處理。請仔細看一下加密後的文字,它的長度大約是原始文字長度的一又三分之一倍,這是Strech方法執行後的結果。

圖18-3顯示的是Cipher物件執行的情形。


 

 圖18-3 以Cipher物件做字串的加密與解密處理

以下的程式即是Cipher物件類別的定義︰

'CIPHER.CLS
`CIPHER.CLS
Option Explicit

Private mstrKey As String
Private mstrText As String

`~~~.KeyString
`A string (key) used in eintCryption and decryption
Public Property Let KeyString(strKey As String)
    mstrKey = strKey
    Initialize
End Property

`~~~.Text
`Write text to be eintCrypted or decrypted
Public Property Let Text(strText As String)
    mstrText = strText
End Property

`Read text that was eintCrypted or decrypted
Public Property Get Text() As String
    Text = mstrText
End Property
`~~~.DoXor
`Exclusive-or method to encrypt or decrypt
Public Sub DoXor()
    Dim lngC As Long
    Dim intB As Long
    Dim lngN As Long
    For lngN = 1 To Len(mstrText)
        lngC = Asc(Mid(mstrText, lngN, 1))
        intB = Int(Rnd * 256)
        Mid(mstrText, lngN, 1) = Chr(lngC Xor intB)
    Next lngN
End Sub

`~~~.Stretch
`Convert any string to a printable, displayable string
Public Sub Stretch()
    Dim lngC As Long
    Dim lngN As Long
    Dim lngJ As Long
    Dim lngK As Long
    Dim lngA As Long
    Dim strB As String
    lngA = Len(mstrText)
    strB = Space(lngA + (lngA + 2) \ 3)
    For lngN = 1 To lngA
        lngC = Asc(Mid(mstrText, lngN, 1))
        lngJ = lngJ + 1
        Mid(strB, lngJ, 1) = Chr((lngC And 63) + 59)
        Select Case lngN Mod 3
        Case 1
            lngK = lngK Or ((lngC \ 64) * 16)
        Case 2
            lngK = lngK Or ((lngC \ 64) * 4)
        Case 0
            lngK = lngK Or (lngC \ 64)
            lngJ = lngJ + 1
            Mid(strB, lngJ, 1) = Chr(lngK + 59)
            lngK = 0
        End Select
    Next lngN
    If lngA Mod 3 Then
        lngJ = lngJ + 1
        Mid(strB, lngJ, 1) = Chr(lngK + 59)
    End If
    mstrText = strB
End Sub
`~~~.Shrink
`Inverse of the Stretch method;
`result can contain any of the 256-byte values
Public Sub Shrink()
    Dim lngC As Long
    Dim lngD As Long
    Dim lngE As Long
    Dim lngA As Long
    Dim lngB As Long
    Dim lngN As Long
    Dim lngJ As Long
    Dim lngK As Long
    Dim strB As String
    lngA = Len(mstrText)
    lngB = lngA - 1 - (lngA - 1) \ 4
    strB = Space(lngB)
    For lngN = 1 To lngB
        lngJ = lngJ + 1
        lngC = Asc(Mid(mstrText, lngJ, 1)) - 59
        Select Case lngN Mod 3
        Case 1
            lngK = lngK + 4
            If lngK > lngA Then lngK = lngA
            lngE = Asc(Mid(mstrText, lngK, 1)) - 59
            lngD = ((lngE \ 16) And 3) * 64
        Case 2
            lngD = ((lngE \ 4) And 3) * 64
        Case 0
            lngD = (lngE And 3) * 64
            lngJ = lngJ + 1
        End Select
        Mid(strB, lngN, 1) = Chr(lngC Or lngD)
    Next lngN
    mstrText = strB
End Sub

`Initializes random numbers using the key string
Private Sub Initialize()
    Dim lngN As Long
    Randomize Rnd(-1)
    For lngN = 1 To Len(mstrKey)
        Randomize Rnd(-Rnd * Asc(Mid(mstrKey, lngN, 1)))
    Next lngN
End Sub

當你使用Cipher物件時,請牢記要在呼叫DoXor方法之前先設定KeyString屬性,設定了KeyString才能使亂數序列回到重複的起始點。


注意:

經過我們的Cipher物件加密後的字串,既不含任何跳離字元(Escape Code)或控制碼,也不會擾亂數據機的通訊作業,因此,你可以透過數據機直接傳送加密後的字串,不須使用任何複雜的二進位碼傳輸模式。如果直接處理二進位資料不會對你造成問題,你不一定非得使用Shrink和Stretch方法;另外也請注意,Shrink和Stretch方法有時候也不一定要和DoXor方法一同使用,例如,當你在網際網路上應用電子郵件傳送二進位檔(如圖形)時,可以使用Shrink和Stretch而不必將資料加密。


確保系統登錄資料的安全性
 

最後,我們來討論如何以資料加密技術來達成保護軟體的目的。

我們可以把軟體的安裝日期、執行的次數、使用者姓名以及其他有關於這軟體的資料,予以加密後存放在系統登錄中。使用者的姓名和公司名稱可以做為鍵值,以便對資料進行加密和解密處理。有了這些資料,那麼,當展示軟體到期、執行次數超過上限或者使用者的名字及公司名稱被更改時,軟體本身可採取適當的措施,防止軟體被非法地繼續使用。

以下這個範例是利用Cipher物件類別而設計的,它讓使用者輸入一個鍵值和使用者姓名;當cmdEncryptUser指令按鈕被按下之後,加密後的使用者姓名便會被存放到系統登錄裡;而當cmdDecryptUser指令按鈕被按下時,解密後的使用者姓名會顯示出來。

執行過這個範例程式之後,你可以叫出登錄編輯程式來找尋機碼User Name,看看加密後的使用者姓名是否已經在登錄裡面。

Option Explicit

Private mstrAppName As String
Private mstrSection As String
Private mstrKey As String
Private mstrSetting As String

Private Sub cmdEncrypt_Click()
    Dim cipherTest As New Cipher
    cipherTest.KeyString = txtKey.Text
    cipherTest.Text = txtUser.Text
    cipherTest.DoXor
    cipherTest.Stretch
    mstrSetting = cipherTest.Text
    mstrAppName = App.Title
    mstrSection = "Testing"
    mstrKey = "User Name"
    SaveSetting mstrAppName, mstrSection, mstrKey, mstrSetting
    txtEncrypted.Text = cipherTest.Text
End Sub

Private Sub cmdDecrypt_Click()
    Dim cipherTest As New Cipher
    mstrAppName = App.Title
    mstrSection = "Testing"
    mstrKey = "User Name"
    cipherTest.Text = GetSetting(mstrAppName, mstrSection, mstrKey)
    cipherTest.KeyString = txtKey.Text
    cipherTest.Shrink
    cipherTest.DoXor
    txtDecrypted.Text = cipherTest.Text
End Sub

圖18-4顯示的是程式執行的情形。


 

 圖18-4 把加密後的使用者姓名存放在系統登錄中

參考資料:

請參閱 第三十四章"進階應用程式" 的Secret應用程式,這個範例對於加密演算法有更深入的介紹。

 第十六章"系統登錄" 完整地介紹了如何取得登錄中的資料。


如何處理Internet的安全問題?
 

在本章中我們介紹了一些維護應用程式安全的方法,這些方法提供了簡單的加密技術和使用者密碼驗證的技術。如果你的Internet應用程式需要一些基本的安全維護機制,那麼可以採用本章所介紹的方法,但如果需要更嚴密的安全維護技術,就要找專家來幫你了!

Microsoft一直關心著安全的問題,他們動用了許多的專家,使這些安全方面的技術可以很容易施行在你的應用程式裡。這個課題雖然不在本書討論的範圍內,但筆者建議你到 http://www.microsoft.com/security 這裡看一看,這個網站提供了一個很好的學習起點,足以讓你開始學習更多的安全技術。