Java中RSA加密兼容Android实现

RSA加密主要有以下几个操作:

  • RSA密钥生成

    密钥的生成有java程序生成,Linux命令生成,java keytool生成

  • RSA密钥存储

    密钥主要存储在java主机上,通过keytool导入或者生成

  • RSA根据公钥加密

    根据对方的公钥对明文加密生成密文,发送给对方

  • RSA根据密钥签名

    用自有的私钥对要明文签名,生成摘要签名,把明文和签名信息发送给你对方

  • RSA根据公钥验证签名

    对方接收到明文和签名,通过自有公钥发送给对方,对象通过发送过来的公钥进行验证.

  • RSA根据密钥解密

    对方接收到密文,通过对方的私钥解密接收到的密文

数字签名(通常的数字签名)的基础是公钥密码体系(例如:RSA)。发送者有独一无二的公钥和私钥,公钥是公开的,私钥秘密保留。发送者利用私钥对数据的消息摘要进行数字签名,接收者利用发送者的公钥来验证数字签名,其实就是把加密过程颠倒过来使用。由于发送者的私钥是独一无二的,而且是秘密的,因此当能够验证解开数字签名的数字摘要是正确的后,那么我们就可以肯定发送者的身份了,这就是数字签名的基本原理。

在java中通常使用默认的方式和android的RSA是不能通用的,android默认使用的Provider是BouncyCastleProvider, 所以要把Java中的Provider也切换到BouncyCastleProvider.这样他们才能相互认识.
BouncyCastle的Maven依赖:

<dependency>
	<groupId>org.bouncycastle</groupId>
	<artifactId>bcprov-jdk15on</artifactId>
	<version>1.51</version>
</dependency>

具体的实现请参照以下代码:

/**
 * RSA公钥/私钥/签名工具包
 * <p/>
 * 字符串格式的密钥在未在特殊说明情况下都为BASE64编码格式<br/>
 * 由于非对称加密速度极其缓慢,一般文件不使用它来加密而是使用对称加密,<br/>
 * 非对称加密算法可以用来对对称加密的密钥加密,这样保证密钥的安全也就保证了数据的安全
 *
 * @author yuml
 * @date 2015/3/4
 */
public class RSAUtils {


    static {
        //添加RSA第三方provider,主要解决android的兼容问题,提供统一的Provider
        Security.addProvider(new BouncyCastleProvider());
    }


    /**
     * 加密算法RSA
     */
    public static final String KEY_ALGORITHM_NAME = "RSA";

    public static final String KEY_ALGORITHM_MODE = "RSA/ECB/PKCS1Padding";


    /**
     * BouncyCastleProvider的名字
     */
    public static final String PROVIDER_NAME = "BC";

    /**
     * 签名算法
     */
    public static final String SIGNATURE_ALGORITHM_NAME = "MD5withRSA";

    /**
     * RSA最大加密明文大小
     */
    private static final int MAX_ENCRYPT_BLOCK = 117;

    /**
     * RSA最大解密密文大小
     */
    private static final int MAX_DECRYPT_BLOCK = 128;

    /**
     * <p>
     * 生成密钥对(公钥和私钥)
     * </p>
     *
     * @return
     * @throws Exception
     */
    public static KeyPair generateKeyPair() throws Exception {
        KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance(KEY_ALGORITHM_NAME, PROVIDER_NAME);
        keyPairGen.initialize(1024);
        KeyPair keyPair = keyPairGen.generateKeyPair();
        keyPair.getPublic();
        keyPair.getPrivate();
        return keyPair;
    }


