Java RSA研究

前几天我做了PHP RSA的测试,想想Java来做RSA应该更简单,就在网上搜了一下Java RSA,发现资料是有一些,但却不是很完整,也不是很严谨,都是转来转去,看来还是自己要测试一下,两个文件RSAUtil.java和EncryptException.java,注意,JDK里没有RSA的provider,所以我们要用第三方的provider,在http://www.bouncycastle.org/下载最新的包,加入工程。

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
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
import javax.crypto.Cipher;
import java.security.*;
import java.security.spec.RSAPublicKeySpec;
import java.security.spec.RSAPrivateKeySpec;
import java.security.spec.InvalidKeySpecException;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.io.*;
import java.math.BigInteger;
 
public class RSAUtil {
 
    /**
     *   * 生成密钥对
     *   * @return KeyPair
     *   * @throws EncryptException
     */
    public static KeyPair generateKeyPair() throws EncryptException {
        try {
            KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA",
                    new org.bouncycastle.jce.provider.BouncyCastleProvider());
            final int KEY_SIZE = 1024;//没什么好说的了,这个值关系到块加密的大小,可以更改,但是不要太大,否则效率会低
            keyPairGen.initialize(KEY_SIZE, new SecureRandom());
            KeyPair keyPair = keyPairGen.genKeyPair();
            return keyPair;
        } catch (Exception e) {
            throw new EncryptException(e.getMessage());
        }
    }
 
    /**
     *   * 生成公钥
     *   * @param modulus
     *   * @param publicExponent
     *   * @return RSAPublicKey
     *   * @throws EncryptException
     */
    public static RSAPublicKey generateRSAPublicKey(byte[] modulus, byte[] publicExponent) throws EncryptException {
        KeyFactory keyFac = null;
        try {
            keyFac = KeyFactory.getInstance("RSA", new org.bouncycastle.jce.provider.BouncyCastleProvider());
        } catch (NoSuchAlgorithmException ex) {
            throw new EncryptException(ex.getMessage());
        }
 
        RSAPublicKeySpec pubKeySpec = new RSAPublicKeySpec(new BigInteger(modulus), new BigInteger(publicExponent));
        try {
            return (RSAPublicKey) keyFac.generatePublic(pubKeySpec);
        } catch (InvalidKeySpecException ex) {
            throw new EncryptException(ex.getMessage());
        }
    }
 
    /**
     *   * 生成私钥
     *   * @param modulus
     *   * @param privateExponent
     *   * @return RSAPrivateKey
     *   * @throws EncryptException
     */
    public static RSAPrivateKey generateRSAPrivateKey(byte[] modulus, byte[] privateExponent) throws EncryptException {
        KeyFactory keyFac = null;
        try {
            keyFac = KeyFactory.getInstance("RSA", new org.bouncycastle.jce.provider.BouncyCastleProvider());
        } catch (NoSuchAlgorithmException ex) {
            throw new EncryptException(ex.getMessage());
        }
 
        RSAPrivateKeySpec priKeySpec = new RSAPrivateKeySpec(new BigInteger(modulus), new BigInteger(privateExponent));
        try {
            return (RSAPrivateKey) keyFac.generatePrivate(priKeySpec);
        } catch (InvalidKeySpecException ex) {
            throw new EncryptException(ex.getMessage());
        }
    }
 
    /**
     *   * 加密
     *   * @param key 加密的密钥
     *   * @param data 待加密的明文数据
     *   * @return 加密后的数据
     *   * @throws EncryptException
     */
    public static byte[] encrypt(Key key, byte[] data) throws EncryptException {
        try {
            Cipher cipher = Cipher.getInstance("RSA", new org.bouncycastle.jce.provider.BouncyCastleProvider());
            cipher.init(Cipher.ENCRYPT_MODE, key);
            int blockSize = cipher.getBlockSize();//获得加密块大小,如:加密前数据为128个byte,而key_size=1024 加密块大小为127 byte,加密后为128个byte;因此共有2个加密块,第一个127 byte第二个为1个byte
            int outputSize = cipher.getOutputSize(data.length);//获得加密块加密后块大小
            int leavedSize = data.length % blockSize;
            int blocksSize = leavedSize != 0 ? data.length / blockSize + 1 : data.length / blockSize;
            byte[] raw = new byte[outputSize * blocksSize];
            int i = 0;
            while (data.length - i * blockSize > 0) {
                if (data.length - i * blockSize > blockSize)
                    cipher.doFinal(data, i * blockSize, blockSize, raw, i * outputSize);
                else
                    cipher.doFinal(data, i * blockSize, data.length - i * blockSize, raw, i * outputSize);
//这里面doUpdate方法不可用,查看源代码后发现每次doUpdate后并没有什么实际动作除了把byte[]放到ByteArrayOutputStream中,而最后doFinal的时候才将所有的byte[]进行加密,可是到了此时加密块大小很可能已经超出了OutputSize所以只好用dofinal方法。
 
                i++;
            }
            return raw;
        } catch (Exception e) {
            throw new EncryptException(e.getMessage());
        }
    }
 
 
 
    /**
     *   * 解密
     *   * @param key 解密的密钥
     *   * @param raw 已经加密的数据
     *   * @return 解密后的明文
     *   * @throws EncryptException
     */
    public static byte[] decrypt(Key key, byte[] raw) throws EncryptException {
        try {
            Cipher cipher = Cipher.getInstance("RSA", new org.bouncycastle.jce.provider.BouncyCastleProvider());
            cipher.init(cipher.DECRYPT_MODE, key);
            int blockSize = cipher.getBlockSize();
            ByteArrayOutputStream bout = new ByteArrayOutputStream(64);
            int j = 0;
 
            while (raw.length - j * blockSize > 0) {
                bout.write(cipher.doFinal(raw, j * blockSize, blockSize));
                j++;
            }
            return bout.toByteArray();
        } catch (Exception e) {
            throw new EncryptException(e.getMessage());
        }
    }
 
    public static String byte2hex(byte[] b) {
        String hs = "";
        String stmp = "";
        for (int i = 0; i < b.length; i++) {
            stmp = Integer.toHexString(b[i] & 0xFF);
            if (stmp.length() == 1) {
                hs += "0" + stmp;
            } else {
                hs += stmp;
            }
        }
        //return hs;
        return hs.toUpperCase();
    }
 
 
    public static byte[] hex2byte(String hex) throws IllegalArgumentException {
        if (hex.length() % 2 != 0) {
            throw new IllegalArgumentException();
        }
        char[] arr = hex.toCharArray();
        byte[] b = new byte[hex.length() / 2];
        for (int i = 0, j = 0, l = hex.length(); i < l; i++, j++) {
            String swap = "" + arr[i++] + arr[i];
            int byteint = Integer.parseInt(swap, 16) & 0xFF;
            b[j] = new Integer(byteint).byteValue();
        }
        return b;
    }
 
    /**
     *   *
     *   * @param args
     *   * @throws Exception
     */
    public static void main(String[] args) throws Exception {
 
        byte[] orgData = "test".getBytes();
        KeyPair keyPair = RSAUtil.generateKeyPair();
        RSAPublicKey pubKey = (RSAPublicKey) keyPair.getPublic();
        RSAPrivateKey priKey = (RSAPrivateKey) keyPair.getPrivate();
 
        byte[] pubModBytes = pubKey.getModulus().toByteArray();
        System.out.println("PubKey Modulus:"+new BigInteger(pubModBytes).toString(16));
        byte[] pubPubExpBytes = pubKey.getPublicExponent().toByteArray();
        System.out.println("publicExponent:"+new BigInteger(pubPubExpBytes).toString(16));
        byte[] priModBytes = priKey.getModulus().toByteArray();
        byte[] priPriExpBytes = priKey.getPrivateExponent().toByteArray();
        RSAPublicKey recoveryPubKey = RSAUtil.generateRSAPublicKey(pubModBytes, pubPubExpBytes);
        RSAPrivateKey recoveryPriKey = RSAUtil.generateRSAPrivateKey(priModBytes, priPriExpBytes);
 
        byte[] raw = RSAUtil.encrypt(priKey, orgData);
        System.out.println("Encrypt:"+byte2hex(raw));
        byte[] data = RSAUtil.decrypt(recoveryPubKey, raw);
        System.out.println("Decrypt:"+new String(data));
 
    }
 
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class EncryptException extends Exception {
 
    public EncryptException() {
        super();
    }
 
    public EncryptException(String message) {
        super(message);
    }
 
    public EncryptException(String message, Throwable cause) {
        super(message, cause);
    }
 
    public EncryptException(Throwable cause) {
        super(cause);
    }
 
}

