Skip to content

Instantly share code, notes, and snippets.

@panpf
Last active June 30, 2023 07:58
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save panpf/8db42562a80a647b22f2c07e3bdaca2e to your computer and use it in GitHub Desktop.
Save panpf/8db42562a80a647b22f2c07e3bdaca2e to your computer and use it in GitHub Desktop.
AES 加解密
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Base64;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.Provider;
import java.security.ProviderException;
import java.security.SecureRandom;
import java.security.spec.AlgorithmParameterSpec;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
/**
* AES 对称加密、解密工具
* <p>
* <p>
* AES 不支持 NoPadding、PKCS1Padding
*/
public class AESHelper {
private static final String ALGORITHM = "AES";
private static final String MODE_ECB = "ECB";
private static final String MODE_CBC = "CBC";
private static final String PADDING_PKCS5 = "PKCS5Padding";
private static final String PADDING_PKCS7 = "PKCS7Padding";
private Key key;
private String mode;
private String padding;
private AESHelper(@NonNull Key key, @Nullable String mode, @Nullable String padding) {
this.key = key;
this.mode = mode;
this.padding = padding;
}
/**
* 创建一个秘钥
*/
@NonNull
@SuppressWarnings({"WeakerAccess", "unused"})
public static Key createKey(int keySize) {
KeyGenerator generator;
try {
generator = KeyGenerator.getInstance(ALGORITHM);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
generator.init(keySize);
return generator.generateKey();
}
/**
* 根据种子生成一个秘钥,种子可以是任意长度的,固定种子始终生成固定的秘钥
*/
@NonNull
@SuppressWarnings({"WeakerAccess", "unused"})
public static Key createKeyBySeed(String seed) {
KeyGenerator generator;
try {
generator = KeyGenerator.getInstance(ALGORITHM);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
SecureRandom random;
try {
if (android.os.Build.VERSION.SDK_INT >= 24) {
// N 以上不支持 Crypto 要自己实现
random = SecureRandom.getInstance("SHA1PRNG", new CryptoProvider());
} else if (android.os.Build.VERSION.SDK_INT >= 17) {
// 在 4.2 以上版本中,SecureRandom 获取方式发生了改变
random = SecureRandom.getInstance("SHA1PRNG", "Crypto");
} else {
random = SecureRandom.getInstance("SHA1PRNG");
}
} catch (NoSuchAlgorithmException | NoSuchProviderException e) {
throw new IllegalArgumentException(e);
}
random.setSeed(seed.getBytes());
generator.init(128, random);
return generator.generateKey();
}
/**
* 秘钥并转换成 BASE64
*/
@NonNull
@SuppressWarnings("unused")
public static String keyToBase64(Key key) {
return Base64.encodeToString(key.getEncoded(), Base64.NO_WRAP);
}
/**
* 秘钥并转换成字节数组
*/
@NonNull
@SuppressWarnings("unused")
public static byte[] keyToBytes(Key key) {
return key.getEncoded();
}
/**
* 根据密码获取 key
*
* @param keyBytes 密码
* @return 用密码生成的 key
*/
@NonNull
@SuppressWarnings("unused")
public static Key keyFromBytes(@NonNull byte[] keyBytes) {
return new SecretKeySpec(keyBytes, ALGORITHM);
}
/**
* 根据密码获取 key
*
* @param passwordBase64 密码使用了 Base64 加密
* @return 用密码生成的 key
*/
@NonNull
@SuppressWarnings("unused")
public static Key keyFromBase64(@NonNull String passwordBase64) {
return new SecretKeySpec(Base64.decode(passwordBase64, Base64.DEFAULT), ALGORITHM);
}
/**
* 创建使用默认 mode 和 padding 的 AESHelper
*
* @param key 秘钥
*/
@NonNull
@SuppressWarnings("unused")
public static AESHelper defaultConfig(@NonNull Key key) {
return new Builder(key).build();
}
/**
* 创建 mode 为 ECB 并使用 PKCS5Padding 填充的 AESHelper
*
* @param key 秘钥
*/
@NonNull
@SuppressWarnings("unused")
public static AESHelper ecbPKCS5Padding(@NonNull Key key) {
return new Builder(key).ecbMode().pkcs5Padding().build();
}
/**
* 创建 mode 为 ECB 并使用 PKCS7Padding 填充的 AESHelper
*
* @param key 秘钥
*/
@NonNull
@SuppressWarnings("unused")
public static AESHelper ecbPKCS7Padding(@NonNull Key key) {
return new Builder(key).ecbMode().pkcs7Padding().build();
}
/**
* 创建 mode 为 CBC 并使用 PKCS5Padding 填充的 AESHelper
*
* @param key 秘钥
*/
@NonNull
@SuppressWarnings({"unused", "WeakerAccess"})
public static AESHelper cbcPKCS5Padding(@NonNull Key key) {
return new Builder(key).cbcMode().pkcs5Padding().build();
}
/**
* 创建 mode 为 CBC 并使用 PKCS7Padding 填充的 AESHelper
*
* @param key 秘钥
*/
@NonNull
@SuppressWarnings("unused")
public static AESHelper cbcPKCS7Padding(@NonNull Key key) {
return new Builder(key).cbcMode().pkcs7Padding().build();
}
/**
* 加密
*
* @param textBytes 待加密的明文
* @return 加密后的密文
* @throws InvalidKeyException 密码无效
* @throws BadPaddingException 密码错误
* @throws IllegalBlockSizeException 密码长度和密文长度不匹配,请检查密码长度个密文长度
*/
@NonNull
@SuppressWarnings("WeakerAccess")
public byte[] encrypt(@NonNull byte[] textBytes) throws InvalidKeyException,
BadPaddingException, IllegalBlockSizeException {
return createCipher(Cipher.ENCRYPT_MODE).doFinal(textBytes);
}
/**
* 加密
*
* @param text 待加密的明文
* @return 加密后的密文
* @throws InvalidKeyException 密码无效
* @throws BadPaddingException 密码错误
* @throws IllegalBlockSizeException 密码长度和密文长度不匹配,请检查密码长度个密文长度
*/
@NonNull
@SuppressWarnings("unused")
public byte[] encrypt(@NonNull String text) throws InvalidKeyException,
BadPaddingException, IllegalBlockSizeException {
return encrypt(text.getBytes());
}
/**
* 加密并返回 BASE64 字符串
*
* @param textBytes 待加密的明文
* @return 加密后的密文
* @throws InvalidKeyException 密码无效
* @throws BadPaddingException 密码错误
* @throws IllegalBlockSizeException 密码长度和密文长度不匹配,请检查密码长度个密文长度
*/
@NonNull
@SuppressWarnings("unused")
public String encryptToBase64(@NonNull byte[] textBytes) throws InvalidKeyException,
BadPaddingException, IllegalBlockSizeException {
return Base64.encodeToString(encrypt(textBytes), Base64.NO_WRAP);
}
/**
* 加密并返回 BASE64 字符串
*
* @param text 待加密的明文
* @return 加密后的密文
* @throws InvalidKeyException 密码无效
* @throws BadPaddingException 密码错误
* @throws IllegalBlockSizeException 密码长度和密文长度不匹配,请检查密码长度个密文长度
*/
@NonNull
@SuppressWarnings({"unused", "WeakerAccess"})
public String encryptToBase64(@NonNull String text) throws InvalidKeyException,
BadPaddingException, IllegalBlockSizeException {
return Base64.encodeToString(encrypt(text.getBytes()), Base64.NO_WRAP);
}
/**
* 解密
*
* @param cipherTextBytes 待解密的密文
* @return 解密后的明文
* @throws InvalidKeyException 密码无效
* @throws BadPaddingException 密码错误
* @throws IllegalBlockSizeException 密码长度和密文长度不匹配,请检查密码长度个密文长度
*/
@NonNull
@SuppressWarnings("WeakerAccess")
public String decrypt(@NonNull byte[] cipherTextBytes) throws InvalidKeyException,
BadPaddingException, IllegalBlockSizeException {
Cipher cipher = createCipher(Cipher.DECRYPT_MODE);
byte[] data = cipher.doFinal(cipherTextBytes);
return new String(data);
}
/**
* 解密
*
* @param cipherText 待解密的密文
* @return 解密后的明文
* @throws InvalidKeyException 密码无效
* @throws BadPaddingException 密码错误
* @throws IllegalBlockSizeException 密码长度和密文长度不匹配,请检查密码长度个密文长度
*/
@NonNull
@SuppressWarnings("unused")
public String decrypt(@NonNull String cipherText) throws InvalidKeyException,
BadPaddingException, IllegalBlockSizeException {
return decrypt(cipherText.getBytes());
}
/**
* 解密使用了 BASE64 转码的密文
*
* @param cipherTextBytes 待解密的密文
* @return 解密后的明文
* @throws InvalidKeyException 密码无效
* @throws BadPaddingException 密码错误
* @throws IllegalBlockSizeException 密码长度和密文长度不匹配,请检查密码长度个密文长度
*/
@NonNull
@SuppressWarnings("unused")
public String decryptFromBase64(@NonNull byte[] cipherTextBytes) throws InvalidKeyException,
BadPaddingException, IllegalBlockSizeException {
return decrypt(Base64.decode(cipherTextBytes, Base64.DEFAULT));
}
/**
* 解密使用了 BASE64 转码的密文
*
* @param cipherText 待解密的密文
* @return 解密后的明文
* @throws InvalidKeyException 密码无效
* @throws BadPaddingException 密码错误
* @throws IllegalBlockSizeException 密码长度和密文长度不匹配,请检查密码长度个密文长度
*/
@NonNull
@SuppressWarnings({"unused", "WeakerAccess"})
public String decryptFromBase64(@NonNull String cipherText) throws InvalidKeyException,
BadPaddingException, IllegalBlockSizeException {
return decrypt(Base64.decode(cipherText.getBytes(), Base64.DEFAULT));
}
@NonNull
private Cipher createCipher(int opMode) throws InvalidKeyException {
String cipherAlgorithm;
if (mode == null || padding == null) {
cipherAlgorithm = ALGORITHM;
} else {
cipherAlgorithm = String.format("%s/%s/%s", ALGORITHM, mode, padding);
}
Cipher cipher;
try {
cipher = Cipher.getInstance(cipherAlgorithm);
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
throw new IllegalArgumentException(e);
}
AlgorithmParameterSpec spec = null;
if (MODE_CBC.equals(mode)) {
spec = new IvParameterSpec(new byte[cipher.getBlockSize()]);
}
try {
cipher.init(opMode, key, spec);
} catch (InvalidAlgorithmParameterException e) {
throw new IllegalArgumentException(e);
}
return cipher;
}
private static class Builder {
private Key key;
private String mode;
private String padding;
private Builder(@NonNull Key key) {
this.key = key;
}
@NonNull
@SuppressWarnings({"unused", "WeakerAccess"})
public Builder ecbMode() {
mode = MODE_ECB;
return this;
}
@NonNull
@SuppressWarnings({"unused", "WeakerAccess"})
public Builder cbcMode() {
mode = MODE_CBC;
return this;
}
@NonNull
@SuppressWarnings({"unused", "WeakerAccess"})
public Builder pkcs5Padding() {
padding = PADDING_PKCS5;
return this;
}
@NonNull
@SuppressWarnings({"unused", "WeakerAccess"})
public Builder pkcs7Padding() {
padding = PADDING_PKCS7;
return this;
}
@SuppressWarnings("WeakerAccess")
@NonNull
public AESHelper build() {
return new AESHelper(key, mode, padding);
}
}
/**
* from: https://android.googlesource.com/platform/libcore-snapshot/+/ics-mr1/luni/src/main/java/org/apache/harmony/security/provider/crypto/CryptoProvider.java
*/
private static class CryptoProvider extends Provider {
private static final long serialVersionUID = 7991202868423459598L;
/**
* Creates a Provider and puts parameters
*/
public CryptoProvider() {
super("Crypto", 1.0, "HARMONY (SHA1 digest; SecureRandom; SHA1withDSA signature)");
// names of classes implementing services
final String MD_NAME = "org.apache.harmony.security.provider.crypto.SHA1_MessageDigestImpl";
final String SR_NAME = "org.apache.harmony.security.provider.crypto.SHA1PRNG_SecureRandomImpl";
final String SIGN_NAME = "org.apache.harmony.security.provider.crypto.SHA1withDSA_SignatureImpl";
final String SIGN_ALIAS = "SHA1withDSA";
final String KEYF_NAME = "org.apache.harmony.security.provider.crypto.DSAKeyFactoryImpl";
put("MessageDigest.SHA-1", MD_NAME);
put("MessageDigest.SHA-1 ImplementedIn", "Software");
put("Alg.Alias.MessageDigest.SHA1", "SHA-1");
put("Alg.Alias.MessageDigest.SHA", "SHA-1");
if (RandomBitsSupplier.isServiceAvailable()) {
put("SecureRandom.SHA1PRNG", SR_NAME);
put("SecureRandom.SHA1PRNG ImplementedIn", "Software");
}
put("Signature.SHA1withDSA", SIGN_NAME);
put("Signature.SHA1withDSA ImplementedIn", "Software");
put("Alg.Alias.Signature.SHAwithDSA", SIGN_ALIAS);
put("Alg.Alias.Signature.DSAwithSHA1", SIGN_ALIAS);
put("Alg.Alias.Signature.SHA1/DSA", SIGN_ALIAS);
put("Alg.Alias.Signature.SHA/DSA", SIGN_ALIAS);
put("Alg.Alias.Signature.SHA-1/DSA", SIGN_ALIAS);
put("Alg.Alias.Signature.DSA", SIGN_ALIAS);
put("Alg.Alias.Signature.DSS", SIGN_ALIAS);
put("Alg.Alias.Signature.OID.1.2.840.10040.4.3", SIGN_ALIAS);
put("Alg.Alias.Signature.1.2.840.10040.4.3", SIGN_ALIAS);
put("Alg.Alias.Signature.1.3.14.3.2.13", SIGN_ALIAS);
put("Alg.Alias.Signature.1.3.14.3.2.27", SIGN_ALIAS);
put("KeyFactory.DSA", KEYF_NAME);
put("KeyFactory.DSA ImplementedIn", "Software");
put("Alg.Alias.KeyFactory.1.3.14.3.2.12", "DSA");
put("Alg.Alias.KeyFactory.1.2.840.10040.4.1", "DSA");
}
}
/**
* from: https://android.googlesource.com/platform/libcore-snapshot/+/ics-mr1/luni/src/main/java/org/apache/harmony/security/provider/crypto/RandomBitsSupplier.java
*/
private static class RandomBitsSupplier {
/**
* names of random devices on Linux platform
*/
private static final String DEVICE_NAMES[] = {"/dev/urandom" /*, "/dev/random" */};
/**
* InputStream to read from device
* <p>
* Using a BufferedInputStream leads to problems
* on Android in rare cases, since the
* BufferedInputStream's available() issues an
* ioctl(), and the pseudo device doesn't seem
* to like that. Since we're reading bigger
* chunks and not single bytes, the FileInputStream
* shouldn't be slower, so we use that. Same might
* apply to other Linux platforms.
* <p>
* TODO: the above doesn't sound true.
*/
private static FileInputStream fis = null;
/**
* File to connect to device
*/
private static File randomFile = null;
/**
* value of field is "true" only if a device is available
*/
private static boolean serviceAvailable = false;
static {
for (String deviceName : DEVICE_NAMES) {
try {
File file = new File(deviceName);
if (file.canRead()) {
fis = new FileInputStream(file);
randomFile = file;
serviceAvailable = true;
}
} catch (FileNotFoundException e) {
}
}
}
/**
* The method is called by provider to determine if a device is available.
*/
static boolean isServiceAvailable() {
return serviceAvailable;
}
/**
* On platforms with "random" devices available,
* the method reads random bytes from the device. <BR>
* <p>
* In case of any runtime failure ProviderException gets thrown.
*/
private static synchronized byte[] getUnixDeviceRandom(int numBytes) {
byte[] bytes = new byte[numBytes];
int total = 0;
int bytesRead;
int offset = 0;
try {
for (; ; ) {
bytesRead = fis.read(bytes, offset, numBytes - total);
// the below case should not occur because /dev/random or /dev/urandom is a special file
// hence, if it is happened there is some internal problem
if (bytesRead == -1) {
throw new ProviderException("bytesRead == -1");
}
total += bytesRead;
offset += bytesRead;
if (total >= numBytes) {
break;
}
}
} catch (IOException e) {
// actually there should be no IOException because device is a special file;
// hence, there is either some internal problem or, for instance,
// device was removed in runtime, or something else
throw new ProviderException("ATTENTION: IOException in RandomBitsSupplier.getLinuxRandomBits(): " + e);
}
return bytes;
}
/**
* The method returns byte array of requested length provided service is available.
* ProviderException gets thrown otherwise.
*
* @param numBytes - length of bytes requested
* @return byte array
* @throws IllegalArgumentException - if numBytes <= 0
*/
public static byte[] getRandomBits(int numBytes) {
if (numBytes <= 0) {
throw new IllegalArgumentException(Integer.toString(numBytes));
}
// We have been unable to get a random device or fall back to the
// native security module code - throw an exception.
if (!serviceAvailable) {
throw new ProviderException("ATTENTION: service is not available : no random devices");
}
return getUnixDeviceRandom(numBytes);
}
}
}
import android.support.test.filters.LargeTest;
import android.support.test.runner.AndroidJUnit4;
import com.yingyonghui.market.util.AESHelper;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.spec.InvalidKeySpecException;
import javax.crypto.BadPaddingException;
import javax.crypto.IllegalBlockSizeException;
@RunWith(AndroidJUnit4.class)
@LargeTest
public class AESHelperTest {
private static final String SOURCE = "小红那年七岁,她跟着爸妈去赶集,站在一个卖童装的摊位旁边,盯着那条裙子,无论如何都不肯走。\n" +
"\n" +
"她太想要这样一条裙子了,或者说白了,她想要一件新衣服,一件不是妈妈手工做的,而是商店里买来的衣服。\n" +
"\n" +
"她听见已经走出了几步的妈妈跟站得更远的爸爸说,“这孩子到底随谁呢?才这么大就这么臭美?我们家里人哪有这种样子的?能不能要点脸?”\n" +
"\n" +
"爸爸已经非常烦躁,走上前来,不由分说劈头给了她一个耳光,“就知道穿穿穿,打扮那么排场出去干什么?”\n" +
"\n" +
"她哇地一声哭了,妈妈倒是很恼怒地跑过来拉住爸爸,“你有病吗?谁让你打她了?”\n" +
"\n" +
"可是裙子最终还是没买。她反而成了全家人的笑柄,一直到很多年后,妈妈提起她小时候,还是笑得肚子疼,“一丁点儿大,臭美得很!”\n" +
"\n" +
"然后有兄弟姐妹在旁边起哄揶揄她\n" +
"\n" +
"作者:kongguyouling\n" +
"链接:http://www.jianshu.com/p/c60af04614af\n" +
"來源:简书\n" +
"著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。";
@Test
public void testBytesAndBase64() throws InvalidKeySpecException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException {
AESHelper aesHelper = AESHelper.ecbPKCS5Padding(AESHelper.createKey(128));
Assert.assertEquals("【AES】Bytes encrypt and decrypt test failed", SOURCE, aesHelper.decrypt(aesHelper.encrypt(SOURCE.getBytes())));
Assert.assertEquals("【AES】Base64 encrypt and decrypt test failed", SOURCE, aesHelper.decryptFromBase64(aesHelper.encryptToBase64(SOURCE)));
}
@Test
public void testModeAndPadding() throws InvalidKeySpecException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
byte[] sourceBytes = SOURCE.getBytes();
Key key = AESHelper.createKey(128);
AESHelper aesHelper = AESHelper.defaultConfig(key);
String decryptResult = aesHelper.decrypt(aesHelper.encrypt(sourceBytes));
Assert.assertEquals("【AES】default encrypt and decrypt test failed", SOURCE, decryptResult);
AESHelper ebcPacks5PaddingAesHelper = AESHelper.ecbPKCS5Padding(key);
String ebcPacks5PaddingDecryptResult = ebcPacks5PaddingAesHelper.decrypt(ebcPacks5PaddingAesHelper.encrypt(sourceBytes));
Assert.assertEquals("【AES】ebcPacks5Padding encrypt and decrypt test failed", SOURCE, ebcPacks5PaddingDecryptResult);
AESHelper ebcPacks7PaddingAesHelper = AESHelper.ecbPKCS7Padding(key);
String ebcPacks7PaddingDecryptResult = ebcPacks7PaddingAesHelper.decrypt(ebcPacks7PaddingAesHelper.encrypt(sourceBytes));
Assert.assertEquals("【AES】ebcPacks7Padding encrypt and decrypt test failed", SOURCE, ebcPacks7PaddingDecryptResult);
AESHelper cbcPacks5PaddingAesHelper = AESHelper.cbcPKCS5Padding(key);
String cbcPacks5PaddingDecryptResult = cbcPacks5PaddingAesHelper.decrypt(cbcPacks5PaddingAesHelper.encrypt(sourceBytes));
Assert.assertEquals("【AES】cbcPacks5Padding encrypt and decrypt test failed", SOURCE, cbcPacks5PaddingDecryptResult);
AESHelper cbcPacks7PaddingAesHelper = AESHelper.cbcPKCS7Padding(key);
String cbcPacks7PaddingDecryptResult = cbcPacks7PaddingAesHelper.decrypt(cbcPacks7PaddingAesHelper.encrypt(sourceBytes));
Assert.assertEquals("【AES】cbcPacks7Padding encrypt and decrypt test failed", SOURCE, cbcPacks7PaddingDecryptResult);
}
@Test
public void testErrorPassword() throws InvalidKeySpecException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
AESHelper aesHelper = AESHelper.ecbPKCS5Padding(AESHelper.createKey(128));
AESHelper errorAesHelper = AESHelper.ecbPKCS5Padding(AESHelper.createKey(128));
String encryptResult = aesHelper.encryptToBase64(SOURCE);
String errorDecryptResult = null;
try {
errorDecryptResult = errorAesHelper.decryptFromBase64(encryptResult);
} catch (BadPaddingException e) {
e.printStackTrace();
}
Assert.assertNotEquals("【AES】eTest error password failed", SOURCE, errorDecryptResult);
}
@Test
public void testCreateKeyBySeed() {
String seed = "" + System.currentTimeMillis();
String key1 = AESHelper.keyToBase64(AESHelper.createKeyBySeed(seed));
String key2 = AESHelper.keyToBase64(AESHelper.createKeyBySeed(seed));
Assert.assertEquals("Test create key by seed failed", key1, key2);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment