Android AES 文件加密解密(解决超大文件内存溢出问题)

  • Post author:
  • Post category:其他



总结:


超大文件采用AES加密,在Android手机上采用FileChannel,一次读取一定的字节数,而后再进行加密解密,最后再通过FileChannel生成新文件

几番折磨终有结果,现将Demo整理出来。。。

package com.king.zjc;
 
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import android.util.Log;
 
public class AESHelper {
	public static final String TAG = AESHelper.class.getSimpleName();
 
	Runtime mRuntime = Runtime.getRuntime();
 
	@SuppressWarnings("resource")
	public boolean <span style="color:#FF0000;">AESCipher</span>(int cipherMode, String sourceFilePath,
			String targetFilePath, String seed) {
		boolean result = false;
		FileChannel sourceFC = null;
		FileChannel targetFC = null;
 
		try {
 
			if (cipherMode != Cipher.ENCRYPT_MODE
					&& cipherMode != Cipher.DECRYPT_MODE) {
				Log.d(TAG,
						"Operation mode error, should be encrypt or decrypt!");
				return false;
			}
 
			Cipher mCipher = Cipher.getInstance("AES/CFB/NoPadding");
 
			byte[] rawkey = getRawKey(seed.getBytes());
			File sourceFile = new File(sourceFilePath);
			File targetFile = new File(targetFilePath);
 
			sourceFC = new RandomAccessFile(sourceFile, "r").getChannel();
			targetFC = new RandomAccessFile(targetFile, "rw").getChannel();
 
			SecretKeySpec secretKey = new SecretKeySpec(rawkey, "AES");
 
			mCipher.init(cipherMode, secretKey, new IvParameterSpec(
					new byte[mCipher.getBlockSize()]));
 
			ByteBuffer byteData = ByteBuffer.allocate(1024);
			while (sourceFC.read(byteData) != -1) {
				// 通过通道读写交叉进行。
				// 将缓冲区准备为数据传出状态
				byteData.flip();
 
				byte[] byteList = new byte[byteData.remaining()];
				byteData.get(byteList, 0, byteList.length);
//此处,若不使用数组加密解密会失败,因为当byteData达不到1024个时,加密方式不同对空白字节的处理也不相同,从而导致成功与失败。 
				byte[] bytes = mCipher.doFinal(byteList);
				targetFC.write(ByteBuffer.wrap(bytes));
				byteData.clear();
			}
 
			result = true;
		} catch (IOException | NoSuchAlgorithmException | InvalidKeyException
				| InvalidAlgorithmParameterException
				| IllegalBlockSizeException | BadPaddingException
				| NoSuchPaddingException e) {
			Log.d(TAG, e.getMessage());
 
		} finally {
			try {
				if (sourceFC != null) {
					sourceFC.close();
				}
				if (targetFC != null) {
					targetFC.close();
				}
			} catch (IOException e) {
				Log.d(TAG, e.getMessage());
			}
		}
 
		return result;
	}
 
	/**
	 * 加密后的字符串
	 * 
	 * @param seed
	 * @param clearText
	 * @return
	 */
	public String encrypt(String seed, String source) {
		// Log.d(TAG, "加密前的seed=" + seed + ",内容为:" + clearText);
		byte[] result = null;
		try {
			byte[] rawkey = getRawKey(seed.getBytes());
			result = encrypt(rawkey, source.getBytes());
		} catch (Exception e) {
			e.printStackTrace();
		}
		String content = toHex(result);
		return content;
 
	}
 
	/**
	 * 解密后的字符串
	 * 
	 * @param seed
	 * @param encrypted
	 * @return
	 */
	public String decrypt(String seed, String encrypted) {
		byte[] rawKey;
		try {
			rawKey = getRawKey(seed.getBytes());
			byte[] enc = toByte(encrypted);
			byte[] result = decrypt(rawKey, enc);
			String coentn = new String(result);
			return coentn;
		} catch (Exception e) {
			e.printStackTrace();
			return null;
		}
	}
 
