Windows phone应用开发[19]-RSA数据加密

在这个系列的第十六章节中Windows phone应用开发[16]-数据加密 中曾详细讲解过windows phone 常用的MD5,HMAC_MD5,DES,TripleDES[3DES] 数据加密的解决方案.本篇作为windows phone 数据加密一个弥补篇幅.将专门来讲解windows phone rsa数据加密存在问题解决方案以及和其他平台[Java]互通存在的问题.

RSA算法起源与现状

如果你关注过近现代密码学的发展.你一定不会否认RSA的出现的重要意义.

Windows phone应用开发[19]-RSA数据加密

[上图:德国的洛伦兹密码机,所使用的二次世界大战加密机密邮件]

RSA 作为计算机安全通信的基石.保证数据在传输过程不被第三方破解.在RSA[非对称加密算法]出现之前 也就是1976年之前.在通常使用数据通信传输过程大多会使用[对称算法].简单用如下一个使用场景来说明这个原理:

Windows phone应用开发[19]-RSA数据加密

对称算法加密最大特点是:如果我们要从A点要向B点传输加密数据. 首先我们在A点采用加密算法进行加密.加密数据传输给B点后. B点必须采用同样规则才能解密.而A作为传输方必须告诉接收方加密规则.否则B点则无法解密. 而这个过程难以避免要传输和保存密文数据的密钥. 而一旦涉及到密钥传递就存在被第三方破解和拦截的风险.

至此之后.在1977年由罗纳德·李维斯特(Ron Rivest)、阿迪·萨莫尔(Adi Shamir)和伦纳德·阿德曼(Leonard Adleman)三人共同提出新的加密算法也就是现在的[非对称加密算法]. 这个密钥传输问题才得以避免. 而RSA加密算也正是采用三人首字母命名而成的. 而其实RSA最早是在1976年,已经由Diffie和Hellman 在“New Directions in Cryptography [密码学新方向]”一文中就已经提出. 二人在文章中提出在不传递密钥的就可以实现数据的解密新构思. 而这也正是“非对称加密算法”最早的雏形. 正是因为二人新的构思才促使RSA基于该理论上出现.

RSA原理图如下:

Windows phone应用开发[19]-RSA数据加密

在B点可以生成两种密钥分别公钥和私钥. 公钥是公开.任何人都可以获得.但私钥却是保密的. 这样一来数据传输流程就发生了变化。A点只需要获取B点颁发的公钥.然后对传输的数据进行加密. B点获取加密数据后在采用私钥进行解密.这样一来采用公钥加密数据只有采用私钥才能解密成功.那么只需确保私钥的安全不被泄露.同时即使知道了算法和若干密文不足以确定密钥的情况.整个通信过程都是安全的.

RSA的明文、密文是0到n-1之间的整数,通常n的大小为1024位或309位十进制数.也就是说密钥越长.它整个加密过程就越难以被破解.而目前被破解的最长RSA密钥是768个二进制位. 而基于1024位的RSA密钥也是相对安全的[至少目前公开的数据显示 没有被破解的先例].RSA 密钥是通过如下过程产生的:

A:随机选择两个大素数 p, q

B:计算 N=p.q [注意 ø(N)=(p-1)(q-1)]

C:选择 e使得1<e<ø(N),且gcd(e,ø(N))=1

D:解下列方程求出 d  [e.d=1 mod ø(N) 且 0≤d≤N]

E:公布公钥: KU={e,N}

F:保存私钥: KR={d,p,q}

其实只需要知道一些数论知识基本可以理解. 当我们知道RSA 原理后. 理论上可以通过如下三种方式来破解攻击RSA算法体系:

三种攻击 RSA的方法:

A:强力穷举密钥

B:数学攻击:实质上是对两个素数乘积的分解

C:时间攻击:依赖解密算法的运行时间

而相对可行是采用数学方式,主要是基于因数分解的问题:

三种数学攻击方法

A:分解 N=p.q, 因此可计算出 ø(N),从而确定d

B:直接确定ø(N),然后找到d

C:直接确定d

其实说到本质. 正式因为对极大整数做因数分解的难度决定了RSA算法的可靠性。换言之,对一极大整数做因数分解愈困难,RSA算法愈可靠。尽管如此,只有一些RSA算法的变种[来源请求]被证明为其安全性依赖于因数分解。假如有人找到一种快速因数分解的算法的话,那么用RSA加密的信息的可靠性就肯定会极度下降。但找到这样的算法的可能性是非常小的。今天只有短的RSA钥匙才可能被强力方式解破。到2008年为止,世界上还没有任何可靠的攻击RSA算法的方式。只要其钥匙的长度足够长,用RSA加密的信息实际上是不能被解破的。但在分布式计算量子计算机理论日趋成熟的今天,RSA加密安全性受到了一定挑战.

wp rsa数据加密

再回到本篇正题.在.NET 平台其实早在FrameWork 2.0 时就已经提供了RSACryptoServiceProvider 来实现基于RSA算法加密.并一直延续到windows phone 版本中.首先来看看基于windows phone 平台做一个最简单加密“Hello World”. 构建一个空白的Project .实现起来非常简单:

   1:   RSACryptoServiceProvider rsa = new System.Security.Cryptography.RSACryptoServiceProvider();
   2:   byte[] contentBytes = System.Text.Encoding.UTF8.GetBytes(“Hello World”);
   3:   return Convert.ToBase64String(rsa.Encrypt(contentBytes, false));

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }

首先声明一个RSACryptoServiceProvider 对象,然后对需要加密的数据采用UTF8格式编码成字节数组.在采用RSACryptoServiceProvider对象的Encrypt()方法执行加密.加密数据因为返回时字节数组转换成Base64字符串可见.

做到这肯定有人会问RSA加密结果成不是采用公钥和私钥吗? 那RSACryptoServiceProvider对象公钥和私钥在那?我们也可以采用如下代码来查看:

   1:   RSACryptoServiceProvider rsa= new System.Security.Cryptography.RSACryptoServiceProvider();
   2:   rsa.ExportParameters(true);

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }

ExportParameters()方法会返回一个RSAParameters结构的对象. 方法的参数True和False用来标识导出的RSAParameters对象是否包含私有参数. 其实当我们采用默认的构造函数实例化一个RSACryptoServiceProvider对象时..net会帮我们默认生成一对公钥和私钥.可以通过调用其ExportParameters()方法或ToXmlString()[WP中没有改方法]方法导出密钥.在看来看RSAParameters结构包含的参数属性:

Windows phone应用开发[19]-RSA数据加密

其实如上可以发现这些参数正式生成RSA公钥和密钥需要的参数. P和Q代表两个大素数.D代表私钥指数.Exponent代表公钥指数.把导出的默认的RSAParameters对象可以看到公钥指数指数是65537. 这是微软选择默认构造方法时生成的公钥都是一样的.65537转换成字节数组就是Exponent的值. 而AQAB正是65337 Base64编码而来. 但当我们把这个字节数组转换成Base64String看一下结果:

   1:  Console.WriteLine(Convert.ToBase64String(Encoding.Default.GetBytes("65537")));

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }

输出却是”NjU1Mzc=” 而并不是我们预想的AQAB. 其实65337 是一个大素数. 我们不能把其当做一个普通的字符串来直接处理. 首先我们需要把它转换成一个二进制字节数组,每8位一截取. 依次取6位每个前面加上“00”. 转换十进制就是我们对应数据. 当然更多细节可以参考如下这篇文章.

