Android產品研發(二十三)-->Android中保存靜態祕鑰實踐

轉載請標明出處:一片楓葉的專欄

上一篇文章中我們講解了Android中的實用調試技巧。講解了Android中的原生Log API以及其使用方式,講解了自定義日誌API、使用方式和實現原理,講解了通過gradle配置日誌框架在正式環境中屏蔽日誌信息等。最後我們還重點講解了Android studio中的斷點調試技巧,主要包括:斷點調試功能、日誌斷點、求值調試、異常斷點、方法斷點等。更多關於Android中實用調試技巧的知識,可以參考我的:Android產品研發(二十二)–>Android實用調試技巧

本文我們將講解一個Android產品研發中可能會碰到的一個問題:如何在App中保存靜態祕鑰以及保證其安全性。許多的移動app需要在app端保存一些靜態字符串常量,其可能是靜態祕鑰、第三方appId等。在保存這些字符串常量的時候就涉及到了如何保證祕鑰的安全性問題。如何保證在App中靜態祕鑰唯一且正確安全,這是一個很重要的問題,公司的產品中就存在着靜態字符串常量類型的祕鑰,所以一個明顯的問題就是如何生成祕鑰,保證祕鑰的安全性?

現今保存靜態祕鑰的幾種主流通用做法:(參考:Android安全開發之淺談密鑰硬編碼

  • 通過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配置變量:

在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配置祕鑰增加逆向反編譯的難度;

  • 不推薦使用文件,數據庫的方式保存靜態祕鑰,容易被用戶惡意刪除,而出現不可預知的錯誤;


另外對產品研發技術,技巧,實踐方面感興趣的同學可以參考我的:
Android產品研發(十二)–>App長連接實現
Android產品研發(十三)–>App輪訓操作
Android產品研發(十四)–>App升級與更新
Android產品研發(十五)–>內存對象序列化
Android產品研發(十六)–>開發者選項
Android產品研發(十七)–>Hybrid開發
Android產品研發(十八)–>webview問題集錦
Android產品研發(十九)–>Android studio中的單元測試
Android產品研發(二十)–>代碼Review
Android產品研發(二十一)–>Android中的UI優化
Android產品研發(二十二)–>Android實用調試技巧


本文以同步至github中:https://github.com/yipianfengye/AndroidProject,歡迎star和follow


發佈了125 篇原創文章 · 獲贊 932 · 訪問量 149萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章