- 原文地址:A follow-up on how to store tokens securely in Android
- 原文做者:Enrique López Mañas
- 譯文出自:掘金翻譯計劃
- 譯者: lovexiaov
- 校對者:luoqiuyu hackerkevin
做爲本文的序言,我想對讀者作一個簡短的聲明。下面的引言對本文的後續內容而言十分重要。html
沒有絕對的安全。所謂的安全是指利用一系列措施的堆積和組合,來試圖延緩必然發生的事情。前端
大約 3 年前,我寫了一篇文章,給出了幾種方法來防止潛在攻擊者反編譯咱們 Android 應用竊取字符串令牌。爲了便於回憶,也爲了防止不可避免的網絡癱瘓,我將會在此從新列出一些章節。java
客戶端應用與服務端的交互是最多見的場景之一。數據交換時的敏感度差異很大,而且登陸請求、用戶數據更改請求等之間交換的數據類型也變化多樣。react
首先要提到並應用的技術是使用 SSL(安全套接層)連接客戶端與服務端。再看一下文章開頭的引言。儘管這樣作是一個良好的開端,但這並不能確保絕對的隱私和安全。android
當你使用 SSL 鏈接時(也就是當你看到瀏覽器上有一個小鎖時),這意味着你與服務器之間的鏈接被加密了。理論上講,沒有什麼可以訪問到你請求裏的信息(*)ios
(*)我說過絕對的安全不存在吧?SSL 鏈接仍然能夠被攻破。本文不打算提供全部可能的攻擊手段列表,只想讓你瞭解幾種攻擊的可能性。好比,能夠僞造 SSL 證書,或者進行中間人攻擊。git
咱們繼續。假設客戶端正在經過加密的 SSL 通道與後臺連接,它們在愉快的交換有用的數據,執行業務邏輯。可是咱們還想提供一個額外的安全層。github
接下來要採起的措施是在通訊中使用受權令牌或 API 密鑰。當後臺收到一個請求時,咱們如何判斷該請求是來自認證的客戶端而不是任意一個想要獲取咱們 API 數據的傢伙?後臺會檢查該客戶端是否提供了一個有效的 API 密鑰。若是密鑰有效,則執行請求操做,不然拒絕該請求並根據業務需求採起一些措施(當出現此狀況時,我通常會紀錄他們的 IP 地址和客戶端 ID,看一下他們的訪問頻率。若是頻率高於個人忍受範圍,我會考慮禁止並觀察一下這個無禮的傢伙想要獲得什麼)。後端
讓咱們從頭開始構建咱們的城堡吧。在咱們的應用中,添加一個叫作 API_KEY 的變量,該變量會自動注入到每次的請求(若是是 Android 應用,可能會是你的 Retrofit 客戶端)中。瀏覽器
private final static String API_KEY = 「67a5af7f89ah3katf7m20fdj202」複製代碼
很好,這樣能夠幫助咱們鑑定客戶端。但問題在於它自己並無提供一個十分有效的安全保證。
若是你使用 apktool 反編譯該應用,而後搜索該字符串,你會在其中一個 .smali 文件中發現:
const-string v1, 「67a5af7f89ah3katf7m20fdj202」複製代碼
是的,我知道。這並不能保證是一個有效的令牌,因此咱們仍然須要經過一個精確的驗證來決定如何找到那個字符串,和它是否能夠用來經過驗證。可是你知道我要表達什麼:這一般只是時間和資源的問題。
Proguard 是否會能咱們保證該字符串的安全呢?並不能。Proguard 在常見問題中提到了字符串的加密是徹底不可能的。
那將字符串保存到 Android 提供的其餘存儲機制中呢,好比說 SharedPreferences?這並非一個好方法。在模擬器或者 root 過的設備中能夠輕易的訪問到 SharedPreferences。幾年前,一個叫 Srinivas 的夥計向咱們證實瞭如何更改一個視頻遊戲中的得分。跑題了!
我將會更新我提出的初始模型,不斷迭代它,以提供更安全的替代方案。咱們假設有兩個函數分別負責加密和解密數據:
private static byte[] encrypt(byte[] raw, byte[] clear) throws Exception {
SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.ENCRYPT_MODE, skeySpec);
byte[] encrypted = cipher.doFinal(clear);
return encrypted;
}
private static byte[] decrypt(byte[] raw, byte[] encrypted) throws Exception {
SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.DECRYPT_MODE, skeySpec);
byte[] decrypted = cipher.doFinal(encrypted);
return decrypted;
}複製代碼
代碼沒啥好說的。這兩個函數會使用一個密鑰值和一個被用來編/解碼的字符串做爲入參。它們會返回相應的加密或解密過的字符串。咱們會用以下方式調用它們:
ByteArrayOutputStream baos = new ByteArrayOutputStream();
bm.compress(Bitmap.CompressFormat.PNG, 100, baos);
byte[] b = baos.toByteArray();
byte[] keyStart = "encryption key".getBytes();
KeyGenerator kgen = KeyGenerator.getInstance("AES");
SecureRandom sr = SecureRandom.getInstance("SHA1PRNG");
sr.setSeed(keyStart);
kgen.init(128, sr);
SecretKey skey = kgen.generateKey();
byte[] key = skey.getEncoded();
// encrypt
byte[] encryptedData = encrypt(key,b);
// decrypt
byte[] decryptedData = decrypt(key,encryptedData);複製代碼
猜到爲何要這麼作了嗎?是的,咱們能夠根據需求來加/解密令牌。這就爲咱們提供了一個額外的安全層:當代碼混淆後,尋找令牌再也不像執行字符串搜索和檢查字符串周圍的環境那樣簡單了。可是,你能指出還有一個須要解決的問題嗎?
找到了嗎?
若是還沒找到就多花點時間。
是的。咱們仍然有一個加密密鑰以字符串的形式存儲。雖然這種隱晦的作法增長了更多的安全層,但無論這個令牌是用於加密或它自己就是一個令牌,咱們仍然有一個以明文形式存在的令牌。
如今,咱們將使用 NDK 來繼續迭代咱們的安全機制。
NDK 容許咱們在 Android 代碼中訪問 C++ 代碼庫。首先咱們來想一下要作什麼。咱們能夠在一個 C++ 函數中存放 API 密鑰或者敏感數據。該函數能夠在以後的代碼中調用,避免了在 Java 文件中存儲字符串。這就提供了一個自動的保護機制來防止反編譯技術。
C++ 函數以下:
Java_com_example_exampleApp_ExampleClass_getSecretKey( JNIEnv* env,
jobject thiz )
{
return (*env)->NewStringUTF(env, "mySecretKey".");
}複製代碼
在 Java 代碼中調用它也很簡單:
static {
System.loadLibrary("library-name");
}
public native String getSecretKey();複製代碼
在加/解密函數中會這樣調用:
byte[] keyStart = getSecretKey().getBytes();複製代碼
此時咱們生成 APK,混淆它,而後反編譯並嘗試在原生函數 getSecretKey() 中查找該字符串,沒法找到!勝利了嗎?
並無!NDK 代碼其實也能夠被反彙編和檢查。只是難度較高,須要更高級的工具和技術。雖然這樣能夠擺脫掉 95% 的腳本小子,但一個有充足資源和動機的團隊讓然能夠拿到令牌。還記得這句話嗎?
沒有絕對的安全。所謂的安全是指利用一系列措施的堆積和組合,來試圖延緩必然發生的事情。
你仍然能夠在反彙編代碼中找到該字符串字面值。Hex Rays 在反編譯原生文件方面就作的很好。我很確信有一大堆的工具能夠解構 Android 生成的任意原生代碼(我跟 Hex Rays 並無關係,也沒有從他們那裏拿到任何形式的資金酬勞)。
那麼,咱們要使用哪一種方案來避免後臺與客戶端的通訊被標記呢?
在設備上實時生成密鑰。
你的設備不須要存儲任何形式的密鑰並處理各類保護字符串字面值的麻煩!這是在服務中用到的很是古老的技術,好比遠程密鑰驗證。
抓到重點了嗎?爲何不使用返回三個隨機素數( 1~100 之間)之和的函數來代替返回一個字符串(很容易被識別)的原生函數呢?或者拿到當天的 UNIX 時間,而後給每一位數字加 1?經過設備的一些上下文相關信息(如正在使用的內存量)來提供一個更高程度的熵值?
上面這段包含了一些想法,但願讀者們已經獲得重點了。
還記得開頭的那段話吧?
沒有絕對的安全。所謂的安全是指利用一系列措施的堆積和組合,來試圖延緩必然發生的事情。
我想再強調一次,你的目標是儘量的保護你的代碼,同時不要忘記 100% 的安全是不可能的。可是,若是你能保證解密你代碼中任意的敏感信息都須要耗費大量的資源,你就能安心睡覺啦。
我知道,讀到此處,縱觀整文,你會納悶「這傢伙怎麼講了全部麻煩的方法而沒有提到 Dexguard 呢?」。是的 Dexguard 能夠混淆字符串,他們在這方面作的很好。然而 Dexguard 的售價讓人望而卻步。我在以前的公司的關鍵安全系統中使用過 Dexguard,但這也許並非一個適合全部人的選擇。再說了,像生活同樣,在軟件開發中選擇越多世界越豐富多彩。
愉快的編碼吧!
我會在 Twitter 上寫一些關於軟件工程和生活點滴的思考。若是你喜歡此文,或者它能幫到你,請隨意分享,點贊或者留言。這是業餘做者寫做的動力。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、React、前端、後端、產品、設計 等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃。