
在本文中,你將學習到一些提高Android應用安全級別的最佳實踐。
在Android中安全是你無法保證的事情。作為一個開發(fā)者,你無法知道你的應用是否足夠安全。每一個系統(tǒng)都會被破解但是你可以讓攻擊者的日子更加難熬。
網絡
現(xiàn)在幾乎所有的應用都會通過網絡和服務器交換用戶數據、令牌。
你應該考慮用戶網絡鏈接的安全以及怎么保護他們的數據不被盜取。

保證你的網絡鏈接更加安全的第一步就是使用HTTPS,當然這還不夠。
最流行的網絡攻擊之一就是中間人攻擊(MITM)。它可以是被動的也可以是主動的。
為了讓你的應用免遭被動MITM攻擊,你只需要使用迪菲-赫爾曼密鑰交換算法(視頻介紹)就可以。
主動MITM攻擊會強大一些。使用SSL鎖定可以使你的數據免遭盜取。
有許多工具支持HTTPS和SSL鎖定。Retrofit和OkHttp就是其中的兩個,在UPTech我們幾乎處處使用它們。
Retrofit使用起來非常簡單,它還支持RxJava,也不需要花很長的時間來配置。
在OkHttp的幫助下,你還可以添加你自己信任的SSL證書。
“OkHttp默認是信任服務平臺的證書授權的。證書認證可以提高安全性,但是它限制了服務端團隊升級其TLS證書的能力。在沒有得到服務端TLS管理員許可的情況下不要使用證書認證!”----Jesse Wilson, Square Inc
關于SSL認證以及怎樣安裝你可以參考這篇文章。
意圖(Intent)
你知道應用之間可以通過Intent交互。Intent的類型有兩種:顯式的和隱式的。
顯式意圖
優(yōu)點:不會被攔截。
缺點:只能在應用內部使用。
隱式意圖
優(yōu)點:可以在Android系統(tǒng)的任何地方使用。
缺點:很容易被攔截。
攻擊者只需要簡單的定義一個相同的intent-filter就可以攔截你的intent并獲取所有數據。
為了避免這種情況,你應該盡可能的使用顯式意圖。例如,你可以指定包名。
Intent intent = new Intent(Intent.ACTION_SEND);
intent.setPackage("com.test.package");
sendBroadcast(intent);
不同外部模塊交互的Broadcast Receiver或者Services不要暴露。
<service
android:name=".service.SomeService"
android:enabled="true"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</service>
結論
在你的應用中盡可能的使用顯式意圖。明確的指定你想發(fā)送數據的接收者。
數據存儲
內存緩存
內存緩存在這些場景下非常有用:當你從服務端獲取到數據并想將這些數據保存在一段時間內有效,或者你在處理bitmaps 位圖時想使用其他地方已經處理好的圖片,或者你想保存一些用戶數據,用戶標識等等。
舉個例子,怎么在內存緩存創(chuàng)建一個臨時文件:
File outputDir = this.getCacheDir();
File outputFile = File.createTempFile("prefix", "extension", outputDir);
比起在外部存儲器中打開一個文件就可以獲取到密碼,攻擊者要dump內存則就難得多。Dump內存需要ROOT權限。
Android官方文檔中關于內存緩存的說明可以戳這里(以緩存圖片為例)。
加解密(Cipher)
此外,你可以在將數據存入緩存、數據庫或者SharedPreferences之前將其加密。這里我推薦使用Cipher。
Cipher是一個內建的數據加密/解密工具。有關Cipher參數的更多信息請戳這里。
舉一個使用Cipher的例子:
private static byte[] encrypt(byte[] key, byte[] input) throws Exception {
SecretKeySpec skeySpec = new SecretKeySpec(key, "AES");
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.ENCRYPT_MODE, skeySpec);
byte[] encrypted = cipher.doFinal(input);
return encrypted;
}
private static byte[] decrypt(byte[] key, byte[] encrypted) throws Exception {
SecretKeySpec skeySpec = new SecretKeySpec(key, "AES");
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.DECRYPT_MODE, skeySpec);
byte[] decrypted = cipher.doFinal(encrypted);
return decrypted;
}
你應該了解某些加密算法(比如 AES)需要固定的密鑰長度,如128,192或者256bit。
更多Clipher所支持算法的詳細信息請戳這里。
Shared Preferences
和其他例子一樣,需要加密所有的數據,不要將原始文本直接存儲在Shared Preferences中。所有你想存放到Shared Preferences的數據都應該以“MODE_PRIVATE”屬性寫入,Android默認就是這個屬性。
有了“MODE_PRIVATE”屬性就能保證只有你的應用能夠讀寫數據。當然如果沒有ROOT權限誰也無法獲取你的數據。
還有另外一個工具即SecureSharedPreferences,你可以在這里了解更多的信息。
這個工具使用起來也很簡單,它是在原生的Shared Preferences基礎添加了很多加密算法。你不需要對它添加的新功能很了解,按照默認的Shared Preferences方式使用即可。
SharedPreferences prefs = new SecurePreferences(context, "userpassword", "my_user_prefs.xml");
這是它加密以后的樣子:
<map>
<string name="TuwbBU0IrAyL9znGBJ87uEi7pW0FwYwX8SZiiKnD2VZ7">
pD2UhS2K2MNjWm8KzpFrag==:MWm7NgaEhvaxAvA9wASUl0HUHCVBWkn3c2T1WoSAE/g=rroijgeWEGRDFSS/hg
</string>
<string name="8lqCQqn73Uo84Rj">k73tlfVNYsPshll19ztma7U>
</map>
密鑰
下一個問題就是將你加密算法的密鑰存放在哪里。這是一個沒有最佳答案的通用問題。不管你將密鑰存放在哪里,攻擊者總是能夠找到它。唯一能夠阻止攻擊者破解應用的就是加密算法的復雜性和尋找密鑰所需要的時間。