我们看一下运行结果:

1
2
3
4
PubKey Modulus:a907de8b5789b1df66c8a4ea90f99c9b00bbad520d487a7e218cd1ee2a1cafcaff2dd03a70cc61d8ccdfe0557b9132dd163a5a6c287d94790fc8b573a154ba0cd799e2cc73fc44c03083274760664125cafc33c647c44df300968665b6dbc9e553c59f8180de0ded3ae2163aab499c1ec0688ed7468fb816cdf05db501cbba19
publicExponent:10001
Encrypt:71521BBA91D871AAA8FF99D7E9D6E44DAE6218FAFDC07CE7C34ABACC1357854BF8F5D94F17FD106346B0916CC81A85B7031F9421810F1E5A568EE408E6DFDB219201CB1DE2AF259A516F3B930130D6AD4FFCB26072EB2BD9CFC11CA727B181A640311BC3023B2D1E54EBC52454C669389C24D0F7AD316B0B24257D53384E1446
Decrypt:test

使用已有的Modulus加密的话

1
2
3
4
5
6
7
8
String modulus = "D192471B8699640F931FE6F4FACC3E990B894F894CEA5BEE0DCBD7A4B76752F7345CF9B5F1271001B724F7A0ABF0A6E911E309536F4BE4749E92DCC531B8E36B95969D206649C9DD2371B413A8DFD9B92569660B1499A5CD310B86A8FDE24988E456897A416D2E7B0B649F0714F322C57EF92563B21A448D1072FF3806C34C75";
byte[] b_modulus = new BigInteger(modulus,16).toByteArray();
String publicExponent = "10001";
byte[] b_publicExponent = new BigInteger(publicExponent,16).toByteArray();
String text = "test";
RSAPublicKey recoveryPubKey = RSAUtil.generateRSAPublicKey(b_modulus, b_publicExponent);        
byte[] ss = RSAUtil.encrypt(recoveryPubKey,text.getBytes());
System.out.println(byte2hex(ss));

