編輯:Android資訊
數據加密,是一門歷史悠久的技術,指通過加密算法和加密密鑰將明文轉變為密文,而解密則是通過解密算法和解密密鑰將密文恢復為明文。它的核心是密碼學。
數據加密目前仍是計算機系統對信息進行保護的一種最可靠的辦法。它利用密碼技術對信息進行加密,實現信息隱蔽從而起到保護信息的安全的作用。
項目中使用Socket進行文件傳輸過程時,需要先進行加密。實現的過程中踏了一些坑,下面對實現過程進行一下總結。
由於加密過程中使用的是DES加密算法,下面貼一下DES加密代碼:
//秘鑰算法
private static final String KEY_ALGORITHM = "DES";
//加密算法:algorithm/mode/padding 算法/工作模式/填充模式
private static final String CIPHER_ALGORITHM = "DES/ECB/PKCS5Padding";
//秘鑰
private static final String KEY = "12345678";//DES秘鑰長度必須是8位
public static void main(String args[]) {
String data = "加密解密";
KLog.d("加密數據:" + data);
byte[] encryptData = encrypt(data.getBytes());
KLog.d("加密後的數據:" + new String(encryptData));
byte[] decryptData = decrypt(encryptData);
KLog.d("解密後的數據:" + new String(decryptData));
}
public static byte[] encrypt(byte[] data) {
//初始化秘鑰
SecretKey secretKey = new SecretKeySpec(KEY.getBytes(), KEY_ALGORITHM);
try {
Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
byte[] result = cipher.doFinal(data);
return Base64.getEncoder().encode(result);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public static byte[] decrypt(byte[] data) {
byte[] resultBase64 = Base64.getDecoder().decode(data);
SecretKey secretKey = new SecretKeySpec(KEY.getBytes(), KEY_ALGORITHM);
try {
Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, secretKey);
byte[] result = cipher.doFinal(resultBase64);
return result;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
輸出:
加密數據:加密解密 加密後的數據:rt6XE06pElmLZMaVxrbfCQ== 解密後的數據:加密解密
Socket socket = new Socket(ApiConstants.HOST, ApiConstants.PORT);
OutputStream outStream = socket.getOutputStream();
InputStream inStream = socket.getInputStream();
RandomAccessFile fileOutStream = new RandomAccessFile(file, "r");
fileOutStream.seek(0);
byte[] buffer = new byte[1024];
int len = -1;
while (((len = fileOutStream.read(buffer)) != -1)) {
outStream.write(buffer, 0, len); // 這裡進行字節流的傳輸
}
fileOutStream.close();
outStream.close();
inStream.close();
socket.close();
Socket socket = server.accept();
InputStream inStream = socket.getInputStream();
OutputStream outStream = socket.getOutputStream();
outStream.write(response.getBytes("UTF-8"));
RandomAccessFile fileOutStream = new RandomAccessFile(file, "rwd");
fileOutStream.seek(0);
byte[] buffer = new byte[1024];
int len;
while ((len = inStream.read(buffer)) != -1) { // 從字節輸入流中讀取數據寫入到文件中
fileOutStream.write(buffer, 0, len);
}
fileOutStream.close();
inStream.close();
outStream.close();
socket.close();
下面對傳輸數據進行加密解密
方案一:直接對io流進行加密解密
客戶端變更如下:
while (((len = fileOutStream.read(buffer)) != -1)) {
outStream.write(DesUtil.encrypt(buffer) ,0, len); // 對字節數組進行加密
}
服務端變更代碼:
while ((len = inStream.read(buffer)) != -1) {
fileOutStream.write(DesUtil.decrypt(buffer), 0, len); // 對字節數組進行解密
}
執行代碼後,服務端解密時會報如下異常:
javax.crypto.BadPaddingException: pad block corrupted
猜測錯誤原因是加密過程會對數據進行填充處理,然後在io流傳輸的過程中,數據有丟包現象發生,所以解密會報異常。
加密後的結果是字節數組,這些被加密後的字節在碼表(例如UTF-8 碼表)上找不到對應字符,會出現亂碼,當亂碼字符串再次轉換為字節數組時,長度會變化,導致解密失敗,所以轉換後的數據是不安全的。
於是嘗試了使用NOPadding填充模式,這樣雖然可以成功解密,測試中發現對於一般文件,如.txt文件可以正常顯示內容,但是.apk等文件則會有解析包出現異常等錯誤提示。
方案二:使用字符流
使用Base64 對字節數組進行編碼,任何字節都能映射成對應的Base64 字符,之後能恢復到字節數組,利於加密後數據的保存於傳輸,所以轉換是安全的。同樣,字節數組轉換成16 進制字符串也是安全的。
由於客戶端從輸入文件中讀取的是字節流,需要先將字節流轉換成字符流,而服務端接收到字符流後需要先轉換成字節流,再將其寫入到文件。測試中發現可以對字符流成功解密,但是將文件轉化成字符流進行傳輸是個連續的過程,而文件的寫出和寫入又比較繁瑣,操作過程中會出現很多問題。
方案三:使用CipherInputStream、CipherOutputStream
使用過程中發現只有當CipherOutputStream流close時,CipherInputStream才會接收到數據,顯然這個方案也只好pass掉。
方案四:使用SSLSocket
在Android上使用SSLSocket的會稍顯復雜,首先客戶端和服務端需要生成秘鑰和證書。生成方法可以參考這篇。Android證書的格式還必須是bks格式(Java使用jks格式)。一般來說,我們使用jdk的keytool只能生成jks的證書庫,如果生成bks的則需要下載BouncyCastle庫。具體方法可以參考這裡
服務端的代碼參考:http://blog.sina.com.cn/s/blog_792cc4290100syyf.html
客戶端的代碼參考:http://blog.sina.com.cn/s/blog_792cc4290100syyt.html
當以上所有的一切都准備完畢後,如果在Android6.0以上使用你會悲催的發現下面這個異常:
javax.net.ssl.SSLHandshakeException: Handshake failed
異常原因:SSLSocket簽名算法默認為DSA,Android6.0(API 23)以後KeyStore發生更改,不再支持DSA,但仍支持ECDSA。
所以如果想在Android6.0以上使用SSLSocket,需要將DSA改成ECDSA…org感覺坑越入越深看不到底呀…於是決定換個思路來解決socket加密這個問題。既然對文件邊傳邊加密解密不好使,那能不能客戶端傳輸文件前先對文件進行加密,然後進行傳輸,服務端成功接收文件後,再對文件進行解密呢。於是就有了下面這個方案。
方案五:先對文件進行加密,然後傳輸,服務端成功接收文件後再對文件進行解密
對文件進行加密解密代碼如下:
public class FileDesUtil {
//秘鑰算法
private static final String KEY_ALGORITHM = "DES";
//加密算法:algorithm/mode/padding 算法/工作模式/填充模式
private static final String CIPHER_ALGORITHM = "DES/ECB/PKCS5Padding";
private static final byte[] KEY = {56, 57, 58, 59, 60, 61, 62, 63};//DES 秘鑰長度必須是8 位或以上
/**
* 文件進行加密並保存加密後的文件到指定目錄
*
* @param fromFile 要加密的文件 如c:/test/待加密文件.txt
* @param toFile 加密後存放的文件 如c:/加密後文件.txt
*/
public static void encrypt(String fromFilePath, String toFilePath) {
KLog.i("encrypting...");
File fromFile = new File(fromFilePath);
if (!fromFile.exists()) {
KLog.e("to be encrypt file no exist!");
return;
}
File toFile = getFile(toFilePath);
SecretKey secretKey = new SecretKeySpec(KEY, KEY_ALGORITHM);
InputStream is = null;
OutputStream out = null;
CipherInputStream cis = null;
try {
Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
is = new FileInputStream(fromFile);
out = new FileOutputStream(toFile);
cis = new CipherInputStream(is, cipher);
byte[] buffer = new byte[1024];
int r;
while ((r = cis.read(buffer)) > 0) {
out.write(buffer, 0, r);
}
} catch (Exception e) {
KLog.e(e.toString());
} finally {
try {
if (cis != null) {
cis.close();
}
} catch (IOException e) {
e.printStackTrace();
}
try {
if (is != null) {
is.close();
}
} catch (IOException e) {
e.printStackTrace();
}
try {
if (out != null) {
out.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
KLog.i("encrypt completed");
}
@NonNull
private static File getFile(String filePath) {
File fromFile = new File(filePath);
if (!fromFile.getParentFile().exists()) {
fromFile.getParentFile().mkdirs();
}
return fromFile;
}
/**
* 文件進行解密並保存解密後的文件到指定目錄
*
* @param fromFilePath 已加密的文件 如c:/加密後文件.txt
* @param toFilePath 解密後存放的文件 如c:/ test/解密後文件.txt
*/
public static void decrypt(String fromFilePath, String toFilePath) {
KLog.i("decrypting...");
File fromFile = new File(fromFilePath);
if (!fromFile.exists()) {
KLog.e("to be decrypt file no exist!");
return;
}
File toFile = getFile(toFilePath);
SecretKey secretKey = new SecretKeySpec(KEY, KEY_ALGORITHM);
InputStream is = null;
OutputStream out = null;
CipherOutputStream cos = null;
try {
Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, secretKey);
is = new FileInputStream(fromFile);
out = new FileOutputStream(toFile);
cos = new CipherOutputStream(out, cipher);
byte[] buffer = new byte[1024];
int r;
while ((r = is.read(buffer)) >= 0) {
cos.write(buffer, 0, r);
}
} catch (Exception e) {
KLog.e(e.toString());
} finally {
try {
if (cos != null) {
cos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
try {
if (out != null) {
out.close();
}
} catch (IOException e) {
e.printStackTrace();
}
try {
if (is != null) {
is.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
KLog.i("decrypt completed");
}
}
使用如上這個方案就完美的繞開了上面提到的一些問題,成功的實現了使用Socket對文件進行加密傳輸。
對於任何技術的使用,底層原理的理解還是很有必要的。不然遇到問題很容易就是一頭霧水不知道Why!接下來准備看一下《圖解加密技術》和《圖解TCP/IP》這兩本書,以便加深對密碼學和Socket底層原理的理解。
每個Android程序員應該知道的Kotlin
Android開發者在語言限制方面面臨著一個困境。眾所周知,目前的Android開發只支持Java 6(語言本身從Java 7開始進行了一些改進),因此我們每天
Android根據屏幕寬度,按比例縮放圖片
ImageView有scaleType屬性可以縮放圖片,讓圖片鋪滿屏幕寬度,但是會出現壓縮或裁剪的情況。 ImageView的scaleType的屬性分別是mat
Android中的Activitys, Threads和內存洩露
Android編程中一個共同的困難就是協調Activity的生命周期和長時間運行的任務(task),並且要避免可能的內存洩露。思考下面Activity的代碼,在它
Android SharedPreferences存儲數據使用實例分析
在Android開發中,存儲數據的方式有很多種,本文將通過一個簡單的實例來分析如何用Android SharedPreferences數據存儲方式來進行輕量級數據