其实整个加密过程就像一个临时的回话Session.如果我们加密数据对外使用是就需要进行传递. 我们就需要对这个临时回话的RSA公钥和私钥进行保存.但更多的应用场景里我们会发现实际项目中一般情况采用公钥证书也就是[.Cer]文件来保存和传递公钥. 然后应用程序中导入公钥文件中的数据.来进行加密数据. 而把一个文件的字节流数组转换我们需要公钥对象RSAParameter对象,这时我们需要用到X509PublicKeyParser这个类. 但遗憾的是目前Windows Phone 并没有提供这个类的实现.但在C-Sharper上已经有人完整移植了该版本.完整的类如下:

   1:  using System;
   2:  using System.Collections.Generic;
   3:  using System.Linq;
   4:  using System.Security.Cryptography;
   5:  using System.Security.Cryptography.X509Certificates;
   6:  using System.Text;
   7:   
   8:  namespace System.Security.Cryptography
   9:  {   
  10:      internal abstract class AbstractAsn1Container
  11:      {
  12:          #region Property
  13:          private int offset;
  14:          private byte[] data;
  15:          private byte tag;
  16:          #endregion
  17:   
  18:          #region Action
  19:          internal protected AbstractAsn1Container(byte[] abyte, int i, byte tag)
  20:          {
  21:              this.tag = tag;
  22:              if (abyte[i] != tag)
  23:              {
  24:                  throw new Exception("Invalid data. The tag byte is not valid");
  25:              }
  26:              int length = DetermineLength(abyte, i + 1);
  27:              int bytesInLengthField = DetermineLengthLen(abyte, i + 1);
  28:              int start = i + bytesInLengthField + 1;
  29:              this.offset = start + length;
  30:              data = new byte[length];
  31:              Array.Copy(abyte, start, data, 0, length);
  32:          }
  33:   
  34:          internal int Offset
  35:          {
  36:              get
  37:              {
  38:                  return offset;
  39:              }
  40:          }
  41:   
  42:          internal byte[] Bytes
  43:          {
  44:              get
  45:              {
  46:                  return this.data;
  47:              }
  48:          }
  49:   
  50:          internal protected virtual int DetermineLengthLen(byte[] abyte0, int i)
  51:          {
  52:              int j = abyte0[i] & 0xff;
  53:              switch (j)
  54:              {
  55:                  case 129:
  56:                      return 2;
  57:   
  58:   
  59:                  case 130:
  60:                      return 3;
  61:   
  62:   
  63:                  case 131:
  64:                      return 4;
  65:   
  66:   
  67:                  case 132:
  68:                      return 5;
  69:   
  70:   
  71:                  case 128:
  72:                  default:
  73:                      return 1;
  74:              }
  75:          }
  76:   
  77:          internal protected virtual int DetermineLength(byte[] abyte0, int i)
  78:          {
  79:              int j = abyte0[i] & 0xff;
  80:              switch (j)
  81:              {
  82:                  case 128:
  83:                      return DetermineIndefiniteLength(abyte0, i);
  84:   
  85:   
  86:                  case 129:
  87:                      return abyte0[i + 1] & 0xff;
  88:   
  89:   
  90:                  case 130:
  91:                      int k = (abyte0[i + 1] & 0xff) << 8;
  92:                      k |= abyte0[i + 2] & 0xff;
  93:                      return k;
  94:   
  95:   
  96:                  case 131:
  97:                      int l = (abyte0[i + 1] & 0xff) << 16;
  98:                      l |= (abyte0[i + 2] & 0xff) << 8;
  99:                      l |= abyte0[i + 3] & 0xff;
 100:                      return l;
 101:              }
 102:              return j;
 103:          }
 104:   
 105:          internal protected virtual int DetermineIndefiniteLength(byte[] abyte0, int i)
 106:          {
 107:              if ((abyte0[i - 1] & 0xff & 0x20) == 0)
 108:                  throw new Exception("Invalid indefinite length.");
 109:              int j = 0;
 110:              int k;
 111:              int l;
 112:              for (i++; abyte0[i] != 0 && abyte0[i + 1] != 0; i += 1 + k + l)
 113:              {
 114:                  j++;
 115:                  k = DetermineLengthLen(abyte0, i + 1);
 116:                  j += k;
 117:                  l = DetermineLength(abyte0, i + 1);
 118:                  j += l;
 119:              }
 120:   
 121:   
 122:              return j;
 123:          }
 124:          #endregion
 125:      }
 126:   
 127:      #region Internal Object 
 128:      internal class IntegerContainer : AbstractAsn1Container
 129:      {
 130:          internal IntegerContainer(byte[] abyte, int i)
 131:              : base(abyte, i, 0x2)
 132:          {
 133:          }
 134:      }
 135:   
 136:      internal class SequenceContainer : AbstractAsn1Container
 137:      {
 138:          internal SequenceContainer(byte[] abyte, int i)
 139:              : base(abyte, i, 0x30)
 140:          {
 141:          }
 142:      }
 143:   
 144:      public class X509PublicKeyParser
 145:      {
 146:          public static RSAParameters GetRSAPublicKeyParameters(byte[] bytes)
 147:          {
 148:              return GetRSAPublicKeyParameters(bytes, 0);
 149:          }
 150:   
 151:   
 152:          public static RSAParameters GetRSAPublicKeyParameters(byte[] bytes, int i)
 153:          {
 154:              SequenceContainer seq = new SequenceContainer(bytes, i);
 155:              IntegerContainer modContainer = new IntegerContainer(seq.Bytes, 0);
 156:              IntegerContainer expContainer = new IntegerContainer(seq.Bytes, modContainer.Offset);
 157:              return LoadKeyData(modContainer.Bytes, 0, modContainer.Bytes.Length, expContainer.Bytes, 0, expContainer.Bytes.Length);
 158:          }
 159:   
 160:   
 161:          public static RSAParameters GetRSAPublicKeyParameters(X509Certificate cert)
 162:          {
 163:              return GetRSAPublicKeyParameters(cert.GetPublicKey(), 0);
 164:          }
 165:   
 166:   
 167:          private static RSAParameters LoadKeyData(byte[] abyte0, int i, int j, byte[] abyte1, int k, int l)
 168:          {
 169:              byte[] modulus = null;
 170:              byte[] publicExponent = null;
 171:              for (; abyte0[i] == 0; i++)
 172:                  j--;
 173:   
 174:   
 175:              modulus = new byte[j];
 176:              Array.Copy(abyte0, i, modulus, 0, j);
 177:              int i1 = modulus.Length * 8;
 178:              int j1 = modulus[0] & 0xff;
 179:              for (int k1 = j1 & 0x80; k1 == 0; k1 = j1 << 1 & 0xff)
 180:                  i1--;
 181:   
 182:   
 183:              if (i1 < 256 || i1 > 4096)
 184:                  throw new Exception("Invalid RSA modulus size.");
 185:              for (; abyte1[k] == 0; k++)
 186:                  l--;
 187:   
 188:   
 189:              publicExponent = new byte[l];
 190:              Array.Copy(abyte1, k, publicExponent, 0, l);
 191:              RSAParameters p = new RSAParameters();
 192:              p.Modulus = modulus;
 193:              p.Exponent = publicExponent;
 194:              return p;
 195:          }
 196:      }
 197:   
 198:      #endregion
 199:  }

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }

那么拿到这个类的实现.我们可以在windows phone 实现把公钥文件[.cer]字节流数据转换公钥信息RSAParameters对象导入到应用程序中.导出公钥PublicKey方法可以采用如下代码实现:

   1:  private System.Security.Cryptography.RSAParameters ConvertPublicKeyToRsaInfo()
   2:  {
   3:     System.Security.Cryptography.RSAParameters RSAKeyInfo;
   4:     using (var cerStream = Application.GetResourceStream(new Uri("/RSAEncryptDemo;component/Files/DemoPublicKey.cer", UriKind.RelativeOrAbsolute)).Stream)
   5:     {
   6:        byte[] cerBuffer = new byte[cerStream.Length];
   7:        cerStream.Read(cerBuffer, 0, cerBuffer.Length);
   8:        System.Security.Cryptography.X509Certificates.X509Certificate cer = new System.Security.Cryptography.X509Certificates.X509Certificate(cerBuffer);
   9:        RSAKeyInfo = X509PublicKeyParser.GetRSAPublicKeyParameters(cer.GetPublicKey());                    
  10:     }
  11:     return RSAKeyInfo;
  12:  }

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }

在执行这段代码你需要保证添加进来引入的公钥.Cer文件是作为项目资源被访问的.Build Action 设置为Resource. 这样我们就可以直接通过通过公钥文件的方式来获取公钥数据的信息.然后再默认构建的RSACryptoServiceProvider对象中通过ImportParameters()方法导入公钥信息:

   1:  RSACryptoServiceProvider rsa = new System.Security.Cryptography.RSACryptoServiceProvider();
   2:  rsa.FromXmlString(publickey);
   3:  RSAParameters publicKey = ConvertPublicKeyToRsaInfo();
   4:  rsa.ImportParameters(publicKey);

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }

其实默认的RSACryptoServiceProvider对象也可以采用XMl格式来导入公钥数据. 但前提是你需要知道公钥的Modulus和Exponent对应的值.但大多情况我们拿到公钥文件来实现跨平台传递信息的.如果需要这两个公钥字段数据. 我们可以通过服务器端通过接口来传递公钥的信息. RSA特点就是公钥是可以公开的.只要保证私钥是保密即可实现加密. 所以这种传递完全可行的. 拿到公钥数据后然后组合成对应XML 格式导入RSACryptoServiceProvider对象中.代码实现如下:

   1:  //导入文件方式代替 服务器接口的数据 原理是一至的
   2:  RSAParameters rsaDefineRap = ConvertPublicKeyToRsaInfo();
   3:  string modulus = Convert.ToBase64String(rsaDefineRap.Modulus);
   4:  string exponent = Convert.ToBase64String(rsaDefineRap.Exponent);
   5:   
   6:  string publickey = @"<RSAKeyValue><Modulus>" + modulus + "</Modulus><Exponent>" + exponent + "</Exponent></RSAKeyValue>";
   7:  RSACryptoServiceProvider rsa= new System.Security.Cryptography.RSACryptoServiceProvider();
   8:  rsa.FromXmlString(publickey);

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }

当我们拿到公钥的数据需要对拿到Byte字节数据进行Base64String编码.然后通过如上格式[不能错]拼接XML字符串.在通过FromXmlString()方法导入.也可以同样实现公钥信息的导入.在做Windows phone RSA 我也看到有人在Windows 8也遇到导入公钥的问题[Windows 8 系列(四):Win8 RSA加密相关问题]. 这个主要因为Windows 8导入公钥数据采用ASC编码.公钥信息头数据缺失导致的. 这个问题解决方案另外博文会说到.

如上我们采用两种方式来导入公钥PublicKey数据.拿到公钥数据后我们采用我么自己公钥进行数据加密 加密操作代码如下:

   1:  RSAParameters rsaDefineRap = ConvertPublicKeyToRsaInfo();
   2:  string modulus = Convert.ToBase64String(rsaDefineRap.Modulus);
   3:  string exponent = Convert.ToBase64String(rsaDefineRap.Exponent);
   4:   
   5:  //Import Public Key
   6:  string publickey = @"<RSAKeyValue><Modulus>" + modulus + "</Modulus><Exponent>" + exponent + "</Exponent></RSAKeyValue>";
   7:  RSACryptoServiceProvider rsaCrypt = new System.Security.Cryptography.RSACryptoServiceProvider();
   8:  rsaCrypt.FromXmlString(publickey); 
   9:   
  10:  //Data Encrypt
  11:  byte[] encryBytes=  rsaCrypt.Encrypt(System.Text.Encoding.UTF8.GetBytes("Hello World"), false);
  12:  return Convert.ToBase64String(encryBytes);

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }

发现加密数据成功.在执行加密操作Encrypt()方法中很多人会碰到CryptographicException Message 为Bad Length的异常 如下:

Windows phone应用开发[19]-RSA数据加密

Bad Length Exception.刚开始调试时特别频繁.这个异常主要问题是因为加密的数据长度超过公钥能够加密数据的范围导致的. 在RSA中我们能够加密数据的大小取决于公钥的长度大小.公钥长度和保密之间区别如下:

Windows phone应用开发[19]-RSA数据加密

RSA的数据加密大多是针对数据量较小的. 首先需要确认当前公钥大小是多Bit.我们目前演示的公钥文件.cer是1024 Bit的. Padding填充算法是[PKCS # 1 V1.5],ok 那么我们为了避免在加密操作出现这个异常我们需要加密数据大小Size进行判断. 可以采用如下方法来判断:

   1:          private bool IsOverEncryptStrMaxSize(int encryptDataSize)
   2:          {
   3:              bool isOver = false;
   4:              if (!IsDoOAEPPadding)
   5:              {
   6:                  #region When Encrypt Is Does't Need Padding Operator
   7:                  int MaxBlockSize = ((KeySize - 384) / 8) + 37;
   8:                  if (encryptDataSize > MaxBlockSize)
   9:                      isOver = true;
  10:                  #endregion
  11:              }
  12:              else
  13:              {
  14:                  #region When Encrypt Is Need Padd Operator
  15:                  int MaxBlockSize = ((KeySize - 384) / 8) + 7;
  16:                  if (encryptDataSize > MaxBlockSize)
  17:                      isOver = true;
  18:                  #endregion
  19:              }
  20:              return isOver;
  21:          }

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }

