zoukankan      html  css  js  c++  java
  • 全面解决.Net与Java互通时的RSA加解密问题,使用PEM格式的密钥文件

    一、缘由

    RSA是一种常用的非对称加密算法。所以有时需要在不用编程语言中分别使用RSA的加密、解密。例如用Java做后台服务端,用C#开发桌面的客户端软件时。
    由于 .Net、Java 的RSA类库存在很多细节区别,尤其是它们支持的密钥格式不同。导致容易出现“我加密的数据对方不能解密,对方加密的数据我不能解密,但是自身是可以正常加密解密”等情况。
    虽然网上已经有很多文章讨论 .Net与Java互通的RSA加解密,但是存在不够全面、需要第三方dll、方案复杂 等问题。
    于是我仔细研究了这一课题,得到了一些稳定可靠的代码。现在将研究成果分享给大家。

    二、密钥

    2.1 RSA密钥文件格式介绍

    要保证 .Net与Java 两端均能正常的加解密,其中的重中之重就是确立一种密钥文件格式,使 .Net与Java 两端均能正确的加载密钥。

    .Net与Java内置类库对密钥文件格式的支持情况——

    • .Net: 支持xml格式的密钥文件。
    • Java: 没有直接提供对密钥文件的支持,仅提供了 PKCS#8、X.509 等编码的密钥数据的解析类。

    2.1.1 技术细节——密钥文件为什么这么复杂

    看到 PKCS#8、X.509,大家是否有些头晕了?
    其实RSA的密钥文件不止这2种,还有许多种存储格式。可参考 蒋国纲《那些证书相关的玩意儿(SSL,X.509,PEM,DER,CRT,CER,KEY,CSR,P12等)》。

    为什么RSA密钥文件这么复杂,这是因为密钥文件需存储多个数值。具体来说,RSA加解密中有5个重要的数字 p,q,n(Modulus),e(Exponent),d。然后公钥与私钥分别要存储不同的值——

    • 公钥:需存储 n、e。
    • 私钥:需存储 n、d。而对于常用的X.509等编码的私钥文件中,其不仅存储了 n、e、d、p、q,还存储了 d mod (p-1)、d mod (q-1)、(inverse of q) mod p 等用于简化、校验加密的值。

    所以我们会发现私钥文件的字节数,一般比公钥文件大一些。

    为了统一密钥文件格式,我们不得不编写密钥解析代码,这需要理解rsa的p、q、n、e、d 具体含义与用法。学习难度较高,需要一定时间仔细研读。
    所以我便封装了一些稳定、可靠的函数来处理这些内容。使下次可以直接用这些函数,不用再次费神处理这些复杂的技术细节。

    若想支持绝大多数的密钥文件格式,推荐使用 OpenSSL库。它支持 .Net与Java。
    可是,该库比较庞大,项目依赖多会导致部署麻烦,不适合小型程序。所以我们还是选择一种格式比较好。

    2.2 确立密钥文件格式

    我挑选密钥文件格式有2个条件——

    1. 文本格式。这样用记事本打开密钥文件,能够方便的复制粘贴,且能作为程序中的字符串常量。使用灵活,方便测试等。
    2. 易于生成。不必编写、运行代码来生成,而是能够通过多种办法来生成密钥对。既可以命令行生成,又可以通过图形界面工具点击生成。

    所以最终选择了 PEM(Privacy Enhanced Mail)格式的密钥文件。用记事本打开可看到文本内容,其以"-----BEGIN..."开头,以"-----END..."结尾,内容是BASE64编码。
    随后对于具体的公钥、私钥的编码格式,选择了 PKCS#8 与 X.509,具体情况是——

    • 公钥:X.509 pem。Java类为 X509EncodedKeySpec 。
    • 私钥:PKCS#8 pem。Java类为 PKCS8EncodedKeySpec 。

    2.3 生成密钥

    首先,可使用代码来生成密钥对,.Net、Java的类库有完善的支持。该办法适合于自己生成、管理密钥的项目。但对于一些小型项目来说,该办法比较复杂,不太实用。
    其次,可以使用 OpenSSL 等命令行工具来生成密钥。需要花点时间来学习命令行,并且需要安装相应工具,稍微有点麻烦。

    其实还有第三种方法,就是用在线工具来生成密钥。因为我们用的是PEM格式的密钥,该格式简单,很多在线工具都支持。

    例如 http://web.chacuo.net/netrsakeypair
    用法——

    1. 选择“生成密钥位数”。直接使用默认的“2048位”就行,因为2048位是目前主流的密钥位数,且.Net、Java均支持该长度。
    2. 选择“密钥格式”。直接使用默认的“PKCS#8”就行,因为我们也是采用这种格式。
    3. 填写“证书密码”。一般不用填写。
    4. 点击“生成密钥对(RSA)”。随后下面的两个文本框分别会出现公钥与私钥,便可复制粘贴进行保存了。

    2.3.1 本文范例用的密钥

    公钥(public1.pem)

    -----BEGIN PUBLIC KEY-----
    MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAywl5THDMsLUbzYX66YGp
    Mr9AaiX6NNHp4gOQMa0BDM125ZftY/YL7ZJT9TgnVegK/vVSJn2PoGTw+x0OMx86
    nCXOxX7h7xRt6oVRq3ekN36kBjGm56MFbYpAaLg0LLfPQcZME1g6T8CGCGpSZR90
    bwqBh56uRFKa5ptJwLCloCc9fvW4uP6M/CcaRcpRcF0f4ofV/Urvq2l4Id+XxQyr
    WX1JgR9mo6dvUaaX9osjZW615t6PlyoewkUUfv5rNTh7wjIZzKLl+pD8YCheZ7aJ
    PlJWaIuwSENgVEYEbXcOyCbr2HqWA7EKA5+QxSaVy5z7q5BDpEz8ky3QxRfj+EDJ
    VQIDAQAB
    -----END PUBLIC KEY-----

    私钥(private1.pem)

    -----BEGIN PRIVATE KEY-----
    MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDLCXlMcMywtRvN
    hfrpgakyv0BqJfo00eniA5AxrQEMzXbll+1j9gvtklP1OCdV6Ar+9VImfY+gZPD7
    HQ4zHzqcJc7FfuHvFG3qhVGrd6Q3fqQGMabnowVtikBouDQst89BxkwTWDpPwIYI
    alJlH3RvCoGHnq5EUprmm0nAsKWgJz1+9bi4/oz8JxpFylFwXR/ih9X9Su+raXgh
    35fFDKtZfUmBH2ajp29Rppf2iyNlbrXm3o+XKh7CRRR+/ms1OHvCMhnMouX6kPxg
    KF5ntok+UlZoi7BIQ2BURgRtdw7IJuvYepYDsQoDn5DFJpXLnPurkEOkTPyTLdDF
    F+P4QMlVAgMBAAECggEAIbtJM7Hpz9HG9LY1oWWxPoUXpor4rp3RRYNiCV68tevM
    vQgooFrYUHfnCu5xWoxah1EqfMqPeg5LGu0Q1t1xV0/Qsm8KCjZSrIvJrbsKxU18
    4qqNGB61YCV/3eX8hRFklYDkUrJtvaI2ol9HoRVAutH8AxQRz7gJlBZogmLWoWyX
    r5CwPat/6n7mw//LtSblP9A10I8X+1G+9LFF48TKIZWvxkCkiLWiFwqQgbmfVdw8
    vtCyMHLb62C3o6qTEjOYGD3xlE5kGPO7AovUihC8e/E5CaR840p+5j12qy62VbG6
    7d0KFHIwAF4njhQA1wEWn+C+27lzE1Ps9eb3xlQdYQKBgQDuHCd0UewvL9YF6TYA
    y2IuYtwDBlF2TZpJ5+y396ncHhdL90vAeIoDcBlK8zwBuH1M7Ewv3NlcNB1zlT95
    itltPqdDkdl4TXboDTWrIhDD5RqiowrLTRSlO1hdZOw9ya88lxLYsUvMrNZzR3zW
    T355YzqIC9JQYRu/O7+nysPiGwKBgQDaSrhz13c+PrUeExE34y3cdlN5aZkn3Rw/
    MRpQWpV0+9NuTdBizENZ5uW3kCTI5+vk3OmgmCa2Lq48LZjKPa7BffIPK406V1Vs
    xSZyzeTRRtaG7+Is1uTyASAimQ/0EIX3HjtZmHSPGeKyvYhKy0M+W1j1zPN1iP6w
    Dy1nUMI5TwKBgQDQ5EQ8yQ4yi33w65rj8Ynt9e7cfHOFHSmpgt1qu8z5/jAkBg0g
    Ct/Riku2NFPFkqviiz9/kfni6RmZaCsqnwSG0bt+DPtDjnottEEMJLOemGTYn779
    gl8FYl3weXTD9CdXOZZgIpLEOjFdKy86+LyVE9equOxGdhsYlvtZ4godVwKBgQCa
    ndpQkwlvGVOIXdEQWOWfBmDR2q4UwlTDnbAZwk+icMytkIhNsojyIM4NWxfzBfLc
    RG1mxt6EpEPddB6JAW/Ktb7CaAK8lCd5x5sYLiYo5ZgGM9tsDzpS/+EXIHtgUGPT
    SaKYL5g/1AHywLTM5XRXsrQsRmMbmVFsuxNZ3qXzmQKBgQDX9MkY7vDz5n27XtIQ
    S65K5Wsmoqx5T+xhxQ9pRSbHm9t7cAO0We5sMLsAIjt1vKNBSeYLgxtqdEUcylb5
    bZNVj5+qQFzcBh9yl7HtcAe3IkBvkrTAkonHN7gNqXKFUGlFkEFTBJm8IiSeUB9E
    J99XfDatcok6GddO++ZMowAAJQ==
    -----END PRIVATE KEY-----

    2.4 Java加载密钥

    2.4.1 PEM解包

    对于解析密钥文件,第一个重要步骤就是进行PEM解包。这是因为PEM文件是以“-----BEGIN”开头、“-----END”结尾的,而实际的密钥数据是以BASE64编码的形式给放在中间的。
    由于Java没有直接提供对密钥文件的支持,仅提供了 PKCS#8、X.509 等编码的密钥数据的解析类。于是需要我们自己来做PEM解包。

    我观察了网上的PEM解包的源码,发现它们一般是用字符串数组存储“-----BEGIN”的各种模式,然后根据该数组查找字符串来来定位数据的。但该办法并不稳定,容易遇到问题——

    1. BEGIN后面的文本内容不规范。例如有写成“-----BEGIN PUBLIC KEY”开头的,有写成“-----BEGIN RSA PUBLIC KEY”开头的,还有其他各种五花八门的模式。
    2. BEGIN(或END)前后的减号(-)长度不定。不同工具生成的PEM文件中,减号(-)长度是不同的。
    3. 有时中间会有多余的空格等空白字符。

    于是我写了个状态机算法来解析PEM数据。这样便能处理各种意外,提高稳定性。
    另外,该算法还增加自动判断是公钥还是私钥的功能。由于Java函数不允许返回多个值,所以用了一个Map来传递多余的返回值。

        /** 用途文本. 如“BEGIN PUBLIC KEY”中的“PUBLIC KEY”. */
        public final static String PURPOSE_TEXT = "PURPOSE_TEXT";
        /** 用途代码. R私钥, U公钥. */
        public final static String PURPOSE_CODE = "PURPOSE_CODE";
        
        /** PEM解包.
         * 
         * <p>从PEM密钥数据中解包得到纯密钥数据. 即去掉BEGIN/END行,并作BASE64解码. 若没有BEGIN/END, 则直接做BASE64解码.</p>
         * 
         * @param data  源数据.
         * @param otherresult   其他返回值. 支持 PURPOSE_TEXT, PURPOSE_CODE。
         * @return  返回解包后的纯密钥数据.
         */
        public static byte[] PemUnpack(String data, Map<String, String> otherresult) {
            byte[] rt = null;
            final String SIGN_BEGIN = "-BEGIN";
            final String SIGN_END = "-END";
            int datelen = data.length();
            String purposetext = "";
            String purposecode = "";
            if (null!=otherresult) {
                purposetext = otherresult.get(PURPOSE_TEXT);
                purposecode = otherresult.get(PURPOSE_CODE);
                if (null==purposetext) purposetext= "";
                if (null==purposecode) purposecode= "";
            }
            // find begin.
            int bodyPos = 0;    // 主体内容开始的地方.
            int beginPos = data.indexOf(SIGN_BEGIN);
            if (beginPos>=0) {
                // 向后查找换行符后的首个字节.
                boolean isFound = false;
                boolean hadNewline = false; // 已遇到过换行符号.
                boolean hyphenHad = false;  // 已遇到过“-”符号.
                boolean hyphenDone = false; // 已成功获取了右侧“-”的范围.
                int p = beginPos + SIGN_BEGIN.length();
                int hyphenStart = p;    // 右侧“-”的开始位置.
                int hyphenEnd = hyphenStart;    // 右侧“-”的结束位置. 即最后一个“-”字符的位置+1.
                while(p<datelen) {
                    char ch = data.charAt(p);
                    // 查找右侧“-”的范围.
                    if (!hyphenDone) {
                        if (ch=='-') {
                            if (!hyphenHad) {
                                hyphenHad = true;
                                hyphenStart = p;
                                hyphenEnd = hyphenStart;
                            }
                        } else {
                            if (hyphenHad) { // 无需“&& !hyphenDone”,因为外层判断了.
                                hyphenDone = true;
                                hyphenEnd = p;
                            }
                        }
                    }
                    // 向后查找换行符后的首个字节.
                    if (ch=='
    ' || ch=='
    ') {
                        hadNewline = true;
                    } else {
                        if (hadNewline) {
                            // 找到了.
                            bodyPos = p;
                            isFound = true;
                            break;
                        }
                    }
                    // next.
                    ++p;
                }
                // purposetext
                if (hyphenDone && null!=otherresult) {
                    purposetext = data.substring(beginPos + SIGN_BEGIN.length(), hyphenStart).trim();
                    String purposetextUp = purposetext.toUpperCase();
                    if (purposetextUp.indexOf("PRIVATE")>=0) {
                        purposecode = "R";
                    } else if (purposetextUp.indexOf("PUBLIC")>=0) {
                        purposecode = "U";
                    }
                    otherresult.put(PURPOSE_TEXT, purposetext);
                    otherresult.put(PURPOSE_CODE, purposecode);
                }
                // bodyPos.
                if (isFound) {
                    //OK.
                } else if (hyphenDone) {
                    // 以右侧右侧“-”的结束位置作为主体开始.
                    bodyPos = hyphenEnd;
                } else {
                    // 找不到结束位置,只能退出.
                    return rt;
                }
            }
            // find end.
            int bodyEnd = datelen;  // 主体内容的结束位置. 即最后一个字符的位置+1.
            int endPos = data.indexOf(SIGN_END, bodyPos);
            if (endPos>=0) {
                // 向前查找换行符前的首个字节.
                boolean isFound = false;
                boolean hadNewline = false;
                int p = endPos-1;
                while(p >= bodyPos) {
                    char ch = data.charAt(p);
                    if (ch=='
    ' || ch=='
    ') {
                        hadNewline = true;
                    } else {
                        if (hadNewline) {
                            // 找到了.
                            bodyEnd = p+1;
                            break;
                        }
                    }
                    // next.
                    --p;
                }
                if (!isFound) {
                    // 忽略.
                }
            }
            // get body.
            if (bodyPos>=bodyEnd) {
                return rt;
            }
            String body = data.substring(bodyPos, bodyEnd).trim();
            // Decode BASE64.
            rt = Base64.decode(body.getBytes());
            return rt;
        }

    2.4.2 加载公钥

    PemUnpack解出纯密钥数据后,便可分别加载公钥与私钥了。
    由于Java提供了X509EncodedKeySpec,加载公钥是比较简单的。
    下面代码中的strDataKey为PEM文本内容,最后的 key 就是公钥对象。

            Map<String, String> map = new HashMap<String, String>();
            byte[] bytesKey = ZlRsaUtil.PemUnpack(strDataKey, map);
            KeyFactory kf = KeyFactory.getInstance("RSA");
            Key key= null;
            X509EncodedKeySpec spec = new X509EncodedKeySpec(bytesKey);
            key = kf.generatePublic(spec);

    2.4.3 加载私钥

    由于Java提供了PKCS8EncodedKeySpec,加载私钥是比较简单的。
    下面代码中的strDataKey为PEM文本内容,最后的 key就是私钥对象。

            Map<String, String> map = new HashMap<String, String>();
            byte[] bytesKey = ZlRsaUtil.PemUnpack(strDataKey, map);
            KeyFactory kf = KeyFactory.getInstance("RSA");
            Key key= null;
            PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(bytesKey);
            key = kf.generatePrivate(spec);

    2.4.4 判断密钥位数

    密钥位数是一个很重要的数值,很多地方都要用到。可是Java没有简单的提供该属性,而是需要一些步骤来得到,且公钥、私钥得使用不同的类。

    1. 调用 KeyFactory.getKeySpec 方法,传递EncodedKeySpec(公钥为X509EncodedKeySpec,私钥为PKCS8EncodedKeySpec),获取 KeySpec(公钥为RSAPublicKeySpec,私钥为RSAPrivateKeySpec)。
    2. 随后调用 KeySpec对象的 getModulus 方法获取 Modulus(即n)。
    3. 获取 Modulus(即n)的位数,它就是密钥位数。

    范例代码如下——

            KeyFactory kf = KeyFactory.getInstance("RSA");
            Key key= null;
            int keysize;
    
            // 公钥.
            X509EncodedKeySpec spec = new X509EncodedKeySpec(bytesKey);
            key = kf.generatePublic(spec);
            RSAPublicKeySpec keySpec = (RSAPublicKeySpec)kf.getKeySpec(key, RSAPublicKeySpec.class);
            keysize = keySpec.getModulus().bitLength();
    
            // 私钥.
            PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(bytesKey);
            key = kf.generatePrivate(spec);
            RSAPrivateKeySpec keySpec = (RSAPrivateKeySpec)kf.getKeySpec(key, RSAPrivateKeySpec.class);
            keysize = keySpec.getModulus().bitLength();
    

    2.4.4 小结

    刚才讲解了加载密钥过程中的各个关键步骤,现在来将它们组合起来吧。演示一下完整的密钥加载过程。

    参数说明——

    • fileKey: 密钥文件.
            String strDataKey = new String(ZlRsaUtil.fileLoadBytes(fileKey));
            Map<String, String> map = new HashMap<String, String>();
            byte[] bytesKey = ZlRsaUtil.PemUnpack(strDataKey, map);
            String purposecode = map.get(ZlRsaUtil.PURPOSE_CODE);
            //out.println(bytesKey);
            // key.
            KeyFactory kf = KeyFactory.getInstance("RSA");
            Key key= null;
            int keysize;
            if ("R".equals(purposecode)) {
                PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(bytesKey);
                key = kf.generatePrivate(spec);
                RSAPrivateKeySpec keySpec = (RSAPrivateKeySpec)kf.getKeySpec(key, RSAPrivateKeySpec.class);
                keysize = keySpec.getModulus().bitLength();
            } else {
                X509EncodedKeySpec spec = new X509EncodedKeySpec(bytesKey);
                key = kf.generatePublic(spec);
                RSAPublicKeySpec keySpec = (RSAPublicKeySpec)kf.getKeySpec(key, RSAPublicKeySpec.class);
                keysize = keySpec.getModulus().bitLength();
            }
            System.out.println(String.format("keysize: %d", keysize));
            System.out.println(String.format("key.getAlgorithm: %s", key.getAlgorithm()));
            System.out.println(String.format("key.getFormat: %s", key.getFormat()));

    其中的 ZlRsaUtil.fileLoadBytes 是一个加载文件的函数。严格来说,是加载文件的二进制数据。因为PEM文件是纯ASCII的,故可以简单的通过 new String 的方式转为字符串。

        /**
         * RSA .
         */
        public final static String RSA = "RSA";
        
    
        /** 加载文件中的所有字节.
         * 
         * @param filename  文件名.
         * @return  返回文件内容的字节数组.
         * @throws IOException IO异常.
         */
        public static byte[] fileLoadBytes(String filename) throws IOException {
            byte[] rt = null;
            File file = new File(filename);  
            long fileSize = file.length();  
            if (fileSize > Integer.MAX_VALUE) {
                throw new IOException(filename + " file too big...");
            }  
            FileInputStream fi = new FileInputStream(filename);
            try {
                rt = new byte[(int) fileSize];
                int offset = 0;  
                int numRead = 0;  
                while (offset < rt.length  
                        && (numRead = fi.read(rt, offset, rt.length - offset)) >= 0) {  
                    offset += numRead;  
                }  
                // 确保所有数据均被读取  
                if (offset != rt.length) {  
                    throw new IOException("Could not completely read file " + file.getName());  
                }  
            }finally{
                try {
                    fi.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            return rt;
        }
    

    2.5 .Net加载密钥

    2.5.1 PEM解包

    .Net里仅提供对Xml密钥文件的支持,所以我们得自己编写PEM的解包代码。

    同样是因为网上范例代码考虑的不周全,于是我写了个状态机算法来解析PEM数据。能处理各种意外,提高了稳定性。

            /// <summary>
            /// PEM解包.
            /// </summary>
            /// <para>从PEM密钥数据中解包得到纯密钥数据. 即去掉BEGIN/END行,并作BASE64解码. 若没有BEGIN/END, 则直接做BASE64解码.</para>
            /// <param name="data">源数据.</param>
            /// <param name="purposetext">用途文本. 如返回“BEGIN PUBLIC KEY”中的“PUBLIC KEY”.</param>
            /// <param name="purposecode">用途代码. R私钥, U公钥. 若无法识别,便保持原值.</param>
            /// <returns>返回解包后的纯密钥数据.</returns>
            /// <exception cref="System.ArgumentNullException">data is empty, or data body is empty.</exception>
            /// <exception cref="System.FormatException">data body is not BASE64.</exception>
            public static byte[] PemUnpack(String data, ref string purposetext, ref char purposecode) {
                byte[] rt = null;
                const string SIGN_BEGIN = "-BEGIN";
                const string SIGN_END = "-END";
                if (String.IsNullOrEmpty(data)) throw new ArgumentNullException("data", "data is empty!");
                int datelen = data.Length;
                // find begin.
                int bodyPos = 0;    // 主体内容开始的地方.
                int beginPos = data.IndexOf(SIGN_BEGIN, StringComparison.OrdinalIgnoreCase);
                if (beginPos >= 0) {
                    // 向后查找换行符后的首个字节.
                    bool isFound = false;
                    bool hadNewline = false;    // 已遇到过换行符号.
                    bool hyphenHad = false; // 已遇到过“-”符号.
                    bool hyphenDone = false;    // 已成功获取了右侧“-”的范围.
                    int p = beginPos + SIGN_BEGIN.Length;
                    int hyphenStart = p;    // 右侧“-”的开始位置.
                    int hyphenEnd = hyphenStart;    // 右侧“-”的结束位置. 即最后一个“-”字符的位置+1.
                    while (p < datelen) {
                        char ch = data[p];
                        // 查找右侧“-”的范围.
                        if (!hyphenDone) {
                            if (ch == '-') {
                                if (!hyphenHad) {
                                    hyphenHad = true;
                                    hyphenStart = p;
                                    hyphenEnd = hyphenStart;
                                }
                            } else {
                                if (hyphenHad) { // 无需“&& !hyphenDone”,因为外层判断了.
                                    hyphenDone = true;
                                    hyphenEnd = p;
                                }
                            }
                        }
                        // 向后查找换行符后的首个字节.
                        if (ch == '
    ' || ch == '
    ') {
                            hadNewline = true;
                        } else {
                            if (hadNewline) {
                                // 找到了.
                                bodyPos = p;
                                isFound = true;
                                break;
                            }
                        }
                        // next.
                        ++p;
                    }
                    // purposetext
                    if (hyphenDone) {
                        int start = beginPos + SIGN_BEGIN.Length;
                        purposetext = data.Substring(start, hyphenStart - start).Trim();
                        string purposetextUp = purposetext.ToUpperInvariant();
                        if (purposetextUp.IndexOf("PRIVATE") >= 0) {
                            purposecode = 'R';
                        } else if (purposetextUp.IndexOf("PUBLIC") >= 0) {
                            purposecode = 'U';
                        }
                    }
                    // bodyPos.
                    if (isFound) {
                        //OK.
                    } else if (hyphenDone) {
                        // 以右侧右侧“-”的结束位置作为主体开始.
                        bodyPos = hyphenEnd;
                    } else {
                        // 找不到结束位置,只能退出.
                        return rt;
                    }
                }
                // find end.
                int bodyEnd = datelen;  // 主体内容的结束位置. 即最后一个字符的位置+1.
                int endPos = data.IndexOf(SIGN_END, bodyPos);
                if (endPos >= 0) {
                    // 向前查找换行符前的首个字节.
                    bool isFound = false;
                    bool hadNewline = false;
                    int p = endPos - 1;
                    while (p >= bodyPos) {
                        char ch = data[p];
                        if (ch == '
    ' || ch == '
    ') {
                            hadNewline = true;
                        } else {
                            if (hadNewline) {
                                // 找到了.
                                bodyEnd = p + 1;
                                break;
                            }
                        }
                        // next.
                        --p;
                    }
                    if (!isFound) {
                        // 忽略.
                    }
                }
                // get body.
                if (bodyPos >= bodyEnd) {
                    return rt;
                }
                string body = data.Substring(bodyPos, bodyEnd - bodyPos).Trim();
                // Decode BASE64.
                if (String.IsNullOrEmpty(body)) throw new ArgumentNullException("data", "data body is empty!");
                rt = Convert.FromBase64String(body);
                return rt;
            }

    2.5.2 加载公钥

    由于.Net平台没有提供 X.509 的解码类,故需要自己编写。
    我参考网上代码,写了一个公钥的解码函数。

            /// <summary>
            /// 根据PEM纯密钥数据,获取公钥的RSA加解密对象.
            /// </summary>
            /// <param name="pubcdata">公钥数据</param>
            /// <returns>返回公钥的RSA加解密对象.</returns>
            public static RSACryptoServiceProvider PemDecodePublicKey(byte[] pubcdata) {
                byte[] SeqOID = { 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01 };
    
                MemoryStream ms = new MemoryStream(pubcdata);
                BinaryReader reader = new BinaryReader(ms);
    
                if (reader.ReadByte() == 0x30)
                    ReadASNLength(reader); //skip the size
                else
                    return null;
    
                int identifierSize = 0; //total length of Object Identifier section
                if (reader.ReadByte() == 0x30)
                    identifierSize = ReadASNLength(reader);
                else
                    return null;
    
                if (reader.ReadByte() == 0x06) { //is the next element an object identifier?
                    int oidLength = ReadASNLength(reader);
                    byte[] oidBytes = new byte[oidLength];
                    reader.Read(oidBytes, 0, oidBytes.Length);
                    if (!SequenceEqualByte(oidBytes, SeqOID)) //is the object identifier rsaEncryption PKCS#1?
                        return null;
    
                    int remainingBytes = identifierSize - 2 - oidBytes.Length;
                    reader.ReadBytes(remainingBytes);
                }
    
                if (reader.ReadByte() == 0x03) { //is the next element a bit string?
    
                    ReadASNLength(reader); //skip the size
                    reader.ReadByte(); //skip unused bits indicator
                    if (reader.ReadByte() == 0x30) {
                        ReadASNLength(reader); //skip the size
                        if (reader.ReadByte() == 0x02) { //is it an integer?
                            int modulusSize = ReadASNLength(reader);
                            byte[] modulus = new byte[modulusSize];
                            reader.Read(modulus, 0, modulus.Length);
                            if (modulus[0] == 0x00) {//strip off the first byte if it's 0
                                byte[] tempModulus = new byte[modulus.Length - 1];
                                Array.Copy(modulus, 1, tempModulus, 0, modulus.Length - 1);
                                modulus = tempModulus;
                            }
    
                            if (reader.ReadByte() == 0x02) { //is it an integer?
                                int exponentSize = ReadASNLength(reader);
                                byte[] exponent = new byte[exponentSize];
                                reader.Read(exponent, 0, exponent.Length);
    
                                RSACryptoServiceProvider RSA = new RSACryptoServiceProvider();
                                RSAParameters RSAKeyInfo = new RSAParameters();
                                RSAKeyInfo.Modulus = modulus;
                                RSAKeyInfo.Exponent = exponent;
                                RSA.ImportParameters(RSAKeyInfo);
                                return RSA;
                            }
                        }
                    }
                }
                return null;
            }
    
            /// <summary>
            /// Read ASN Length.
            /// </summary>
            /// <param name="reader">reader</param>
            /// <returns>Return ASN Length.</returns>
            private static int ReadASNLength(BinaryReader reader) {
                //Note: this method only reads lengths up to 4 bytes long as
                //this is satisfactory for the majority of situations.
                int length = reader.ReadByte();
                if ((length & 0x00000080) == 0x00000080) { //is the length greater than 1 byte
                    int count = length & 0x0000000f;
                    byte[] lengthBytes = new byte[4];
                    reader.Read(lengthBytes, 4 - count, count);
                    Array.Reverse(lengthBytes); //
                    length = BitConverter.ToInt32(lengthBytes, 0);
                }
                return length;
            }
    
            /// <summary>
            /// 字节数组内容是否相等.
            /// </summary>
            /// <param name="a">数组a</param>
            /// <param name="b">数组b</param>
            /// <returns>返回是否相等.</returns>
            private static bool SequenceEqualByte(byte[] a, byte[] b) {
                var len1 = a.Length;
                var len2 = b.Length;
                if (len1 != len2) {
                    return false;
                }
                for (var i = 0; i < len1; i++) {
                    if (a[i] != b[i])
                        return false;
                }
                return true;
            }

    2.5.3 加载私钥

    .Net平台也没有提供 PKCS#8 的解码类,也需要自己编写。
    我最初测试了很多网上的私钥解码代码,均不能正常工作。直到后来查了 OpenSSL 的源码,才找到了解决办法。发现这是因为PKCS#8的私钥数据,其实还嵌套了一层X.509编码,故得按顺序分别进行解码。

            /// <summary>
            /// 解码 PKCS#8 编码的私钥,获取私钥的RSA加解密对象.
            /// </summary>
            /// <param name="privkey">私钥数据。</param>
            /// <returns>返回私钥的RSA加解密对象. 失败时返回null.</returns>
            public static RSACryptoServiceProvider PemDecodePkcs8PrivateKey(byte[] pkcs8) {
                // encoded OID sequence for  PKCS #1 rsaEncryption szOID_RSA_RSA = "1.2.840.113549.1.1.1"
                // this byte[] includes the sequence byte and terminal encoded null 
                byte[] SeqOID = { 0x30, 0x0D, 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01, 0x05, 0x00 };
                byte[] seq = new byte[15];
                // ---------  Set up stream to read the asn.1 encoded SubjectPublicKeyInfo blob  ------
                MemoryStream mem = new MemoryStream(pkcs8);
                int lenstream = (int)mem.Length;
                BinaryReader binr = new BinaryReader(mem);    //wrap Memory Stream with BinaryReader for easy reading
                byte bt = 0;
                ushort twobytes = 0;
    
                try {
    
                    twobytes = binr.ReadUInt16();
                    if (twobytes == 0x8130) //data read as little endian order (actual data order for Sequence is 30 81)
                        binr.ReadByte();    //advance 1 byte
                    else if (twobytes == 0x8230)
                        binr.ReadInt16();   //advance 2 bytes
                    else
                        return null;
    
    
                    bt = binr.ReadByte();
                    if (bt != 0x02)
                        return null;
    
                    twobytes = binr.ReadUInt16();
    
                    if (twobytes != 0x0001)
                        return null;
    
                    seq = binr.ReadBytes(15);       //read the Sequence OID
                    if (!SequenceEqualByte(seq, SeqOID))    //make sure Sequence for OID is correct
                        return null;
    
                    bt = binr.ReadByte();
                    if (bt != 0x04) //expect an Octet string 
                        return null;
    
                    bt = binr.ReadByte();       //read next byte, or next 2 bytes is  0x81 or 0x82; otherwise bt is the byte count
                    if (bt == 0x81)
                        binr.ReadByte();
                    else
                        if (bt == 0x82)
                            binr.ReadUInt16();
                    //------ at this stage, the remaining sequence should be the RSA private key
    
                    byte[] rsaprivkey = binr.ReadBytes((int)(lenstream - mem.Position));
                    RSACryptoServiceProvider rsacsp = PemDecodeX509PrivateKey(rsaprivkey);
                    return rsacsp;
                } finally { binr.Close(); }
    
            }
    
            /// <summary>
            /// 解码 X.509 编码的私钥,获取私钥的RSA加解密对象.
            /// </summary>
            /// <param name="privkey">私钥数据。</param>
            /// <returns>返回私钥的RSA加解密对象. 失败时返回null.</returns>
            public static RSACryptoServiceProvider PemDecodeX509PrivateKey(byte[] privkey)  
            {  
                byte[] MODULUS, E, D, P, Q, DP, DQ, IQ;  
                  
                // --------- Set up stream to decode the asn.1 encoded RSA private key ------    
                MemoryStream mem = new MemoryStream(privkey);  
                BinaryReader binr = new BinaryReader(mem);  //wrap Memory Stream with BinaryReader for easy reading    
                byte bt = 0;  
                ushort twobytes = 0;  
                int elems = 0;  
                try  
                {  
                    twobytes = binr.ReadUInt16();  
                    if (twobytes == 0x8130) //data read as little endian order (actual data order for Sequence is 30 81)    
                        binr.ReadByte();    //advance 1 byte    
                    else if (twobytes == 0x8230)  
                        binr.ReadInt16();    //advance 2 bytes    
                    else  
                        return null;  
      
                    twobytes = binr.ReadUInt16();  
                    if (twobytes != 0x0102) //version number    
                        return null;  
                    bt = binr.ReadByte();  
                    if (bt != 0x00)  
                        return null;  
      
      
                    //------ all private key components are Integer sequences ----    
                    elems = GetIntegerSize(binr);  
                    MODULUS = binr.ReadBytes(elems);  
      
                    elems = GetIntegerSize(binr);  
                    E = binr.ReadBytes(elems);  
      
                    elems = GetIntegerSize(binr);  
                    D = binr.ReadBytes(elems);  
      
                    elems = GetIntegerSize(binr);  
                    P = binr.ReadBytes(elems);  
      
                    elems = GetIntegerSize(binr);  
                    Q = binr.ReadBytes(elems);  
      
                    elems = GetIntegerSize(binr);  
                    DP = binr.ReadBytes(elems);  
      
                    elems = GetIntegerSize(binr);  
                    DQ = binr.ReadBytes(elems);  
      
                    elems = GetIntegerSize(binr);  
                    IQ = binr.ReadBytes(elems);  
      
      
                    // ------- create RSACryptoServiceProvider instance and initialize with public key -----    
                    CspParameters CspParameters = new CspParameters();  
                    CspParameters.Flags = CspProviderFlags.UseMachineKeyStore;  
                    RSACryptoServiceProvider RSA = new RSACryptoServiceProvider(1024, CspParameters);  
                    RSAParameters RSAparams = new RSAParameters();  
                    RSAparams.Modulus = MODULUS;  
                    RSAparams.Exponent = E;  
                    RSAparams.D = D;  
                    RSAparams.P = P;  
                    RSAparams.Q = Q;  
                    RSAparams.DP = DP;  
                    RSAparams.DQ = DQ;  
                    RSAparams.InverseQ = IQ;  
                    RSA.ImportParameters(RSAparams);  
                    return RSA;  
                }  
                finally  
                {  
                    binr.Close();  
                }  
            }  
      
            /// <summary>
            /// 取得整数大小.
            /// </summary>
            /// <param name="binr">BinaryReader</param>
            /// <returns>返回整数大小.</returns>
            private static int GetIntegerSize(BinaryReader binr)  
            {  
                byte bt = 0;  
                byte lowbyte = 0x00;  
                byte highbyte = 0x00;  
                int count = 0;  
                bt = binr.ReadByte();  
                if (bt != 0x02)    //expect integer    
                    return 0;  
                bt = binr.ReadByte();  
      
                if (bt == 0x81)  
                    count = binr.ReadByte();    // data size in next byte    
                else  
                    if (bt == 0x82)  
                    {  
                        highbyte = binr.ReadByte(); // data size in next 2 bytes    
                        lowbyte = binr.ReadByte();  
                        byte[] modint = { lowbyte, highbyte, 0x00, 0x00 };  
                        count = BitConverter.ToInt32(modint, 0);  
                    }  
                    else  
                    {  
                        count = bt;    // we already have the data size    
                    }  
      
                while (binr.ReadByte() == 0x00)  
                {  //remove high order zeros in data    
                    count -= 1;  
                }  
                binr.BaseStream.Seek(-1, SeekOrigin.Current);      //last ReadByte wasn't a removed zero, so back up a byte    
                return count;  
            }

    2.5.4 判断密钥位数

    在 .Net中,访问 RSACryptoServiceProvider.KeySize 便可得到密钥位数,非常简单。

    int keysize = rsa.KeySize;

    2.5.4 小结

    刚才讲解了加载密钥过程中的各个关键步骤,现在来将它们组合起来吧。演示一下完整的密钥加载过程。

    参数说明——

    • fileKey: 密钥文件.
                string strDataKey = File.ReadAllText(fileKey);
                string purposetext = null;
                char purposecode = '';
                byte[] bytesKey = ZlRsaUtil.PemUnpack(strDataKey, ref purposetext, ref purposecode);
                //export.WriteLine(bytesKey);
                // key.
                RSACryptoServiceProvider rsa;
                if ('R' == purposecode) {
                    rsa = ZlRsaUtil.PemDecodePkcs8PrivateKey(bytesKey); // try 
                    if (null == rsa) {
                        rsa = ZlRsaUtil.PemDecodeX509PrivateKey(bytesKey);
                    }
                } else {    // 公钥或无法判断时, 均当成公钥处理.
                    rsa = ZlRsaUtil.PemDecodePublicKey(bytesKey);
                }
                if (null == rsa) {
                    export.WriteLine("Key decode fail!");
                    return;
                }
                export.WriteLine(string.Format("KeyExchangeAlgorithm: {0}", rsa.KeyExchangeAlgorithm));
                export.WriteLine(string.Format("KeySize: {0}", rsa.KeySize));

    三、加解密

    3.1 确立加密模式与填充方式

    虽然都是RSA算法,但是若加密模式与填充方式不同的话,会导致加密结果不匹配。所以需要确定好 .Net与Java 均支持的方式。

    加密模式一般有 ECB/CBC/CFB/OFB 这四种。对于RSA来说,ECB最简单但安全性比较薄弱,而CBC等模式就很复杂且还需考虑IV(initialization vector,初始化向量)的管理。所以一般情况下可以用 ECB 模式,.Net与Java均支持它,且ECB是.Net的默认模式。

    由于加密算法都是按块来处理的,故理论上只有当明文长度正好是块长度的倍数时才能进行加解密。但那样太麻烦了,故有了填充方式的概念,即在明文后面填充一些数据,使其长度正好是块的倍数。填充方式还有2个作用,一是能标记原始数据长度使解码时自动去掉末尾的填充数据,二是能提高安全性。
    .Net的RSA算法默认是使用PKCS#1填充方式的,故Java中可选择 PKCS1Padding 填充方式。

    现在算法已经确定了,Java中可定义这些常数。

        /**
         * RSA .
         */
        public final static String RSA = "RSA";
        
        /**
         * 具体的 RSA 算法.
         */
        public final static String RSA_ALGORITHM = "RSA/ECB/PKCS1Padding";

    3.2 分段加密

    对于.Net、Java自带的RSA库来说,填充方式只是解决了“明文长度小于块尺寸”的问题。而当明文长度大于块尺寸时,便会抛出异常,常见的异常信息有——

    // .Net
    不正确的长度
    
    // Java
    javax.crypto.IllegalBlockSizeException: Data must not be longer than 117 bytes
    javax.crypto.IllegalBlockSizeException: Data must not be longer than 245 bytes

    此时便需要对数据进行分段加密。

    3.2.1 块尺寸的计算

    密文的块尺寸是很容易计算的,即“密钥位数/8”。即把二进制长度转为字节长度。
    而明文的块尺寸的计算就稍微麻烦了一点,与填充方式有关。因目前使用了PKCS#1填充方式,该方式需占用11个字节。于是块尺寸为“密钥位数/8 - 11”。

    例如密钥长度为2048位时——

    • 密文的块尺寸 = 密钥位数/8 = 2048/8 = 256
    • 明文的块尺寸 = 密钥位数/8 - 11 = 2048/8 - 11 = 256 - 11 = 245

    即——

    • 加密时:明文的块为245字节,加密后输出的密文块为256字节。
    • 解密时:密文的块为256字节,解密后输出的明文块为245字节。

    3.3 Java加解密

    3.3.1 加密

        /** RSA加密. 当数据较长时, 能自动分段加密.
         * 
         * @param cipher    加解密服务提供者. 需是已初始化的, 即已经调了init的.
         * @param keysize   密钥长度. 例如2048位的RSA,传2048 .
         * @param data  欲加密的数据.
         * @return  返回加密后的数据.
         * @throws BadPaddingException  On Cipher.doFinal
         * @throws IllegalBlockSizeException    On Cipher.doFinal
         */
        public static byte[] encrypt(Cipher cipher, int keysize, byte[] data) throws IllegalBlockSizeException, BadPaddingException {
            byte[] cipherBytes = null;
            int blockSize = keysize/8 - 11; // RSA加密时支持的最大字节数:证书位数/8 -11(比如:2048位的证书,支持的最大加密字节数:2048/8 - 11 = 245).
            if (data.length <= blockSize) {
                // 整个加密.
                cipherBytes = cipher.doFinal(data);
            } else {
                // 分段加密.
                int inputLen = data.length;
                ByteArrayOutputStream ostm = new ByteArrayOutputStream();
                try {
                    for(int offSet = 0; inputLen - offSet > 0; ) {
                        int len = inputLen - offSet;
                        if (len>blockSize) len=blockSize;
                        byte[] cache = cipher.doFinal(data, offSet, len);
                        ostm.write(cache, 0, cache.length);
                        // next.
                        offSet += len;
                    }
                    cipherBytes = ostm.toByteArray();
                }finally {
                    try {
                        ostm.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }           
                }
            }
            return cipherBytes;
        }

    3.3.2 解密

        /** RSA解密. 当数据较长时, 能自动分段解密.
         * 
         * @param cipher    加解密服务提供者. 需是已初始化的, 即已经调了init的.
         * @param keysize   密钥长度. 例如2048位的RSA,传2048 .
         * @param data  欲解密的数据.
         * @return  返回解密后的数据.
         * @throws BadPaddingException  On Cipher.doFinal
         * @throws IllegalBlockSizeException    On Cipher.doFinal
         */
        public static byte[] decrypt(Cipher cipher, int keysize, byte[] data) throws IllegalBlockSizeException, BadPaddingException {
            byte[] cipherBytes = null;
            int blockSize = keysize/8;
            if (data.length <= blockSize) {
                // 整个加密.
                cipherBytes = cipher.doFinal(data);
            } else {
                // 分段加密.
                int inputLen = data.length;
                ByteArrayOutputStream ostm = new ByteArrayOutputStream();
                try {
                    for(int offSet = 0; inputLen - offSet > 0; ) {
                        int len = inputLen - offSet;
                        if (len>blockSize) len=blockSize;
                        byte[] cache = cipher.doFinal(data, offSet, len);
                        ostm.write(cache, 0, cache.length);
                        // next.
                        offSet += len;
                    }
                    cipherBytes = ostm.toByteArray();
                }finally {
                    try {
                        ostm.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }           
                }
            }
            return cipherBytes;
        }

    3.4 .Net加解密

    3.3.1 加密

            /// <summary>
            /// RSA加密. 当数据较长时, 能自动分段加密.
            /// </summary>
            /// <param name="rsa">加解密服务提供者. 需是已初始化的.</param>
            /// <param name="data">欲加密的数据.</param>
            /// <returns>返回加密后的数据.</returns>
            /// <exception cref="System.Security.Cryptography.CryptographicException">On RSACryptoServiceProvider.Encrypt .</exception>
            public static byte[] Encrypt(RSACryptoServiceProvider rsa, byte[] data) {
                byte[] cipherBytes = null;
                int keysize = rsa.KeySize;
                int blockSize = keysize / 8 - 11;   // RSA加密时支持的最大字节数:证书位数/8 -11(比如:2048位的证书,支持的最大加密字节数:2048/8 - 11 = 245).
                if (data.Length <= blockSize) {
                    // 整个加密.
                    cipherBytes = rsa.Encrypt(data, false);
                } else {
                    // 分段加密.
                    int inputLen = data.Length;
                    using (MemoryStream ostm = new MemoryStream()) {
                        for (int offSet = 0; inputLen - offSet > 0; ) {
                            int len = inputLen - offSet;
                            if (len > blockSize) len = blockSize;
                            byte[] tmp = new byte[len];
                            Array.Copy(data, offSet, tmp, 0, len);
                            byte[] cache = rsa.Encrypt(tmp, false);
                            ostm.Write(cache, 0, cache.Length);
                            // next.
                            offSet += len;
                        }
                        ostm.Position = 0;
                        cipherBytes = ostm.ToArray();
                    }
                }
                return cipherBytes;
            }

    3.3.2 解密

            /// <summary>
            /// RSA解密. 当数据较长时, 能自动分段解密.
            /// </summary>
            /// <param name="rsa">加解密服务提供者. 需是已初始化的.</param>
            /// <param name="data">欲解密的数据.</param>
            /// <returns>返回解密后的数据.</returns>
            /// <exception cref="System.Security.Cryptography.CryptographicException">On RSACryptoServiceProvider.Encrypt .</exception>
            public static byte[] Decrypt(RSACryptoServiceProvider rsa, byte[] data) {
                byte[] cipherBytes = null;
                int keysize = rsa.KeySize;
                int blockSize = keysize / 8;
                if (data.Length <= blockSize) {
                    // 整个解密.
                    cipherBytes = rsa.Decrypt(data, false);
                } else {
                    // 分段解密.
                    int inputLen = data.Length;
                    using (MemoryStream ostm = new MemoryStream()) {
                        for (int offSet = 0; inputLen - offSet > 0; ) {
                            int len = inputLen - offSet;
                            if (len > blockSize) len = blockSize;
                            byte[] tmp = new byte[len];
                            Array.Copy(data, offSet, tmp, 0, len);
                            byte[] cache = rsa.Decrypt(tmp, false);
                            ostm.Write(cache, 0, cache.Length);
                            // next.
                            offSet += len;
                        }
                        ostm.Position = 0;
                        cipherBytes = ostm.ToArray();
                    }
                }
                return cipherBytes;
            }

    四、测试验证

    4.1 编程测试

    为了验证.Net、Java的加解密代码是否吻合,最好是写一个测试程序进行验证。然后便可分别测试——

    • Java 端加密生成密文文件,随后 Java 端读取密文文件做解密。
    • .Net 端加密生成密文文件,随后 .Net 端读取密文文件做解密。
    • Java 端加密生成密文文件,随后 .Net 端读取密文文件做解密。
    • .Net 端加密生成密文文件,随后 Java 端读取密文文件做解密。

    这4种测试都通过后,便表示加解密没问题。可稳定的运行在.Net、Java通讯的场景下。

    4.1.1 命令行设计

    为了方便多次重复测试,于是将该程序设计为命令行程序。这样便能灵活的做各种测试。

    该程序命名为 rsapemdemo。用法为 rsapemdemo [options] srcfile

    命令的范例——

    # 使用公钥进行加密
    rsapemdemo -e -l publickey.pem -o dstfile srcfile
    
    # 使用私钥进行解密
    rsapemdemo -d -l privatekey.pem -o dstfile srcfile

    参数说明——

    -e:RSA加密,并进行BASE64编码。因加密后得到的二进制数据不易查看、复制,故再做了一次BASE64编码。
    -d:BASE64解码,并进行RSA解密。
    -l [keyfile]:加载密钥文件。
    -o [outfile]:指定输出文件。
    srcfile:源文件名。

    实际测试时所使用的命令行——

    rsapemdemo -e -l "E:
    sapemdemodatapublic1.pem" -o "E:
    sapemdemodatasrc1_pub.log" "E:
    sapemdemodatasrc1.txt"
    rsapemdemo -e -l "E:
    sapemdemodataprivate1.pem" -o "E:
    sapemdemodatasrc1_pri.log" "E:
    sapemdemodatasrc1.txt"
    
    rsapemdemo -d -l "E:
    sapemdemodatapublic1.pem" -o "E:
    sapemdemodatasrc1_pri_d.log" "E:
    sapemdemodatasrc1_pri.log"
    rsapemdemo -d -l "E:
    sapemdemodataprivate1.pem" -o "E:
    sapemdemodatasrc1_pub_d.log" "E:
    sapemdemodatasrc1_pub.log"

    4.1.2 Java的测试办法

    在Eclipse中打开项目。

    双击打开含有main函数的文件(RsaPemDemo.java),然后在源码区域右击鼠标,在弹出菜单中选择“Debug As -> Debug Configurations”。

    “Debug Configurations”对话框打开后,切换到“Arguments”页,在“Program arguments”文本框中输入命令行参数(不用输入程序名,只需输入后面的参数)。

    随后便可点击“Debug”按钮进行调试了。

    4.1.3 .Net的测试办法

    在VS中打开项目。

    点击菜单栏的“项目->属性”。

    属性对话框打开后,切换到“调试”页,在“命令行参数”文本框中输入命令行参数(不用输入程序名,只需输入后面的参数)。

    随后便可按F5调试了。

    测试后发现——

    • .NET 的RSA,仅支持公钥加密、私钥解密。若用私钥加密,则仍是返回公钥加密结果。若用公钥解密,会出现 System.Security.Cryptography.CryptographicException: 不正确的项。 异常.

    4.2 在线测试

    除了自己编码测试外,还可以使用RSA在线工具进行对比测试。检测我们测试程序所生成的密文,是否能被在线工具解密,或者让在线工具生成密文由我们程序进行解密。

    例如可利用这个网站进行测试——

    # 在线RSA公钥加密解密、RSA public key encryption and decryption
    http://tool.chacuo.net/cryptrsapubkey
    
    # 在线RSA私钥加密解密、RSA private key encryption and decryption
    http://tool.chacuo.net/cryptrsaprikey

    附录、测试程序的主体源码

    附录.1 Java版

    package rsapemdemo;
    
    import java.io.IOException;
    import java.io.PrintStream;
    import java.security.InvalidKeyException;
    import java.security.Key;
    import java.security.KeyFactory;
    import java.security.NoSuchAlgorithmException;
    import java.security.spec.InvalidKeySpecException;
    import java.security.spec.PKCS8EncodedKeySpec;
    import java.security.spec.RSAPrivateKeySpec;
    import java.security.spec.RSAPublicKeySpec;
    import java.security.spec.X509EncodedKeySpec;
    import java.util.HashMap;
    import java.util.Map;
    
    import javax.crypto.BadPaddingException;
    import javax.crypto.Cipher;
    import javax.crypto.IllegalBlockSizeException;
    import javax.crypto.NoSuchPaddingException;
    
    /** Java/.NET RSA demo, use pem key file (Java/.NET的RSA加解密演示项目,使用pem格式的密钥文件).
     * 
     * @author zyl910
     * @since 2017-10-27
     *
     */
    public class RsaPemDemo {
        /** 帮助文本. */
        private static final String helpText = "Usage: rsapemdemo [options] srcfile
    
    For example:
    
        # encode by public key
        rsapemdemo -e -l publickey.pem -o dstfile srcfile
    
        # decode by private key
        rsapemdemo -d -l privatekey.pem -o dstfile srcfile
    
    The options:
    
        -e        RSA encryption and BASE64 encode.
        -d        BASE64 decode and RSA decryption.
        -l [keyfile]  Load key file.
        -o [outfile]  out file.
    ";
        
        /** 是否为空.
         * 
         * @param str   字符串.
         * @return  如果字符串为null或空串,则返回true,否则返回false.
         */
        private static boolean isEmpty(String str) {
            return null==str || str.length()<=0;
        }
    
        /** 运行.
         * 
         * @param export    文本打印流.
         * @param args  参数.
         * @return  程序退出码.
         */
        public void run(PrintStream export, String[] args) {
            boolean showhelp = true;
            // args
            String state = null;    // 状态.
            boolean isEncode = false;
            boolean isDecode = false;
            String fileKey = null;
            String fileOut = null;
            String fileSrc = null;
            int keysize = 0;    // RSA密钥位数. 0表示自动获取.
            for(String s: args) {
                if ("-e".equalsIgnoreCase(s)) {
                    isEncode = true;
                } else if ("-d".equalsIgnoreCase(s)) {
                    isDecode = true;
                } else if ("-l".equalsIgnoreCase(s)) {
                    state = "l";
                } else if ("-o".equalsIgnoreCase(s)) {
                    state = "o";
                } else {
                    if ("l".equalsIgnoreCase(state)) {
                        fileKey = s;
                        state = null;
                    } else if ("o".equalsIgnoreCase(state)) {
                        fileOut = s;
                        state = null;
                    } else {
                        fileSrc = s;
                    }
                }
            }
            try{
                if (isEmpty(fileKey)) {
                    export.println("No key file! Command need add `-l [keyfile]`.");
                } else if (isEmpty(fileOut)) {
                    export.println("No out file! Command need add `-o [outfile]`.");
                } else if (isEmpty(fileSrc)) {
                    export.println("No src file! Command need add `[srcfile]`.");
                } else if (isEncode!=false && isDecode!=false) {
                    export.println("No set Encode/Encode! Command need add `-e`/`-d`.");
                } else if (isEncode) {
                    showhelp = false;
                    doEncode(export, keysize, fileKey, fileOut, fileSrc, null);
                } else if (isDecode) {
                    showhelp = false;
                    doDecode(export, keysize, fileKey, fileOut, fileSrc, null);
                }
            } catch (Exception e) {
                e.printStackTrace(export);
            }
            // do.
            if (showhelp) {
                export.println(helpText);
            }
        }
    
        /** 进行加密.
         * 
         * @param export    文本打印流.
         * @param keysize   密钥位数. 为0表示自动获取.
         * @param fileKey   密钥文件.
         * @param fileOut   输出文件.
         * @param fileSrc   源文件.
         * @param exargs    扩展参数.
         * @throws IOException 
         * @throws NoSuchPaddingException 
         * @throws NoSuchAlgorithmException 
         * @throws InvalidKeySpecException 
         * @throws InvalidKeyException 
         * @throws BadPaddingException 
         * @throws IllegalBlockSizeException 
         */
        private void doEncode(PrintStream export, int keysize, String fileKey, String fileOut,
                String fileSrc, Map<String, ?> exargs) throws IOException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeySpecException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
            byte[] bytesSrc = ZlRsaUtil.fileLoadBytes(fileSrc);
            String strDataKey = new String(ZlRsaUtil.fileLoadBytes(fileKey));
            Map<String, String> map = new HashMap<String, String>();
            byte[] bytesKey = ZlRsaUtil.PemUnpack(strDataKey, map);
            String purposecode = map.get(ZlRsaUtil.PURPOSE_CODE);
            //out.println(bytesKey);
            // key.
            KeyFactory kf = KeyFactory.getInstance(ZlRsaUtil.RSA);
            Key key= null;
            //boolean isPrivate = false;
            if ("R".equals(purposecode)) {
                //isPrivate = true;
                PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(bytesKey);
                key = kf.generatePrivate(spec);
                RSAPrivateKeySpec keySpec = (RSAPrivateKeySpec)kf.getKeySpec(key, RSAPrivateKeySpec.class);
                keysize = keySpec.getModulus().bitLength();
            } else {
                X509EncodedKeySpec spec = new X509EncodedKeySpec(bytesKey);
                key = kf.generatePublic(spec);
                RSAPublicKeySpec keySpec = (RSAPublicKeySpec)kf.getKeySpec(key, RSAPublicKeySpec.class);
                keysize = keySpec.getModulus().bitLength();
            }
            export.println(String.format("keysize: %d", keysize));
            export.println(String.format("key.getAlgorithm: %s", key.getAlgorithm()));
            export.println(String.format("key.getFormat: %s", key.getFormat()));
            // encrypt.
            Cipher cipher = Cipher.getInstance(ZlRsaUtil.RSA_ALGORITHM);
            cipher.init(Cipher.ENCRYPT_MODE, key);
            byte[] cipherBytes = ZlRsaUtil.encrypt(cipher, keysize, bytesSrc);
            byte[] cipherBase64 = Base64.encode(cipherBytes);
            ZlRsaUtil.fileSaveBytes(fileOut, cipherBase64, 0, cipherBase64.length);
            export.println(String.format("%s save done.", fileOut));
        }
    
        /** 进行解密.
         * 
         * @param export    文本打印流.
         * @param keysize   密钥位数. 为0表示自动获取.
         * @param fileKey   密钥文件.
         * @param fileOut   输出文件.
         * @param fileSrc   源文件.
         * @param exargs    扩展参数.
         * @throws IOException 
         * @throws NoSuchAlgorithmException 
         * @throws InvalidKeySpecException 
         * @throws NoSuchPaddingException 
         * @throws InvalidKeyException 
         * @throws BadPaddingException 
         * @throws IllegalBlockSizeException 
         */
        private void doDecode(PrintStream export, int keysize, String fileKey, String fileOut,
                String fileSrc, Object exargs) throws IOException, NoSuchAlgorithmException, InvalidKeySpecException, NoSuchPaddingException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
            byte[] bytesB64Src = ZlRsaUtil.fileLoadBytes(fileSrc);
            byte[] bytesSrc = Base64.decode(bytesB64Src);
            if (null==bytesSrc || bytesSrc.length<=0) {
                export.println(String.format("Error: %s is not BASE64!", fileSrc));
                return;
            }
            String strDataKey = new String(ZlRsaUtil.fileLoadBytes(fileKey));
            Map<String, String> map = new HashMap<String, String>();
            byte[] bytesKey = ZlRsaUtil.PemUnpack(strDataKey, map);
            String purposecode = map.get(ZlRsaUtil.PURPOSE_CODE);
            //out.println(bytesKey);
            // key.
            KeyFactory kf = KeyFactory.getInstance(ZlRsaUtil.RSA);
            Key key= null;
            //boolean isPrivate = false;
            if ("R".equals(purposecode)) {
                //isPrivate = true;
                PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(bytesKey);
                key = kf.generatePrivate(spec);
                RSAPrivateKeySpec keySpec = (RSAPrivateKeySpec)kf.getKeySpec(key, RSAPrivateKeySpec.class);
                keysize = keySpec.getModulus().bitLength();
            } else {    // 公钥或无法判断时, 均当成公钥处理.
                X509EncodedKeySpec spec = new X509EncodedKeySpec(bytesKey);
                key = kf.generatePublic(spec);
                RSAPublicKeySpec keySpec = (RSAPublicKeySpec)kf.getKeySpec(key, RSAPublicKeySpec.class);
                keysize = keySpec.getModulus().bitLength();
            }
            export.println(String.format("key.getAlgorithm: %s", key.getAlgorithm()));
            export.println(String.format("key.getFormat: %s", key.getFormat()));
            // decrypt.
            Cipher cipher = Cipher.getInstance(ZlRsaUtil.RSA_ALGORITHM);
            cipher.init(Cipher.DECRYPT_MODE, key);
            byte[] cipherBytes = ZlRsaUtil.decrypt(cipher, keysize, bytesSrc);
            ZlRsaUtil.fileSaveBytes(fileOut, cipherBytes, 0, cipherBytes.length);
            export.println(String.format("%s save done.", fileOut));
        }
    
        public static void main(String[] args) {
            RsaPemDemo demo = new RsaPemDemo();
            demo.run(System.out, args);
        }
    }

    附录.2 .Net版

    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Text;
    using System.Collections;
    using System.Security.Cryptography.X509Certificates;
    using System.Security.Cryptography;
    
    namespace RsaPemDemo {
        /// <summary>
        /// Java/.NET RSA demo, use pem key file (Java/.NET的RSA加解密演示项目,使用pem格式的密钥文件).
        /// </summary>
        class Program {
            /// <summary>
            /// 帮助文本.
            /// </summary>
            private const string helpText = "Usage: RsaPemDemo [options] srcfile
    
    For example:
    
        # encode by public key
        rsapemdemo -e -l publickey.pem -o dstfile srcfile
    
        # decode by private key
        rsapemdemo -d -l privatekey.pem -o dstfile srcfile
    
    The options:
    
        -e        RSA encryption and BASE64 encode.
        -d        BASE64 decode and RSA decryption.
        -l [keyfile]  Load key file.
        -o [outfile]  out file.
    ";
    
            /// <summary>
            /// 运行.
            /// </summary>
            /// <param name="export">文本打印流.</param>
            /// <param name="args">参数.</param>
            public void run(TextWriter export, string[] args) {
                bool showhelp = true;
                // args
                string state = null;    // 状态.
                bool isEncode = false;
                bool isDecode = false;
                string fileKey = null;
                string fileOut = null;
                string fileSrc = null;
                int keysize = 0;    // RSA密钥位数. 0表示自动获取.
                foreach(string s in args) {
                    if ("-e".Equals(s, StringComparison.OrdinalIgnoreCase)) {
                        isEncode = true;
                    } else if ("-d".Equals(s, StringComparison.OrdinalIgnoreCase)) {
                        isDecode = true;
                    } else if ("-l".Equals(s, StringComparison.OrdinalIgnoreCase)) {
                        state = "l";
                    } else if ("-o".Equals(s, StringComparison.OrdinalIgnoreCase)) {
                        state = "o";
                    } else {
                        if ("l".Equals(state, StringComparison.OrdinalIgnoreCase)) {
                            fileKey = s;
                            state = null;
                        } else if ("o".Equals(state, StringComparison.OrdinalIgnoreCase)) {
                            fileOut = s;
                            state = null;
                        } else {
                            fileSrc = s;
                        }
                    }
                }
                try{
                    if (string.IsNullOrEmpty(fileKey)) {
                        export.WriteLine("No key file! Command need add `-l [keyfile]`.");
                    } else if (string.IsNullOrEmpty(fileOut)) {
                        export.WriteLine("No out file! Command need add `-o [outfile]`.");
                    } else if (string.IsNullOrEmpty(fileSrc)) {
                        export.WriteLine("No src file! Command need add `[srcfile]`.");
                    } else if (isEncode!=false && isDecode!=false) {
                        export.WriteLine("No set Encode/Encode! Command need add `-e`/`-d`.");
                    } else if (isEncode) {
                        showhelp = false;
                        doEncode(export, keysize, fileKey, fileOut, fileSrc, null);
                    } else if (isDecode) {
                        showhelp = false;
                        doDecode(export, keysize, fileKey, fileOut, fileSrc, null);
                    }
                } catch (Exception ex) {
                    export.WriteLine(ex.ToString());
                }
                // do.
                if (showhelp) {
                    export.WriteLine(helpText);
                }
            }
    
            /// <summary>
            /// 进行加密.
            /// </summary>
            /// <param name="export">文本打印流.</param>
            /// <param name="keysize">密钥位数. 为0表示自动获取.</param>
            /// <param name="fileKey">密钥文件.</param>
            /// <param name="fileOut">输出文件.</param>
            /// <param name="fileSrc">源文件.</param>
            /// <param name="exargs">扩展参数.</param>
            private void doEncode(TextWriter export, int keysize, string fileKey, string fileOut,
                    string fileSrc, IDictionary exargs) {
                byte[] bytesSrc = File.ReadAllBytes(fileSrc);
                string strDataKey = File.ReadAllText(fileKey);
                string purposetext = null;
                char purposecode = '';
                byte[] bytesKey = ZlRsaUtil.PemUnpack(strDataKey, ref purposetext, ref purposecode);
                //export.WriteLine(bytesKey);
                // key.
                RSACryptoServiceProvider rsa;
                if ('R' == purposecode) {
                    rsa = ZlRsaUtil.PemDecodePkcs8PrivateKey(bytesKey); // try 
                    if (null == rsa) {
                        rsa = ZlRsaUtil.PemDecodeX509PrivateKey(bytesKey);
                    }
                } else {    // 公钥或无法判断时, 均当成公钥处理.
                    rsa = ZlRsaUtil.PemDecodePublicKey(bytesKey);
                }
                if (null == rsa) {
                    export.WriteLine("Key decode fail!");
                    return;
                }
                export.WriteLine(string.Format("KeyExchangeAlgorithm: {0}", rsa.KeyExchangeAlgorithm));
                export.WriteLine(string.Format("KeySize: {0}", rsa.KeySize));
                // encrypt.
                byte[] cipherBytes = ZlRsaUtil.Encrypt(rsa, bytesSrc);
                string cipherBase64 = Convert.ToBase64String(cipherBytes);
                File.WriteAllText(fileOut, cipherBase64);
                export.WriteLine(string.Format("{0} save done.", fileOut));
            }
    
            /// <summary>
            /// 进行解密.
            /// </summary>
            /// <param name="export">文本打印流.</param>
            /// <param name="keysize">密钥位数. 为0表示自动获取.</param>
            /// <param name="fileKey">密钥文件.</param>
            /// <param name="fileOut">输出文件.</param>
            /// <param name="fileSrc">源文件.</param>
            /// <param name="exargs">扩展参数.</param>
            private void doDecode(TextWriter export, int keysize, string fileKey, string fileOut,
                    string fileSrc, IDictionary exargs) {
                String bytesSrcB64Src = File.ReadAllText(fileSrc);
                byte[] bytesSrc = Convert.FromBase64String(bytesSrcB64Src);
                string strDataKey = File.ReadAllText(fileKey);
                string purposetext = null;
                char purposecode = '';
                byte[] bytesKey = ZlRsaUtil.PemUnpack(strDataKey, ref purposetext, ref purposecode);
                //export.WriteLine(bytesKey);
                // key.
                RSACryptoServiceProvider rsa;
                if ('R' == purposecode) {
                    rsa = ZlRsaUtil.PemDecodePkcs8PrivateKey(bytesKey); // try 
                    if (null == rsa) {
                        rsa = ZlRsaUtil.PemDecodeX509PrivateKey(bytesKey);
                    }
                } else {    // 公钥或无法判断时, 均当成公钥处理.
                    rsa = ZlRsaUtil.PemDecodePublicKey(bytesKey);
                }
                if (null == rsa) {
                    export.WriteLine("Key decode fail!");
                    return;
                }
                export.WriteLine(string.Format("KeyExchangeAlgorithm: {0}", rsa.KeyExchangeAlgorithm));
                export.WriteLine(string.Format("KeySize: {0}", rsa.KeySize));
                // encryption.
                byte[] cipherBytes = ZlRsaUtil.Decrypt(rsa, bytesSrc);
                File.WriteAllBytes(fileOut, cipherBytes);
                export.WriteLine(string.Format("{0} save done.", fileOut));
            }
    
            static void Main(string[] args) {
                Program demo = new Program();
                demo.run(Console.Out, args);
            }
        }
    }

    源码地址:

    https://github.com/zyl910/rsapemdemo

    参考文献

    • 《RSA (cryptosystem)》: https://en.wikipedia.org/wiki/RSA_(cryptosystem)
    • Michel I. Gallant Ph.D.《RSA Public, Private, and PKCS #8 key parser》(OpenSSLKey.cs). http://www.jensign.com/opensslkey/
    • 《PKCS#1:RSA加密》. http://man.chinaunix.net/develop/rfc/RFC2313.txt
    • 《在线生成生成RSA密钥对》. http://web.chacuo.net/netrsakeypair
    • 蒋国纲《那些证书相关的玩意儿(SSL,X.509,PEM,DER,CRT,CER,KEY,CSR,P12等)》. http://www.cnblogs.com/guogangj/p/4118605.html
    • 阮一峰《RSA算法原理(二)》. http://www.ruanyifeng.com/blog/2013/07/rsa_algorithm_part_two.html
    • 任家《OPENSSL中RSA私钥文件(PEM格式)解析【一】》. http://blog.sina.com.cn/s/blog_4fcd1ea30100yh4s.html
    • 写代码的二妹《PHP,C# 和JAVARSA签名及验签》. http://www.cnblogs.com/frankyou/p/5993756.html
    • FrankYou《C# RSA 分段加解密》. http://www.cnblogs.com/frankyou/p/5993756.html
    • FrankYou《Java RSA 分段加解密》. http://www.cnblogs.com/frankyou/p/5993685.html
    • sahusoft《分组对称加密模式:ECB/CBC/CFB/OFB缺CTR》. http://blog.csdn.net/sahusoft/article/details/6867848
  • 相关阅读:
    go函数
    Linux 查看磁盘容量、查找大文件、查找大目录
    五分钟理解一致性哈希算法(consistent hashing)
    使用Java实现三个线程交替打印0-74
    Python实现IOC控制反转
    Wannafly挑战赛5 A珂朵莉与宇宙 前缀和+枚举平方数
    Yandex Big Data Essentials Week1 Scaling Distributed File System
    Yandex Big Data Essentials Week1 Unix Command Line Interface Processes managing
    Yandex Big Data Essentials Week1 Unix Command Line Interface File Content exploration
    Yandex Big Data Essentials Week1 Unix Command Line Interface File System exploration
  • 原文地址:https://www.cnblogs.com/Alex80/p/11526437.html
Copyright © 2011-2022 走看看