    /**
     * 用私钥对信息生成数字签名
     *
     * @param data       已加密数据
     * @param privateKey 私钥
     * @return
     * @throws Exception
     */
    public static String sign(byte[] data, PrivateKey privateKey) throws Exception {
        Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM_NAME, PROVIDER_NAME);
        signature.initSign(privateKey);
        signature.update(data);
        return Base64.encode(signature.sign());
    }

    /**
     * 校验数字签名
     *
     * @param data      已加密数据
     * @param publicKey 公钥
     * @param sign      数字签名
     * @return
     * @throws Exception
     */
    public static boolean verify(byte[] data, PublicKey publicKey, String sign)
            throws Exception {
        Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM_NAME, PROVIDER_NAME);
        signature.initVerify(publicKey);
        signature.update(data);
        return signature.verify(Base64.decode(sign));
    }

    /**
     * 私钥解密
     *
     * @param encryptedData 已加密数据
     * @param privateKey    私钥
     * @return
     * @throws Exception
     */
    public static byte[] decryptByPrivateKey(byte[] encryptedData, PrivateKey privateKey)
            throws Exception {

        Cipher cipher = Cipher.getInstance(KEY_ALGORITHM_MODE, PROVIDER_NAME);
        cipher.init(Cipher.DECRYPT_MODE, privateKey);

        int inputLen = encryptedData.length;
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        int offSet = 0;
        byte[] cache;
        int i = 0;
        // 对数据分段解密
        while (inputLen - offSet > 0) {
            if (inputLen - offSet > MAX_DECRYPT_BLOCK) {
                cache = cipher.doFinal(encryptedData, offSet, MAX_DECRYPT_BLOCK);
            } else {
                cache = cipher.doFinal(encryptedData, offSet, inputLen - offSet);
            }
            out.write(cache, 0, cache.length);
            i++;
            offSet = i * MAX_DECRYPT_BLOCK;
        }
        byte[] decryptedData = out.toByteArray();
        out.close();
        return decryptedData;
    }

    /**
     * 公钥解密
     *
     * @param encryptedData 已加密数据
     * @param publicKey     公钥
     * @return
     * @throws Exception
     */
    public static byte[] decryptByPublicKey(byte[] encryptedData, PublicKey publicKey)
            throws Exception {

        Cipher cipher = Cipher.getInstance(KEY_ALGORITHM_MODE, PROVIDER_NAME);
        cipher.init(Cipher.DECRYPT_MODE, publicKey);

        int inputLen = encryptedData.length;
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        int offSet = 0;
        byte[] cache;
        int i = 0;
        // 对数据分段解密
        while (inputLen - offSet > 0) {
            if (inputLen - offSet > MAX_DECRYPT_BLOCK) {
                cache = cipher.doFinal(encryptedData, offSet, MAX_DECRYPT_BLOCK);
            } else {
                cache = cipher.doFinal(encryptedData, offSet, inputLen - offSet);
            }
            out.write(cache, 0, cache.length);
            i++;
            offSet = i * MAX_DECRYPT_BLOCK;
        }
        byte[] decryptedData = out.toByteArray();
        out.close();
        return decryptedData;
    }

    /**
     * 公钥加密
     *
     * @param data      源数据
     * @param publicKey 公钥
     * @return
     * @throws Exception
     */
    public static byte[] encryptByPublicKey(byte[] data, PublicKey publicKey)
            throws Exception {
        // 对数据加密
        Cipher cipher = Cipher.getInstance(KEY_ALGORITHM_MODE, PROVIDER_NAME);
        cipher.init(Cipher.ENCRYPT_MODE, publicKey);


        int inputLen = data.length;
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        int offSet = 0;
        byte[] cache;
        int i = 0;
        // 对数据分段加密
        while (inputLen - offSet > 0) {
            if (inputLen - offSet > MAX_ENCRYPT_BLOCK) {
                cache = cipher.doFinal(data, offSet, MAX_ENCRYPT_BLOCK);
            } else {
                cache = cipher.doFinal(data, offSet, inputLen - offSet);
            }
            out.write(cache, 0, cache.length);
            i++;
            offSet = i * MAX_ENCRYPT_BLOCK;
        }
        byte[] encryptedData = out.toByteArray();
        out.close();
        return encryptedData;
    }

    /**
     * 私钥加密
     *
     * @param data       源数据
     * @param privateKey 私钥
     * @return
     * @throws Exception
     */
    public static byte[] encryptByPrivateKey(byte[] data, PrivateKey privateKey)
            throws Exception {

        Cipher cipher = Cipher.getInstance(KEY_ALGORITHM_MODE, PROVIDER_NAME);
        cipher.init(Cipher.ENCRYPT_MODE, privateKey);

        int inputLen = data.length;
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        int offSet = 0;
        byte[] cache;
        int i = 0;
        // 对数据分段加密
        while (inputLen - offSet > 0) {
            if (inputLen - offSet > MAX_ENCRYPT_BLOCK) {
                cache = cipher.doFinal(data, offSet, MAX_ENCRYPT_BLOCK);
            } else {
                cache = cipher.doFinal(data, offSet, inputLen - offSet);
            }
            out.write(cache, 0, cache.length);
            i++;
            offSet = i * MAX_ENCRYPT_BLOCK;
        }
        byte[] encryptedData = out.toByteArray();
        out.close();
        return encryptedData;
    }


    /**
     * 从文件中输入流中加载公钥
     *
     * @param in 公钥输入流
     * @throws Exception 加载公钥时产生的异常
     */
    public PublicKey loadPublicKey(InputStream in) throws Exception {

        BufferedReader br = new BufferedReader(new InputStreamReader(in));
        String readLine = null;
        StringBuilder sb = new StringBuilder();
        while ((readLine = br.readLine()) != null) {
            if (readLine.charAt(0) == '-') {
                continue;
            } else {
                sb.append(readLine);
                sb.append('\r');
            }
        }
        return loadPublicKey(sb.toString());

    }

    /**
     * 从文件中加载私钥
     *
     * @param in 私钥文件名
     * @return 是否成功
     * @throws Exception
     */
    public PrivateKey loadPrivateKey(InputStream in) throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(in));
        String readLine = null;
        StringBuilder sb = new StringBuilder();
        while ((readLine = br.readLine()) != null) {
            if (readLine.charAt(0) == '-') {
                continue;
            } else {
                sb.append(readLine);
                sb.append('\r');
            }
        }
        return loadPrivateKey(sb.toString());
    }

    /**
     * 从字符串中加载私钥
     *
     * @param privateKey
     * @return
     * @throws Exception
     */
    public static PrivateKey loadPrivateKey(String privateKey) throws Exception {
        byte[] keyBytes = Base64.decode(privateKey);
        PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
        KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM_NAME, PROVIDER_NAME);
        return keyFactory.generatePrivate(keySpec);
    }

    /**
     * 从字符串中加载公钥
     *
     * @param publicKey
     * @return
     * @throws Exception
     */
    public static PublicKey loadPublicKey(String publicKey) throws Exception {
        byte[] keyBytes = Base64.decode(publicKey);
        X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
        KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM_NAME, PROVIDER_NAME);
        return keyFactory.generatePublic(keySpec);
    }

    /**
     * 从keystore中读取私钥
     *
     * @param alias
     * @param path     包含私钥的证书路径
     * @param password
     * @return 私钥PrivateKey
     * @throws Exception
     */
    public static PrivateKey loadKeyStorePrivateKey(String alias, String path, String password) throws Exception {
        FileInputStream fis = new FileInputStream(path);
        char[] passwordChars = null;
        if ((password != null) || "".equals(password.trim())) {
            passwordChars = password.toCharArray();
        }

        KeyStore keyStore = KeyStore.getInstance("PKCS12");
        keyStore.load(fis, passwordChars);
        fis.close();
        String keyAlias = alias;
        if (alias == null) {
            Enumeration<String> en = keyStore.aliases();
            if (en.hasMoreElements()) {
                keyAlias = en.nextElement();
            }
        }
        return (PrivateKey) keyStore.getKey(keyAlias, passwordChars);
    }

    /**
     * 从keystore中读取私钥
     *
     * @param path     包含私钥的证书路径
     * @param password
     * @return 私钥PrivateKey
     * @throws Exception
     */
    public static PrivateKey loadKeyStorePrivateKey(String path, String password) throws Exception {
        return loadKeyStorePrivateKey(null, path, password);
    }

    /**
     * 从keystore中读取公钥
     *
     * @param alias
     * @param path     包含公钥的证书路径
     * @param password
     * @return
     * @throws Exception
     */
    public static PublicKey loadKeyStorePublicKey(String alias, String path, String password) throws Exception {
        FileInputStream fis = new FileInputStream(path);
        char[] passwordChars = null;
        if ((password != null) || "".equals(password.trim())) {
            passwordChars = password.toCharArray();
        }

        KeyStore keyStore = KeyStore.getInstance("PKCS12");
        keyStore.load(fis, passwordChars);
        fis.close();
        String keyAlias = alias;
        if (alias == null) {
            Enumeration<String> en = keyStore.aliases();
            if (en.hasMoreElements()) {
                keyAlias = en.nextElement();
            }
        }
        return keyStore.getCertificate(keyAlias).getPublicKey();
    }

    /**
     * 从keystore中读取公钥
     *
     * @param path     包含公钥的证书路径
     * @param password
     * @return
     * @throws Exception
     */
    public static PublicKey loadKeyStorePublicKey(String path, String password) throws Exception {
        return loadKeyStorePublicKey(null, path, password);
    }

    /**
     * 根据公钥n、e生成公钥
     *
     * @param modulus        公钥n串
     * @param publicExponent 公钥e串
     * @return 返回公钥PublicKey
     * @throws Exception
     */
    public static PublicKey loadPublicKey(String modulus, String publicExponent) throws Exception {
        KeySpec publicKeySpec = new RSAPublicKeySpec(
                new BigInteger(modulus, 16), new BigInteger(publicExponent, 16));
        KeyFactory factory = KeyFactory.getInstance(KEY_ALGORITHM_NAME, PROVIDER_NAME);
        PublicKey publicKey = factory.generatePublic(publicKeySpec);
        return publicKey;
    }

}