参数encryptDataSize 是当前把需要加密的数据转换成字节数组的长度. IsDoOAEPPanding是判断是否采用填充算法.这里需要说明的是:

   1:     byte[] encryBytes=  rsaCrypt.Encrypt(System.Text.Encoding.UTF8.GetBytes("Hello World"), false);

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }

加密操作. 如果采用False则默认RSA加密采用填充算法是[PKCS#1 1.5]Ps:我们证书一致. 如果为true.则对应加密填充算法是OAEP填充方式.目前.NET在RSA只支持[OAEP & PKCS#1 1.5]两种填充算法.当然还有更多.后买会讲到.我们这里采用FAlse也就是[PKCS#1 1.5]和我们证书保持一致. 如果这样我们通过该方法计算.如果是1024 Bit大小的公钥能够加密的数据是117个字节. 同理可以推断2048Bit 能够加密 245字节. 也就是说我们目前能够加密的不能够超过1024Bit对应的117个字节. 如果超过这个字节 就会抛出Bad Length异常.

有人说我要采用RSA加密一个整个Xml文件.这就涉及到另外一个问题-RSA如何加密到大数据. 可以参考StackOverFllow 给出解决方案[RSA of large file using C# [duplicate] .其实核心的原理信息头采用RSA进行加密.主干内容也就是大数据内容采用的3DES配合进行加密. 解密是采用相同规则解密即可.并无难度. 如果需要解决我会在另外一个章节讲到.

如果是windows phone 我并不推荐你加密整个文件或是其他特别大的数据方式.如果你的数据小于1M 以内. 采用RSA加密. 并不推荐直接加密整个数据内容. 而是在加密前.把整个数据进行MD5加密.这样一来几百字节数据内容会被生成一个唯一的16位或32为大小的字符进行代替. 然后把MD5 对应的字符数据进行加密.就不会出现大数据加密时Bad Length异常.当然解密时也需要同样的规则. RSA加密前规则和解密必须保证一致.即可.

跨平台互通

RSA在客户端加密一般要涉及到服务器端的解密. 而服务器端大多采用其他平台构建的.类似Java. 在调试时大多情况会碰到客户端加密已经成功.而服务器端一值无法解密.存在互通的问题.

首先来看一个基本问题我们在客户端采用如上加密方式来加密同一个MD5 字符串:

   1:  //MD5 String
   2:  7E9667A96C301BF79979E49956D189C7

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }

加密两次查看查看同一个公钥对同一条数据的加密结果:

   1:  //First
   2:  tbn5ejK21uJxObBYRp1Bh8k9FrmMWDFKRuithTKU7OITeO8Wss+j6Q3FAcE7x7EA1KpPMhCgnIj6BbQlw+Xeat8Kj/s8SLH3Vel0UPS3+gvshDW8vm2qQsPlsbg3HQ7xD6P/OLdnRleOY9VWG31n3ZouYEKJp2G0FnK/w2VD9zs=
   3:   
   4:  //Second
   5:  lMw9QaU8MtXvyknEv2M9RNGcOR2UQKC44BD4i95seBjBnthcXosGh9O9DCYBEQuMNTTXX8pI5ipUwEBJNKbN4jmx7lPoxg4khxbmgaofq71sd1hFwY58Or29lxprm4dXPHkM2LsgifRazlpddHN3lKszH3i065fy8LJcrNmZmCU=

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }

很明显我们发现在同一公钥针对同一条数据加密结果居然是不一致的.而同样的算法在Java针对同一条数据加密始终都是一致的.

这里需要说明的是在.net平台上为了保证RSA加密算法的安全性.在每次加密的时候都会生成一定的随机数和原始数据一块被加密.导致每次加密的结果因为添加随机数不同加密结果也不一致.其实这些随机数也是遵循算法标准的.也就是上面提到的随机填充算法.比如NoPadding、ISO10126Padding、OAEPPadding、PKCS1Padding、PKCS5Padding、SSL3Padding,而最常见的应该是OAEPPadding【Optimal Asymmetric Encryption Padding】、PKCS1Padding;.Net支持PKCS1Padding或OAEPPadding,其中OAEPPadding仅在XP或更高版本的操作系统上可用.

Java的实现,主要通过Cipher.getInstance实现,传入的参数是描述为产生某种输出而在给定的输入上执行的操作(或一组操作)的字符串。必须包括加密算法的名称,后面可能跟有一个反馈模式和填充方案。这样的实现就比较灵活,我们可以通过参数指定不同的反馈模式和填充方案;比如Cipher.getInstance("RSA/ECB/PKCS1Padding"),或者Cipher.getInstance("RSA")均可,但用其加密的效果也会不一样.

NET平台加入随机数的概念.当采用.NEt 平台加密后.在通过Java服务器端解密时因Java采用的是标准的RSA加密.不添加随机数的. 导致java平台服务器端解密失败.这个时候回导致.NET 加密数据无法与Java平台的互通.

解决这个问题有两种思路:

A:在.NET 客户端采用剔除掉加密时添加的随机数. 采用标准的RSA算法加密数据. Java服务器端采用标准RSA解密即可

B:.NET客户端和Java服务器端采用相同随机数填充标准. 实现一致的数据加密和解密操作.

思路A.其实现在。NEt FrameWork提供的RSACryptoServiceProvider对象因为在家随机数. 我们需要自己实现一个标准的RSA算法. 然后对需要加密数据进行加密.针对windows phone 标准的RSA算法移植代码可以参考[C# BigInteger Class]: 这里移植版本.这样一来我们加密操作就采用BitInteger类来进行 代码如下:

   1:   System.Security.Cryptography.RSAParameters RSAKeyInfo = ConvertPublicKeyToRsaInfo();
   2:   BigInteger bi_e = new BigInteger(RSAKeyInfo.Exponent);
   3:   BigInteger bi_n = new BigInteger(RSAKeyInfo.Modulus);
   4:   
   5:   BigInteger bi_data = new BigInteger(System.Text.Encoding.UTF8.GetBytes("Hello World"));//
   6:   BigInteger bi_encrypted = bi_data.modPow(bi_e, bi_n);
   7:   return bi_encrypted.getBytes();

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }

.NET客户端构建一个公钥对应的BigInteger e、一个模对应的BigInteger n和一个明文对应的BigInteger m,然后执行语句BigInteger c=m.modPow(e,n),便可以实现加密操作,密文为c,这样的加密是标准加密,没有附加任何填充算法的加密. 然对加密数据进行Base64String编码. 在传递服务器端解密验证.通过.

思路B.如果.net和Java 平台在进行RSA加密时采用的填充保持标准一致. 那么Java和.net 平台数据加密和解密.即使每次加密数据结果都一致.也是可以互通的.而.NET 平台目前只是实现两种填充方式.[OAEP & PKCS#1 1.5]. 而Java平台恰好支持其中的PKCS#1 1.5. 这样一来我们可以采用统一的填充标准加密即可.

   1:  rsa.Encrypt(contentBytes, false)

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }

加密时设置是否填充为False.也就是采用默认的PKC#1 1.5填充标准.

但调试依然发现服务器端解密失败. 经历过很长一段时间都搞不清楚这个问题具体出在哪.直到我看到MSDN上关于RSACryptoServiceProvider对象Remark里卖弄描述.瞬间明白这个问题出在那如下:

Remark:

This is the default implementation of RSA.

The RSACryptoServiceProvider supports key lengths from 384 bits to 16384 bits in increments of 8 bits if you have the Microsoft Enhanced Cryptographic Provider installed. It supports key lengths from 384 bits to 512 bits in increments of 8 bits if you have the Microsoft Base Cryptographic Provider installed.

Interoperation with the Microsoft Cryptographic API (CAPI)

Unlike the RSA implementation in unmanaged CAPI, the RSACryptoServiceProvider class reverses the order of an encrypted array of bytes after encryption and before decryption. By default, data encrypted by the RSACryptoServiceProvider class cannot be decrypted by the CAPI CryptDecrypt function and data encrypted by the CAPI CryptEncrypt method cannot be decrypted by the RSACryptoServiceProvider class.

If you do not compensate for the reverse ordering when interoperating between APIs, the RSACryptoServiceProvider class throws a CryptographicException.

To interoperate with CAPI, you must manually reverse the order of encrypted bytes before the encrypted data interoperates with another API. You can easily reverse the order of a managed byte array by calling the Array.Reverse method.

你看到了吧.最后两段译文:

默认情况下,CAPI CryptDecrypt 函数无法解密由 RSACryptoServiceProvider 类加密的数据,RSACryptoServiceProvider 类无法解密由 CAPI CryptEncrypt 方法加密的数据。

如果在 API 之间互相操作时没有对颠倒的顺序进行补偿,RSACryptoServiceProvider 类会引发 CryptographicException

要同 CAPI 相互操作,必须在加密数据与其他 API 相互操作之前,手动颠倒加密字节的顺序。 通过调用 Array.Reverse 方法可轻松颠倒托管字节数组的顺序

也就是说。net平台对标准的RSA算法字节数组采用自动翻转.你要是还原到标准的加密结果需要采用Array.Reverse()方法翻转过来/.[汗啊].我采用系统对象加密采用Reverse()操作发现Java服务器端解密成功了 完成代码如下:

   1:             #region Get Data Encrypt Public Key
   2:              RSAParameters rsaDefineRap = ConvertPublicKeyToRsaInfo();
   3:              string modulus = Convert.ToBase64String(rsaDefineRap.Modulus);
   4:              string exponent = Convert.ToBase64String(rsaDefineRap.Exponent);
   5:              string publickey = @"<RSAKeyValue><Modulus>" + modulus + "</Modulus><Exponent>" + exponent + "</Exponent></RSAKeyValue>";
   6:   
   7:              RSACryptoServiceProvider rsa = new System.Security.Cryptography.RSACryptoServiceProvider();
   8:              rsa.ImportParameters(rsaDefineRap);
   9:              //rsa.ExportParameters(false);
  10:   
  11:              #region Control Encrypt To Md5 Format String
  12:              string encryptWithMd5Str = MD5Core.GetHashString(needEncryptStr);
  13:              #endregion
  14:   
  15:              #region Encrypt Not Over The Max Size
  16:              byte[] needEncryptBytes = System.Text.UTF8Encoding.UTF8.GetBytes(encryptWithMd5Str);
  17:              byte[] encryptBytes = null;
  18:              if (!IsOverEncryptStrMaxSize(needEncryptBytes.Length))
  19:              {
  20:                  encryptBytes = rsa.Encrypt(needEncryptBytes, IsDoOAEPPadding);
  21:                  if (encryptBytes != null)
  22:                      encryptBytes.Reverse();
  23:              }
  24:              #endregion
  25:   
  26:              return encryptBytes == null ? "" : Convert.ToBase64String(encryptBytes);
  27:              #endregion

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }

如果你在最后数据加密成功后忘了这段代码:

   1:    if (encryptBytes != null)
   2:        encryptBytes.Reverse();

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }

我只能对你说God Bless you.

如上两种思路分别都能实现.NET 和Java平台的互通.其中思路B的方式最为廉价.只需要确保两个平台之间填充算法一致. 然后对加密结果进行Reverse()翻转操作即可.这个问题在我看到MSDN文档REmark文档突然就释然了.

核心代码都在上面.需要源码在直接@我把. 等我整理上传到Github上[https://github.com/chenkai]

Contact [@chenkaihome]

参考资料:

C# BigInteger Class

RSACryptoServiceProvider

Why does my Java RSA encryption give me an Arithmetic Exception?

Java RSA Encrypt - Decrypt .NET - need help

上一篇:I/O requests taking longer than 15 seconds to complete on file I/O瓶颈问题


下一篇:044 01 Android 零基础入门 01 Java基础语法 05 Java流程控制之循环结构 06 使用do-while循环实现猜字游戏