	/**
	 * 使用一个安全的随机数来产生一个密匙,密匙加密使用的
	 * 
	 * @param seed
	 * @return
	 * @throws NoSuchAlgorithmException
	 */
	private byte[] <span style="color:#FF0000;">getRawKey</span>(byte[] seed) throws NoSuchAlgorithmException {
		// 获得一个随机数,传入的参数为默认方式。
		SecureRandom sr = SecureRandom.getInstance("SHA1PRNG");
		// 设置一个种子,一般是用户设定的密码
		sr.setSeed(seed);
		// 获得一个key生成器(AES加密模式)
		KeyGenerator keyGen = KeyGenerator.getInstance("AES");
		// 设置密匙长度128位
		keyGen.init(128, sr);
		// 获得密匙
		SecretKey key = keyGen.generateKey();
		// 返回密匙的byte数组供加解密使用
		byte[] raw = key.getEncoded();
		return raw;
	}
 
	/**
	 * 结合密钥生成加密后的密文
	 * 
	 * @param raw
	 * @param input
	 * @return
	 * @throws Exception
	 */
	private byte[] encrypt(byte[] raw, byte[] input) throws Exception {
		// 根据上一步生成的密匙指定一个密匙
		SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
		// Cipher cipher = Cipher.getInstance("AES");
		// 加密算法,加密模式和填充方式三部分或指定加密算
		Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
		// 初始化模式为加密模式,并指定密匙
		cipher.init(Cipher.ENCRYPT_MODE, skeySpec, new IvParameterSpec(
				new byte[cipher.getBlockSize()]));
		byte[] encrypted = cipher.doFinal(input);
		return encrypted;
	}
 
	/**
	 * 根据密钥解密已经加密的数据
	 * 
	 * @param raw
	 * @param encrypted
	 * @return
	 * @throws Exception
	 */
	private byte[] decrypt(byte[] raw, byte[] encrypted) throws Exception {
		SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
		Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
		cipher.init(Cipher.DECRYPT_MODE, skeySpec, new IvParameterSpec(
				new byte[cipher.getBlockSize()]));
		byte[] decrypted = cipher.doFinal(encrypted);
		return decrypted;
	}
 
	public String toHex(String txt) {
		return toHex(txt.getBytes());
	}
 
	public String fromHex(String hex) {
		return new String(toByte(hex));
	}
 
	public byte[] toByte(String hexString) {
		int len = hexString.length() / 2;
		byte[] result = new byte[len];
		for (int i = 0; i < len; i++)
			result[i] = Integer.valueOf(hexString.substring(2 * i, 2 * i + 2),
					16).byteValue();
		return result;
	}
 
	public String toHex(byte[] buf) {
		if (buf == null || buf.length <= 0)
			return "";
		StringBuffer result = new StringBuffer(2 * buf.length);
		for (int i = 0; i < buf.length; i++) {
			appendHex(result, buf[i]);
		}
		return result.toString();
	}
 
	private void appendHex(StringBuffer sb, byte b) {
		final String HEX = "0123456789ABCDEF";
		sb.append(HEX.charAt((b >> 4) & 0x0f)).append(HEX.charAt(b & 0x0f));
	}
}
 

1. 其实我的Demo中只用到了AESCipher和getRawKey两个方法。若要返回字符串,一定要注意编码,如

    public static String encrypt(String data, String key) throws Exception {  
            try {  
                Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");  
                SecretKeySpec keyspec = new SecretKeySpec(key.getBytes(), "AES");  
                cipher.init(Cipher.ENCRYPT_MODE, keyspec);  
                byte[] encrypted = cipher.doFinal(data.getBytes());  
                return Base64.encodeToString(encrypted, Base64.DEFAULT);  
            } catch (Exception e) {  
                e.printStackTrace();  
                return null;  
            }  
        }  
      
        public static String desEncrypt(String data, String key) throws Exception {  
            try {  
                byte[] encrypted1 = Base64.decode(data.getBytes(), Base64.DEFAULT);  
                Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");  
                SecretKeySpec keyspec = new SecretKeySpec(key.getBytes(), "AES");  
                cipher.init(Cipher.DECRYPT_MODE, keyspec);  
                byte[] original = cipher.doFinal(encrypted1);  
                return new String(original, "UTF-8");  
            } catch (Exception e) {  
                e.printStackTrace();  
                return null;  
            }  
        }  

 


2. 如果先把一个文件转换成字节数组,然后再加密,最后生成文件,这样很大机率很会产生OOM,所以这里利用了FileChannel,一次读取一定的字节数,而后再进行加密解密,最后再通过Channel生成新文件。

3. 先前一直失败,其重点是对

“填充模式”

的应用,我最终使用了

AES/CFB/NoPadding

,当不满16字节时,加密后数据长度不变。

    算法/模式/填充                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两种模式下则加密数据长度等于原始数据长度。