Java中AES加密兼容PHP等其他语言实现

在之前的项目中涉及到java和php通信时采用AES加密,但是因为一些算法差异,导致不能直接通信,java需要做补位操作才能实现,后换了一种算法协议。今天在整理加密算法时,发现可以通过补位的方式来达到和PHP的通信,具体实现参照了 AES加密CBC模式兼容互通四种编程语言平台.

两种语言的差别主要是对于内容的填充实现上不一致.

AES支持五中模式:CBC,CFB,ECB,OFB,PCBC;支持三种填充:NoPadding,PKCS5Padding,ISO10126Padding。不支持SSL3Padding。不支持“NONE”模式。
其中AES/ECB/NoPadding和我现在使用的AESUtil得出的结果相同(在16的整数倍情况下)。
不带模式和填充来获取AES算法的时候,其默认使用ECB/PKCS5Padding。

算法/模式/填充                16字节加密后数据长度        不满16字节加密后长度
AES/CBC/NoPadding             16                          不支持
AES/CBC/PKCS5Padding          32                          16
AES/CBC/ISO10126Padding       32                          16
AES/CFB/NoPadding             16                          原始数据长度
AES/CFB/PKCS5Padding          32                          16
AES/CFB/ISO10126Padding       32                          16
AES/ECB/NoPadding             16                          不支持
AES/ECB/PKCS5Padding          32                          16
AES/ECB/ISO10126Padding       32                          16
AES/OFB/NoPadding             16                          原始数据长度
AES/OFB/PKCS5Padding          32                          16
AES/OFB/ISO10126Padding       32                          16
AES/PCBC/NoPadding            16                          不支持
AES/PCBC/PKCS5Padding         32                          16
AES/PCBC/ISO10126Padding      32                          16

