Android中儲存靜態祕鑰(加密祕鑰,特殊id欄位等)
如何在App中儲存靜態祕鑰以及保證其安全性。許多的移動app需要在app端儲存一些靜態字串常量,其可能是靜態祕鑰、第三方appId等。在儲存這些字串常量的時候就涉及到了如何保證祕鑰的安全性問題。如何保證在App中靜態祕鑰唯一且正確安全,這是一個很重要的問題,公司的產品中就存在著靜態字串常量型別的祕鑰,所以一個明顯的問題就是如何生成祕鑰,保證祕鑰的安全性?
-
通過SharedPreferences儲存靜態祕鑰;
-
通過Java硬編碼的方式儲存
-
通過NDK的方式,將靜態祕鑰儲存在so檔案中;
幾種儲存靜態祕鑰方式的優劣勢:
金鑰直接明文存在sharedprefs檔案中,這是最不安全的。
金鑰直接硬編碼在Java程式碼中,這很不安全,dex檔案很容易被逆向成java程式碼。
將金鑰分成不同的幾段,有的儲存在檔案中、有的儲存在程式碼中,最後將他們拼接起來,可以將整個操作寫的很複雜,這因為還是在java層,逆向者只要花點時間,也很容易被逆向。
用ndk開發,將金鑰放在so檔案,加密解密操作都在so檔案裡,這從一定程度上提高了的安全性,擋住了一些逆向者,但是有經驗的逆向者還是會使用IDA破解的。
在so檔案中不儲存金鑰,so檔案中對金鑰進行加解密操作,將金鑰加密後的金鑰命名為其他普通檔案,存放在assets目錄下或者其他目錄下,接著在so檔案裡面新增無關程式碼(花指令),雖然可以增加靜態分析難度,但是可以使用動態調式的方法,追蹤加密解密函式,也可以查詢到金鑰內容。
可以說在裝置上安全儲存金鑰這個基本無解,只能選擇增大逆向成本。而要是普通開發者的話,這需要耗費很大的心血,要評估你的app應用的重要程度來選擇相應的技術方案。
產品App中需要儲存的祕鑰:
由於app需要與伺服器互動所以這時候若使用者為登入則客戶端需要一個預設的祕鑰來確認App的遊客身份,因此需要在app中儲存一個預設的請求祕鑰。
-
當用戶未登入或者是退出登入時,請求伺服器使用預設的祕鑰字串;
-
當用戶登入時使用從伺服器或其的祕鑰字串;
這樣我們需要在App端儲存一個預設的祕鑰字串用於標識使用者的遊客身份,而一個問題就是如何保證祕鑰字串的安全性?
考慮到的其他幾種儲存祕鑰方式:
-
通過儲存檔案的方式儲存祕鑰資訊;
-
通過資料庫的方式儲存祕鑰資訊;
-
通過配置gradle的方式儲存祕鑰資訊;
-
通過配置string.xml的方式儲存祕鑰資訊;
檔案方式:顯而易見的通過儲存檔案的方式儲存祕鑰資訊,有一個問題就是,如果使用者惡意刪除App儲存的祕鑰資訊,那麼App就無法使用預設的祕鑰資訊了,這樣祕鑰的唯一性也就無法保證。
資料庫方式:和通過檔案的方式儲存祕鑰資訊一樣,通過資料庫儲存也是無法保證惡意使用者刪除資料庫資訊,這樣我們的App就丟失了預設的祕鑰資訊。
gradle配置:我們通過下面的gradle配置變數的方式,來講解如何在gradle配置變數資訊。
string.xml:在下面我們做詳細的介紹。
通過gradle配置變數:
defaultConfig {
buildConfigField "String", "appKey", "\"C046B2DC\""
applicationId "com.test.demo"
minSdkVersion 15
targetSdkVersion 22
versionCode 1
versionName "1.0"
ndk {
abiFilters 'armeabi' //, 'x86', 'armeabi-v7a', 'x86_64', 'arm64-v8a'
}
}
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
在Android gradle中我們不單可以引用依賴包,執行指令碼還可以配置靜態變數:
buildTypes {
debug {
// 顯示Log
buildConfigField "boolean", "LOG_DEBUG", "true"
buildConfigField "String", "appKeyPre", "\"xxx\""
//混淆
minifyEnabled false
//Zipalign優化
zipAlignEnabled true
// 移除無用的resource檔案
shrinkResources true
//載入預設混淆配置檔案
proguardFiles getDefaultProguardFile('proguard-Android.txt'), 'proguard-rules.pro'
//簽名
signingConfig signingConfigs.debug
}
release {
// 不顯示Log
buildConfigField "boolean", "LOG_DEBUG", "false"
buildConfigField "String", "appKeyPre", "\"xxx\""
//混淆
minifyEnabled true
//Zipalign優化
zipAlignEnabled true
// 移除無用的resource檔案
shrinkResources true
//載入預設混淆配置檔案
proguardFiles getDefaultProguardFile('proguard-Android.txt'), 'proguard-rules.pro'
//簽名
signingConfig signingConfigs.relealse
}
}
如上面程式碼所示我們在gradle中配置了一個名稱為appKey的字串變數,編譯gradle則會在gradle的編譯類:BuildConfig中生成該靜態變數:
/**
* gradle編譯後生成的編譯類
*/
public final class BuildConfig {
public static final boolean DEBUG = Boolean.parseBoolean("true");
public static final String APPLICATION_ID = "com.sample.renter";
public static final String BUILD_TYPE = "debug";
public static final String FLAVOR = "internal";
public static final int VERSION_CODE = 1;
public static final String VERSION_NAME = "1.0.0";
// Fields from build type: debug
public static final boolean LOG_DEBUG = true;
public static final String appKey = "xxx";
}
可以發現通過配置gradle的方式配置靜態祕鑰反編譯的時候也是找到祕鑰的,但是增加了逆向的難度,而且避免了被使用者的惡意刪除,所以通過gradle配置字串的方式儲存app中的靜態祕鑰是一個不錯的選擇。
通過string.xml配置祕鑰資訊
通過string.xml配置祕鑰資訊也是一個不錯的選擇。在string.xml中定義字串值之後,通過程式碼獲取反編譯apk效果如下:
可以發現這裡無法顯式的展示出string字串值,但是這裡出現了string字串的ID值,但是需要說明的是惡意攻擊是可以通過string的ID之在R.java檔案中查詢到相應的string名稱,進而在string.xml中找到字串值。但是這樣也是增加了反編譯的難度,相對來說也是一個比較不錯的選擇。
產品中儲存靜態祕鑰實踐:
最終經過比對各種靜態祕鑰儲存方案,我們決定使用gradle配置 + 靜態程式碼 + 字串運算 + string.xml值的方式實現靜態祕鑰的儲存。
首先將靜態祕鑰分為四部分:
-
第一部分通過gradle配置的方式儲存;
-
第二部分通過java硬編碼的方式儲存;
-
第三部分通過java字串拼接運算的方式儲存;
-
第四部分通過string.xml儲存;
獲取appKey第一部分字串:
通過gradle配置的方式我們上面已經做了介紹,其就是在mudle中的gradle檔案中再起buildType節點下定義字串變數,這裡需要注意的是若是有正式環境和測試環境之分,需要分別定義字串變數,這樣我們就可以通過BuildConfig獲取appKay第一部分的字串了。
/**
* 獲取AppKey part1
*/
public static String getBK1() {
return BuildConfig.appKey;
}
獲取appKey第二部分字串:
第二部分的appKay字串是通過運算的出來的,這裡的運算可以是任意運算方式,越複雜越好,越讓人看不懂越好,當然結果需要時唯一的。比如:
public static StringBuffer getBk2() {
StringBuffer sb = new StringBuffer();
sb.append(Config.getGBS(2, 5));
return sb;
}
而這裡的getGBS方法的實現:
public static int getGBS(int x, int y){
for(int i = 1; i<= x * y; i++){
if(i % x == 0 && i % y == 0)
return i;
}
return x * y;
}
最終的結果返回是:10,當然了不同的字串需要不同的演算法;
獲取appKey第三部分字串:
這裡就只是使用了簡單的字串硬編碼
public static String getBK3() {
return "xhxh";
}
獲取appKey第四部分字串
- 在string.xml中定義appKey的第四部分
<string name="bk4">chs</string>
- 在程式碼中獲取string中定義的字串值
public static String getBk4() { mContext().getResources().getString(R.string.bk4); }
- 獲取最終的appKey字串:
/** * 獲取最終的appKey字串 */ public static byte[] getDefaultKey() { StringBuffer sb = new StringBuffer(); sb.append(getBK1()).append(getBK2()).append(getBk3()).append(getBk4()); return sb.toString(); }
- 這樣經過一系列的操作之後我們就獲取到了最終的靜態祕鑰。當然了產品中最好實現了程式碼混淆的功能,這樣也能增大逆向的難度。
總結:
-
在App端儲存靜態祕鑰可以通過SharedPreferences、java硬編碼,ndk中的so檔案,檔案,資料庫,gradle配置的方式實現;
-
為了保證祕鑰的安全性可以採用多種方式混合,這樣可以增加惡意反編譯的難度;
-
在App端儲存祕鑰不能真正的保證祕鑰的安全性,只能增加反編譯的難易程度;
-
可以使用gradle配置的方式配置靜態祕鑰,使用string.xml配置祕鑰增加逆向反編譯的難度;
-
不推薦使用檔案,資料庫的方式儲存靜態祕鑰,容易被使用者惡意刪除,而出現不可預知的錯誤;