4. 文件大小不同,用时不定,所以把加密解密过程放到一个AsyncTask内进行

MainActivity.java

package com.king.zjc;
 
import javax.crypto.Cipher;
 
import com.hisense.ad.encryption.AESHelper;
import com.hisense.ad.encryption.R;
 
import android.annotation.SuppressLint;
import android.app.Activity;
import android.os.AsyncTask;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
 
@SuppressLint("SdCardPath")
public class MainActivity extends Activity {
 
	private final String SDcardPath = "/mnt/sdcard/encry/";
	private Button mEncryptButton;
	private Button mDecryptButton;
	private TextView mShowMessage;
	private EditText mFileName;
	private EditText mNewFileName;
	private AESHelper mAESHelper;
	private EncryptionOrDecryptionTask mTask = null;
 
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.main);
		mAESHelper = new AESHelper();
 
		mFileName = (EditText) findViewById(R.id.file_name);
		mNewFileName = (EditText) findViewById(R.id.new_file_name);
		mShowMessage = (TextView) findViewById(R.id.message);
		mEncryptButton = (Button) findViewById(R.id.encrypt);
		mDecryptButton = (Button) findViewById(R.id.decrypt);
 
		mEncryptButton.setOnClickListener(new OnClickListener() {
 
			@Override
			public void onClick(View v) {
 
				mShowMessage.setText("开始加密,请稍等...");
				if (mTask != null) {
					mTask.cancel(true);
				}
 
				mTask = new EncryptionOrDecryptionTask(true, SDcardPath
						+ mFileName.getText(), SDcardPath, mNewFileName
						.getText().toString(), "zjc");
				mTask.execute();
			}
		});
 
		mDecryptButton.setOnClickListener(new OnClickListener() {
 
			@Override
			public void onClick(View v) {
 
				mShowMessage.setText("开始解密,请稍等...");
 
				if (mTask != null) {
					mTask.cancel(true);
				}
 
				mTask = new EncryptionOrDecryptionTask(false, SDcardPath
						+ mFileName.getText(), SDcardPath, mNewFileName
						.getText().toString(), "zjc");
				mTask.execute();
 
			}
		});
	}
 
	// #######################
	/**
	 * 加密解密
	 */
	private class EncryptionOrDecryptionTask extends
			AsyncTask<Void, Void, Boolean> {
 
		private String mSourceFile = "";
		private String mNewFilePath = "";
		private String mNewFileName = "";
		private String mSeed = "";
		private boolean mIsEncrypt = false;
 
		public EncryptionOrDecryptionTask(boolean isEncrypt, String sourceFile,
				String newFilePath, String newFileName, String seed) {
			this.mSourceFile = sourceFile;
			this.mNewFilePath = newFilePath;
			this.mNewFileName = newFileName;
			this.mSeed = seed;
			this.mIsEncrypt = isEncrypt;
		}
 
		@Override
		protected Boolean doInBackground(Void... params) {
 
			boolean result = false;
 
			if (mIsEncrypt) {
				result = mAESHelper.AESCipher(Cipher.ENCRYPT_MODE, mSourceFile,
						mNewFilePath + mNewFileName, mSeed);
			} else {
				result = mAESHelper.AESCipher(Cipher.DECRYPT_MODE, mSourceFile,
						mNewFilePath + mNewFileName, mSeed);
			}
 
			return result;
		}
 
		@Override
		protected void onPostExecute(Boolean result) {
			super.onPostExecute(result);
			String showMessage = "";
 
			if (mIsEncrypt) {
				showMessage = result ? "加密已完成" : "加密失败!";
			} else {
				showMessage = result ? "解密完成" : "解密失败!";
			}
 
			mShowMessage.setText(showMessage);
		}
	}
 
}

main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >
 
    <TextView
        android:id="@+id/textView1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="请输入文件名" />
 
    <EditText
        android:id="@+id/file_name"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:ems="10"
        android:hint="请输入文件名" >
 
        <requestFocus />
    </EditText>
 
    <EditText
        android:id="@+id/new_file_name"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:ems="10"
        android:hint="请输入新的文件名" >
    </EditText>
 
    <Button
        android:id="@+id/encrypt"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="加密" />
 
    <Button
        android:id="@+id/decrypt"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="解密" />
 
    <TextView
        android:id="@+id/message"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="TextView" />
 
</LinearLayout>
 

注:许多代码来自网络,但已记不清最初来自哪里了。