可以看到,在原始数据长度为16的整数倍时,假如原始数据长度等于16*n,则使用NoPadding时加密后数据长度等于16*n,其它情况下加密数据长度等于16*(n+1)。在不足16的整数倍的情况下,假如原始数据长度等于16*n+m[其中m小于16],除了NoPadding填充之外的任何方式,加密数据长度都等于16*(n+1);NoPadding填充情况下,CBC、ECB和PCBC三种模式是不支持的,CFB、OFB两种模式下则加密数据长度等于原始数据长度。

我的主要实现思路是对于密码和IV我们通过MD5算法强制让所得到的byte[]长度是16的倍数,对于要加密的内容,先获取长度,如果长度不是16的倍数,则申请大于现有长度而且是16倍数的byte[]内存,然后通过拷贝把原有的内容拷贝到新申请的内存空间,这就相当于做了补零操作。其实就是实现了java的AES/CBC/ZeroPadding算法;对于解密也一样需要对末端的补零位清理,达到加密和解密的值相等;具体实现代码下面给出参考实现。

java参考代码:

/**
 * 兼容PHP等其他语言AES加密、解密方法
 *
 * @param mode
 * @param data
 * @param password
 * @param iv
 * @return
 * @throws Exception
 */
public static byte[] aesZeroPadding(int mode, byte[] data, String password, String iv) throws Exception {
	SecretKeySpec secretKeySpec = new SecretKeySpec(
		StringUtils.getBytesUtf8(md5(password).substring(0, 16)), 
		"AES");
	Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");

	IvParameterSpec ivParameterSpec = new IvParameterSpec(
		StringUtils.getBytesUtf8(md5(iv).substring(0, 16)));
	cipher.init(mode, secretKeySpec, ivParameterSpec);

	return cipher.doFinal(data);
}