你可以使用KeyStore,但是只有在Android API 18以后的版本才支持。另外一個問題還是ROOT,獲取到ROOT權限的攻擊者可以獲取到任何數據。
或者使用Java Native Interface(JNI)。反編譯C/C++編譯出來的代碼要困難的多。Jadex和dex2jar這種面向Java語言的反編譯器這時就無法工作了。
還有一個可能的辦法就是使用隱寫術算法,然后將密鑰藏在任何你想藏的地方。例如:你可以將圖片中的某些bit替換為你的密鑰的bit。
上面描述的所有方法都可以為你的應用的“健康”增加時間。采取一些措施總比什么都不做要好。
結論
關于怎樣隱藏或者加密密鑰或者數據的算法數以萬計。即使最好的、最變態(tài)的算法也無法保護你的數據。
最好的辦法是不存儲任何數據,但是有時是不可能的。
其他建議
關于增加應用安全性還有這些建議:
模擬器
檢查你的應用是否正在模擬器上運行。你可以使用android.os包中的Build類來檢查。它可以提供很多運行當前應用的設備的信息。如果有興趣可以戳這里了解更多信息。
Debug
檢查當前設備是否有調試器連接。最簡單的解決辦法是:
Debug.isDebuggerConnected()
Root
檢查設備是否被root。你可以通過允許你使用終端的程序來檢查設備是否已經被root。來看一個例子:
private static boolean isRooted() {
return findBinary("su");
}
public static boolean findBinary(String binaryName) {
boolean found = false;
if (!found) {
String[] places = {"/sbin/", "/system/bin/",
"/system/xbin/", "/data/local/xbin/",
"/data/local/bin/", "/system/sd/xbin/",
"/system/bin/failsafe/", "/data/local/"};
for (String where : places) {
if (new File(where + binaryName).exists()) {
found = true;
break;
}
}
}
return found;
}
這段代碼摘自RootTools這個庫。
su命令常常用于將當前用戶的權限修改為root用戶。戳這里可以了解更多關于su的信息。
一旦這個檢測到設備已經被root,通知服務器,暫停連接,中斷服務。但是不要做的太過,以至損害用戶體驗,造成用戶流失。
字符串
使用字符數組而不是使用字符串,或者使用 new 操作符來創(chuàng)建一個字符串,這樣新創(chuàng)建的字符串是在堆(heap)里而不是在字符串池(string pool)里。
String a = new String("a");
a = new String("a");
在上面的例子中會創(chuàng)建兩個對象。在第二行代碼被執(zhí)行后,垃圾回收器(GC)就會回收第一個對象。
String a = "a";
String b = "a";
在這個例子中,只會創(chuàng)建一個對象。因為使用了字符串,對象會保存在字符串池(string pool)中,垃圾回收器暫時還不會回收它。
混淆
使用ProGuard,DexGuard或者DexProtector等混淆器。
ProGuard是Android Studio默認的代碼精簡工具,它也可以混淆你的代碼。在創(chuàng)建新的Android Studio工程時會自動生成一個ProGuard配置文件。在你的工程中是以proguard-rules.pro命名的。
在這個文件中你可以指定編譯APK時需要執(zhí)行的規(guī)則。你也可以在這里發(fā)現(xiàn)流行庫所使用的一些規(guī)則。
開啟ProGuard你需要這樣做:
android {
buildTypes {
dev {
minifyEnabled true // enables ProGuard
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
使用混淆你可以讓攻擊者困惑從而花更多的時間來破解你的應用。
有許多公司提供混淆工具。這里有一份最流行的混淆器的清單。其中最好的那個相當的貴。它們能把你的應用的代碼邏輯轉換得晦澀難懂。不僅如此,花更多的錢你還可以開啟修改root/debug/emulator/MITM/Code等選項的功能。
一些有用的鏈接
總結
我盡力在這篇8分鐘長的文章里提供大量的信息。希望你能有所收獲。并不是所有的方式你都需要嚴格遵循,但是你可以嘗試一些新的東西(或者不是新的 :))。
關于Android安全再啰嗦兩句。在攻擊者獲得ROOT權限之前,每一條建議都很有用。一旦ROOT權限擊潰你所有的保護,再無回天之力。希望未來發(fā)布的Android系統(tǒng)會更加安全。
如果你有任何問題、建議或者你知道某一領域我可以研究的都請告訴我。謝謝!