前言

一般來說在.net中實現加密時我們會使用 RSACryptoServiceProvider 這個類別來實現產生公私鑰進行加解密的行為,而且一個簡單的 [ToXMLString()]](https://docs.microsoft.com/zh-tw/dotnet/api/system.security.cryptography.rsa.toxmlstring?view=netframework-4.7.2) 方法就能把公鑰(或甚至是私鑰,但實務上很不建議)給吐出來。不過除了使用這個類別來產生鑰匙外我們其實還可以利用強式名稱工具來幫我們產生鑰匙,好處是不但可以作為加解密的鑰匙來源,沒有金鑰容器名稱以及存取層級(User Layer, Machine Layer)的管理 issue 簡單用,還可以順便用它來簽署組件,一檔多用資源徹底利用。以下就是利用強式名稱工具來實作的方式。

內文

強式名稱工具(Strong Name Tool) 是安裝 Visual Studio 之後就會附上的開發用工具我們可以開啟 “Visual Studio x64 Win64命令提示字元”,鍵入 “sn.exe /?” 來觀看可以使用的參數,這裡我們會用到的就是最簡單的 -k 參數而且就是使用 default 值。我們就利用這個工具產生一組全新的 snk 檔案放在 D 槽吧~

sn_generate_key

我們已經可以在D槽的跟目錄下面看到產生好的 Strong Name Key File 了~

要注意的是使用預設的話 key size 長度就是 1024 bit 這長度與能拿來加密的文字長度會有直接關係使用預設長度的話一次能夠加密的文字長度為 128bits,不過要小心在 .net 中會有 padding 的 issue 造成實際上可以加密的文字長度會更短(註一),這個問題的解決方式我們會在稍後的範例中看到。

準備好之後我們就來開啟一個專案試試看這 snk 檔要怎麼幫我們加密資料吧~

我們這邊就使用最單純的 Console 專案來進行測試即可~

ps.這邊記得把 D 槽根目錄中的 snk 檔案放在專案的 bin 資料夾底下。

ps2.為了保持目的單純不小心類別就被搞大了…

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
using System;
using System.Collections;
using System.IO;
using System.Security.Cryptography;

class Program
{
   private static string _strongNameKeyFile = "Test.snk";
   public static string GetPublicKey
   {
      get;
      private set;
   }
   private static string GetPrivateKey
   {
      get;
      set;
   }

   public static string Data(string plaintext)
   {
      using (RSACryptoServiceProvider rsa = new RSACryptoServiceProvider())
      {
         rsa.ImportCspBlob(File.ReadAllBytes(_strongNameKeyFile));
         GetPublicKey = rsa.ToXmlString(false);
         GetPrivateKey = rsa.ToXMLString(true);

         // base-64 encoded.
         return Convert.ToBase64String(EncryptData(plaintext));
      }
   }

   // 若資料量很大的話(資料量小以預設來說就是資料量小於117 bits,那可以直接呼叫Encrypt()來進行加密及可)
   // 就需要分段加密不然一定會炸掉,吐一個Bad Length的例外訊息。
   private static byte[] EncryptData(string rawdata)
   {
      // 憑證的金鑰預設就是1024 bit
      // buffer扣去11 bits是為了padding使用,請參考註一的連結。
      const int encryptionBufferSize = (1024 / 8) - 11;
      const int DecryptionBufferSize = 1024 / 8;

      RSACryptoServiceProvider rsa = new RSACryptoServiceProvider();
      // 這邊利用公鑰來加密,因此解密的一方需要取得密鑰才能解密,這樣可以確保有私鑰的人才有辦法看到資訊。
      rsa.FromXmlString(GetPublicKey);

      // 進行加密時都需要把字串轉成byte陣列來操作,這邊需要注意轉換的問題!
      byte[] dataEncoded = System.Text.Encoding.UTF8.GetBytes(rawdata);
      using (MemoryStream ms = new MemoryStream())
      {
         byte[] buffer;
         int pos = 0;
         int copyLength = encryptionBufferSize;
         while (true)
         {
            if (pos + copyLength > dataEncoded.Length)
               copyLength = dataEncoded.Length - pos;
            buffer = new byte[copyLength];
            Array.Copy(dataEncoded, pos, buffer, 0, copyLength);
            pos += copyLength;

            // 這邊不使用OEAP padding,使用 PKCS#1 v1.5 padding(填補法的差異請參考註一)
            // 另外也要注意雖然加密的資料需要扣除 padding 量,但是寫入的長度依然不變。
            ms.Write(rsa.Encrypt(buffer, false), 0, DecryptionBufferSize);
            Array.Clear(buffer, 0, copyLength);
            if (pos >= dataEncoded.Length)
               break;
         }
         return ms.ToArray();
      }
   }

   public static string GetPlainDataBack(string encryptdata)
   {
      string result = string.Empty;
      using (RSACryptoServiceProvider rsa = new RSACryptoServiceProvider())
      {
         const int decryptionBufferSize = 1024 / 8;

         // 使用公鑰加密就必須用私鑰解密。
         rsa.FromXmlString(GetPrivateKey);
         byte[] encryptdataarray = Convert.FromBase64String(encryptdata);
         using (MemoryStream ms = new MemoryStream(encryptdataarray.Length))
         {
            byte[] buffer = new byte[decryptionBufferSize];
            int pos = 0;
            int copyLength = buffer.Length;
            while (true)
            {
               Array.Copy(encryptdataarray, pos, buffer, 0, copyLength);
               pos += copyLength;
               byte[] resp = rsa.Decrypt(buffer, false);
               ms.Write(resp, 0, resp.Length);
               Array.Clear(resp, 0, resp.Length);
               Array.Clear(buffer, 0, buffer.Length);

               if (pos >= encryptdataarray.Length)
                  break;
            }
            result = System.Text.Encoding.UTF8.GetString((ms.ToArray()));
         }
      }
      return result;
   }

   static void Main(string[] args)
   {
      var plaintext = "This is a plain text!";
      var result = Data(plaintext);
      Console.WriteLine("Orignal Text:{0}", plaintext);
      Console.WriteLine("After Encryption:{0}", result);

      result = GetPlainDataBack(result);
      Console.WriteLine("After Decryption:{0}",result);
      Console.Read();
   }
}

結語

以上就是使用強式名稱工具來達成資訊加密、解密的需求,我們可以發現若對方要可以解讀這個密文那 snk 檔是對方勢必得取得的檔案,因此該檔案的存取是一定要控管的,這是不方便之處。另外其實實務上除了加解密之外還會順手做數位簽章….這就留到下次吧…

參考資料