/**
 * 兼容PHP等其他语言AES加密方法
 *
 * @param data
 * @param password
 * @param iv
 * @return
 * @throws Exception
 */
public static String encodeAESZeroPadding(String data, String password, String iv) {
	try {
		Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
		//对内容不是16整数倍数时补位
		int blockSize = cipher.getBlockSize();
		byte[] bytes = StringUtils.getBytesUtf8(data);
		int dataBytesLength = bytes.length;
		int mod = dataBytesLength % blockSize;
		if (mod != 0) {
			dataBytesLength = dataBytesLength + (blockSize - mod);
		}
		byte[] newBytes = new byte[dataBytesLength];
		System.arraycopy(bytes, 0, newBytes, 0, bytes.length);
		return Base64.encode(aesZeroPadding(Cipher.ENCRYPT_MODE, newBytes, password, iv));
	} catch (Exception e) {
		logger.error("AES ZeroPadding模式加密失败", e);
		throw new RuntimeException(e);
	}
}

/**
 * 兼容PHP等其他语言AES解密方法
 *
 * @param data
 * @param password
 * @param iv
 * @return
 * @throws Exception
 */
public static String decodeAESZeroPadding(String data, String password, String iv) {
	try {
		byte[] decodedBytes = aesZeroPadding(
			Cipher.DECRYPT_MODE, Base64.decode(data), password, iv);
		//去除增加的zero padding
		int emptyLength = 0;
		for (int i = decodedBytes.length; i > 0; i--) {

			if (decodedBytes[i - 1] == 0) {
				emptyLength++;
			} else {
				break;
			}
		}
		byte[] newBytes = new byte[decodedBytes.length - emptyLength];
		System.arraycopy(decodedBytes, 0, newBytes, 0, decodedBytes.length - emptyLength);
		return StringUtils.newStringUtf8(newBytes);
	} catch (Exception e) {
		logger.error("AES ZeroPadding模式解密失败", e);
		throw new RuntimeException(e);
	}
}

php参考代码:

//加密
$encrypted = mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $privateKey, $data, MCRYPT_MODE_CBC, $iv);
echo(base64_encode($encrypted));
echo '<br/>'; 

//解密
$encryptedData = base64_decode($encrypted);
$decrypted = mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $privateKey, $encryptedData, MCRYPT_MODE_CBC, $iv);
echo($decrypted);