PHP RSA研究

最近研究了一下QQ邮箱的登录过程,发现QQ邮箱登录比较严谨,加了一些小技巧,其中一点就是用JavaScript对用户的密码做了一下RSA的加密,在它的登录页面里有一段

1
2
3
4
5
6
7
8
var PublicKey = "CF87D7B4C864F4842F1D337491A48FFF54B73A17300E8E42FA365420393AC0346AE55D8AFAD975DFA175FAF0106CBA81AF1DDE4ACEC284DAC6ED9A0D8FEB1CC070733C58213EFFED46529C54CEA06D774E3CC7E073346AEBD6C66FC973F299EB74738E400B22B1E7CDC54E71AED059D228DFEB5B29C530FF341502AE56DDCFE9";
var RSA = new RSAKey();
RSA.setPublic(PublicKey, "10001");
var Res = RSA.encrypt(document.form1.pp.value + '\n' + document.form1.ts.value + '\n');
if (Res)
{
document.form1.p.value = hex2b64(Res);
}

再看看RSAKey的相关源码,应该是在http://m367.mail.qq.com/zh_CN/htmledition/js/safeauth.js里,看了一下还是蛮复杂的,我就在Google上搜一下有没有相关的资料,找到了这个网站《BigIntegers and RSA in JavaScript》,看了一下他们的代码,和QQ里的基本一样嘛,估计QQ也是用了人家的代码,这篇文章里有密钥对的生成、加密解密的测试页面,由此推断QQ代码里的PublicKey,应该是密钥对的modulus(也可以理解为公钥吧),并且QQ用的是1024位的密钥,那我们是否可以用这个modulus,使用别的语言(比如PHP)来做密码加密呢?

想到就做,我在Google上搜索一下“PHP RSA”,找到了这个网站http://www.edsko.net/misc/,里面有PHP RSA的实现,不过我对它的rsa_encrypt($message, $public_key, $modulus, $keylength)方法产生了疑惑,它有4个参数,第一个是要加密的字串,那后面三个怎么填,我们现在仅知道的就是modulus了,而$public_key和$keylength从何而来呢,真有点摸不着头脑了,还是看看它带的例子吧,原来它例子里是从密钥对文件中取得相关的信息,QQ的密钥对文件自然取不到,那我只能自己生成一个密钥对文件,来看看有什么规律吧,在Linux下用openssl做

openssl genrsa -out key.pem 1024

生成了一个1024位的密钥对文件,可以打开看看,里面应该是Base64编码的,之后我们通过下面的命令可以得到modulus

1
2
openssl rsa -in key.pem -noout -modulus
Modulus=D192471B8699640F931FE6F4FACC3E990B894F894CEA5BEE0DCBD7A4B76752F7345CF9B5F1271001B724F7A0ABF0A6E911E309536F4BE4749E92DCC531B8E36B95969D206649C9DD2371B413A8DFD9B92569660B1499A5CD310B86A8FDE24988E456897A416D2E7B0B649F0714F322C57EF92563B21A448D1072FF3806C34C75

比照QQ的,位数是一样的,接下来我们用命令

openssl rsa -in key.pem -text -noout

输出的内容如下:
Private-Key: (1024 bit)
modulus:
00:d1:92:47:1b:86:99:64:0f:93:1f:e6:f4:fa:cc:
3e:99:0b:89:4f:89:4c:ea:5b:ee:0d:cb:d7:a4:b7:
67:52:f7:34:5c:f9:b5:f1:27:10:01:b7:24:f7:a0:
ab:f0:a6:e9:11:e3:09:53:6f:4b:e4:74:9e:92:dc:
c5:31:b8:e3:6b:95:96:9d:20:66:49:c9:dd:23:71:
b4:13:a8:df:d9:b9:25:69:66:0b:14:99:a5:cd:31:
0b:86:a8:fd:e2:49:88:e4:56:89:7a:41:6d:2e:7b:
0b:64:9f:07:14:f3:22:c5:7e:f9:25:63:b2:1a:44:
8d:10:72:ff:38:06:c3:4c:75
publicExponent: 65537 (0x10001)
privateExponent:
00:83:d3:d9:08:f6:95:3c:bd:13:56:29:09:07:4e:
3d:3e:36:64:8c:74:98:be:7f:4f:72:bc:3c:0c:f0:
15:7d:b9:e4:e5:6b:6a:c8:a4:42:cc:61:71:4e:97:
72:30:f2:3d:80:33:e9:a4:e3:48:c1:0f:9e:c4:51:
3d:75:f6:90:8e:f3:c3:f8:ce:45:59:2a:67:42:a8:
c6:d0:4c:1d:12:c4:cf:53:f8:b1:58:b4:e1:23:71:
0e:e9:e9:e0:40:3d:9a:99:e3:5f:e1:93:04:e2:0a:
60:34:77:56:be:f9:8f:e6:4e:87:23:46:48:ba:38:
9d:dd:46:ce:20:b7:82:27:cd
prime1:
00:ee:a9:e4:70:9c:d4:fe:bf:cd:87:5c:00:cb:ea:
ef:82:92:e1:88:f7:99:6a:42:09:f4:fd:78:93:bd:
30:28:1f:2e:ed:c1:cd:d3:60:8b:34:52:89:a7:ac:
98:37:cd:96:81:1e:57:2f:46:08:0e:8d:fb:13:92:
8d:f5:7a:50:5f
prime2:
00:e0:cb:65:5e:31:f2:3b:c0:7f:93:ae:d9:6c:35:
75:e5:ce:8b:37:7d:39:ce:82:dd:9b:43:00:09:a6:
d8:c1:ab:bc:10:fe:3d:56:34:fe:bd:38:fe:fc:6c:
f2:74:a8:d6:40:25:e5:5a:35:7b:d0:24:71:44:8d:
53:23:71:83:ab
exponent1:
4b:d5:7f:d8:a8:7c:a5:55:9c:a0:de:03:02:c8:6b:
c2:39:99:a0:43:cc:63:8f:08:4a:e8:1f:60:12:45:
32:fa:75:96:e6:75:d8:2c:5d:0f:0b:0a:e2:54:5d:
29:9e:11:ac:85:4f:7e:9d:ea:01:75:eb:c9:94:4f:
b7:28:5e:51
exponent2:
00:9b:9f:d4:56:a8:e7:55:3c:88:55:fa:97:a5:55:
41:80:ce:44:0d:2f:51:a4:c9:6e:97:fd:83:7a:2b:
1b:26:c1:38:da:de:d8:21:e5:60:72:29:92:45:b9:
3b:05:4e:99:bd:21:3f:2d:fb:96:f2:db:37:db:48:
a7:c5:02:e2:2f
coefficient:
00:c2:75:38:a5:02:24:39:1e:0e:e9:ec:56:6a:31:
5d:38:82:ca:3e:9b:67:cb:40:7e:7b:2f:91:26:bb:
4e:64:3d:60:53:f1:21:67:8b:b7:af:f8:2e:95:f7:
af:cf:42:75:ab:6c:5c:42:97:42:17:94:17:ff:e0:
b9:cb:c9:e8:6d

通过它例子的代码,我明白了,$public_key应该是1024,$keylength就是65537,$modulus不能直接用这段文字,要先转成BigInteger,再转成文本传进去,BigInteger实现在PEAR里有,http://pear.php.net/package/Math_BigInteger

我们接下来就写程序吧

1
2
3
4
5
6
7
8
9
10
11
include('rsa.php');
include('BigInteger.php');
 
$public = 65537;
$modulus = "D192471B8699640F931FE6F4FACC3E990B894F894CEA5BEE0DCBD7A4B76752F7345CF9B5F1271001B724F7A0ABF0A6E911E309536F4BE4749E92DCC531B8E36B95969D206649C9DD2371B413A8DFD9B92569660B1499A5CD310B86A8FDE24988E456897A416D2E7B0B649F0714F322C57EF92563B21A448D1072FF3806C34C75";
$keylength = 1024;
$modulus_16 = new Math_BigInteger($modulus,16);
$mend = $modulus_16->toString();
 
$encrypted = rsa_encrypt("test", $public, $mend, $keylength);
echo bin2hex($encrypted); //这里也可以用Base64,QQ就是Base64

最后说一下我对于RSA的理解,首先生成了公钥/私钥的密钥对,之后把公钥发布出去,外部系统用公钥加密,传给内部系统用私钥解密。