Maven使用常见问题

  1. maven打包时不执行测试用例?

    有时因为测试不能运行,或者为了更快的编译,希望用mvn package的时候跳过test,有如下两种方法:
    第一种方法:

    mvn -DskipTests clean package
    

    第二种方法

    mvn -Dmaven.test.skip=true clean package
    

    但是这两个命令是有区别的,一定要注意。参数-DskipTests和-Dmaven.test.skip=true,这两个参数的主要区别是:

    • -DskipTests,不执行测试用例,但编译测试用例类生成相应的class文件至target/test-classes下。
    • -Dmaven.test.skip=true,不执行测试用例,也不编译测试用例类。

    如果在pom.xml设置了跳过,用命令即使设置跳过也不能覆盖,所以要清理掉xml中配置的<skipTests>false</skipTests>:

    <plugin>
    	<groupId>org.apache.maven.plugins</groupId>
    	<artifactId>maven-surefire-plugin</artifactId>
    	<version>2.10</version>
    	<configuration>
    		<skipTests>false</skipTests>
    	</configuration>
    </plugin>
    
  2. 如何手动上传打好的jar包到nexus仓库?

    如果想人工通过mvn命令上传已经存在的jar包到nexus上,执行maven命令如下:

    	mvn deploy:deploy-file 
    	-Durl=http://{ip}:{port}/nexus/content/repositories/{repository-id} 
    	-Dfile=./target/{jar-name}.jar 
    	-DrepositoryId={repository-id}  
    	-DgroupId={group-id} 
    	-DartifactId={artifact-id} 
    	-Dversion={version} 
    	-Dpackaging=jar
    
  3. 如何强制每次编译时都从仓库取最新的依赖jar包?

    在执行命令时添加-U参数来强制从仓库取最新的jar包依赖,形如下面的命令:

    mvn -U clean package
    
  4. maven项目模块循环依赖如何解决?

    对于项目因为配置了循环依赖的maven工程,可以通过设定不同的artifactId来达到断掉依赖,从而保证项目能正确编译打包,就涉及到手动上传打好的jar包,并且修改artifactId上传到nexus仓库,手动上传参见第2点。

对于J2EE架构以及开发流程的一些感想

工作好久,也接触过不同架构和框架,多少有些感触,总结一下如下:

  • 开发阶段,能够让开发人员快速上手,不能有太高的学习成本,最好是常用的Struts,spring,mybatis,hibernate等常用开源框架。提倡不要重复造轮子,最理想情况是升级改造!
  • 开发工具能够支持代码规范的自动检查,报错。
  • 有持续集成环境不断验证服务,必须有测试用例验证,并且验证代码是否符合规范,不符合规范报错,另外还要做代码覆盖率,注释检查等。做到错误提前发现
  • 功能交付前要有代码review
  • 测试以及交付阶段产生的功能需求必须跟踪
  • 开发过程中遇到的问题和解决方案要沉淀,文档必须迭代。发挥文档应有的功能价值。
  • 开发完成后,产品发布时自动发布成分布式或者集群式。开发不用关心如何部署。
  • 架构要支持HA,支持故障迁移,支持动态可伸缩
  • 产品交付阶段能够实时了解运行情况,监视实时动态,包含服务健康以及服务负载等指标,最好是图形化展示
  • 对于不同的项目一定要有协同看板,让团队知道团队的总体目标,让团队成员明确自己的任务,一个任务要具体人员,以及交付时间,以及交付产物有哪些!并且这些产物都是可以验证和review的。在一个固定的周期内要评审回顾。建议以敏捷的方式操作
  • 对于项目或者产品研发一定要明确不同角色的具体的流程,流程一定要管控死,流程可以根据迭代情况不断调整,但是调整前必须严格执行。否则就会有破窗效应,影响后续计划的执行,降低流程的执行效率。对于流程中的问题一定要及时弥补和反应,否则也会引起不好的后果。
  • 测试要自动化,部署自动化,运维要自动化,尽量减少人工干预,人可以在所有流程环节中,可以设置为最不可信任节点,有可以机器代替的尽量机器执行,做到高度自动化,高度可靠,高度量化!

穹顶之下 同呼吸共命运

今天放假,乘着小孩睡觉的时间看了“柴静雾霾调查:穹顶之下 同呼吸共命运”。感触很深,问题发生了,怎么解决才是重中之重。

希望我家旁边的河流流淌的不再是黑水、臭水,而是中间游着鱼儿,可以垂钓的净水。希望政府可以多做点事情,让家园更加美好,让世界更加美好!愿你我生活的世界都是美好的。

发现污染记得拨打12369.这个也许是我们举手之劳。