07/09/2018, 15:49

Các thủ thuật để giấu secret key khỏi source control và ứng dụng với CI trong lập trình Android

Xin chào các bạn. Chắc hẳn mỗi chúng ta đều đã từng phát triển app sử dụng API của bên thứ 3, và chắc mọi người đều biết là hầu hết các API service đều yêu cầu chúng ta phải có 1 API key (secret token) để verify nguồn gốc của các request. Vậy thì tại sao chúng ta lại phải "giấu" API key này khỏi ...

Xin chào các bạn. Chắc hẳn mỗi chúng ta đều đã từng phát triển app sử dụng API của bên thứ 3, và chắc mọi người đều biết là hầu hết các API service đều yêu cầu chúng ta phải có 1 API key (secret token) để verify nguồn gốc của các request. Vậy thì tại sao chúng ta lại phải "giấu" API key này khỏi các VCS như git? Một trong những lý do quan trọng nhất chính là việc những người khác sẽ có toàn quyền sử dụng API dưới danh nghĩa của bạn, và thậm chí là họ có thể chỉnh sửa các dữ liệu bí mật hay violate các rule của service. Lí do thứ 2 là hầu hết các free service đều áp đặt 1 rate limit lên request; ví dụ 1 API key chỉ được phép thực hiện 60 request trong 1 phút, nên việc những người phát triển app của bạn cùng sử dụng 1 API key sẽ làm cho số request này hết rất nhanh và hiệu suất làm việc sẽ bị giảm đi đáng kể. Ở bài này chúng ta sẽ thảo luận về các cách để "giấu" API key mà không làm hỏng cấu trúc của ứng dụng khi làm việc theo team trên git, cũng như cách để áp dụng các thủ thuật này với các CI như Travis hay Circle.

Cách thường dùng

Cách phổ biến nhất để giấu API key chính là cho nó làm XML resource và đưa vào .gitignore:

  1. Đầu tiên chúng ta tạo 1 file XML mới, tạm đặt tên là keys.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="api_key">5093450734057234872347234</string>
</resources>
  1. Đọc giá trị của key khi app chạy:
String apiKey = getString(R.string.api_key);
  1. Cho path của file này vào .gitignore để git không track nó.

  2. Chỉnh sửa file Readme để hướng dẫn mọi người làm theo các bước trên.

Nhìn thì khá là đơn giản, nhưng cách này có rất nhiều nhược điểm:

  • Rắc rối để chỉnh sửa key.
  • Cần cho ra file riêng với tên chính xác là keys.xml và path chính xác để .gitignore hoạt động đúng. Ngoài ra còn cần thêm thuộc tính translatable="false" khi sử dụng nhiều ngôn ngữ.
  • Sẽ không thể build pass CI vì lỗi thiếu resource.

Cách tốt hơn: Sử dụng gradle.properties

gradle.properties là file chứa các setting để configure build environment của Gradle. File này sẽ được đọc ra khi gradle build project của bạn. Chúng ta sẽ lợi dụng điều này để định nghĩa các API key:

  1. Thêm API key vào gradle.properties:
API_KEY=5093450734057234872347234
  1. Gán giá trị vào biến trong file build.gradle của module (app/build.gradle)
android {
    defaultConfig {
        buildConfigField "String", "API_KEY", ""${API_KEY}""
        //Lí do chúng ta phải sử dụng  là để kèm escape character " " vào chuỗi String
    }
}

Khi build project, gradle sẽ tự generate ra 1 file tên BuildConfig.java với các thông tin của build.gradle:

public final class BuildConfig {
    public static final String API_KEY = "5093450734057234872347234";
}
  1. Sử dụng trong Java:
String apiKey = BuildConfig.API_KEY;
  1. Chúng ta cần commit file này lên git, vì thế đầu tiên chúng ta phải "giấu" key thật đi như sau:

gradle.properties

API_KEY=INSERT_YOUR_API_KEY_HERE 
  1. git add và git commit như bình thường.

  2. Sử dụng lệnh git update-index --skip-worktree gradle.properties . Về cơ bản thì lệnh này sẽ nói với git là "Đừng quan tâm đến bất kì thay đổi nào trong file gradle.properties, luôn luôn sử dụng file ở local và không pull những thay đổi từ upstream về đối với file này".

  3. Sửa lại gradle.properties để dùng lại API thực của chúng ta:

API_KEY=5093450734057234872347234

Sau đó bạn thử dùng git status xem, git sẽ nói là không có bất kì thay đổi nào ở local :) đó là lí do chúng ta dùng lệnh skip-worktree ở bước 6, từ giờ git sẽ coi như file gradle.properties không tồn tại.

  1. Edit README và hướng dẫn teammate thay API key của họ vào gradle.properties.

Cách này tốt hơn cách trên bởi vì chúng ta có 1 nơi quy định để thay đổi API key. Hơn thế nữa là chúng ta có thể sử dụng key này ở bất cứ đâu trong app mà không cần context để getString(), bởi vì key này sẽ được generate ra như 1 constant. CI cũng sẽ build thành công bởi vì lí do như trên.

A more interesting situation

Tôi muốn chia sẻ cho các bạn 1 vấn đề mà tôi mới gặp gần đây. Tôi muốn tích hợp Fabric vào ứng dụng của mình để sử dụng Crashlytics. Như thường lệ thì Fabric yêu cầu chúng ta phải có 1 API key và 1 API secret để nó có thể verify account. Ban đầu tôi cũng nghĩ chúng ta có thể sử dụng cách trên để đưa những key này vào gradle.properties, nhưng không, Fabric yêu cầu chúng ta cung cấp key dưới 1 trong 2 kiểu sau:

  1. Đặt key vào manifest
      <meta-data
          android:name="io.fabric.ApiKey"
          android:value="io34jnto3i4nt34it3049g394g"
      />
  1. Tạo file fabric.properties trong module app (app/fabric.properties) với nội dung sau:
apiSecret=09df09bdfvskdvmp3olmr32pmưlkmvv
apiKey=io34jnto3i4nt34it3049g394g

Với cách #1 thì về cơ bản chúng ta vẫn làm theo các bước để thêm API vào gradle.properties, nhưng bây giờ chúng ta sẽ phải sử dụng placeholder để file manifest có thể đọc ra giá trị từ build.gradle như sau:

app/build.gradle

android {
    defaultConfig {
        manifestPlaceholders = [ apiKey:API_KEY ]
    }
}

AndroidManifest.xml

<meta-data
    android:name="io.fabric.ApiKey"
    android:value="${apiKey}"
/>

Nếu không muốn sử dụng cách này thì chúng ta có thể chuyển sang cách 2. Với cách #2 thì chúng ta sẽ sử dụng gradle task để generate ra file fabric.properties khi compile project:

  1. Thực hiện các bước để thêm API key và API secret vào gradle.properties như hướng dẫn ở trên.

  2. Gradle lifecycle bao gồm 2 phase chính là configuration phase và execution phase. Trong configuration phase, các setting và task sẽ được thực thi, và build sẽ được chạy trong execution phase. Lợi dụng điểm này, chúng ta sẽ sử dụng hàm afterEvaluate vốn sẽ được gọi khi configuration phase kết thúc để tạo ra file fabric.properties với những thông tin cần thiết.

app/build.gradle

afterEvaluate {
    //generate a file named fabric.properties with the api key and api secret when the project is being compiled
    //edit gradle.properties to include your api key and secret under the name API_KEY and API_SECRET, respectively
    initFabricProperties()
}

def initFabricProperties() {
    def propertiesFile = file('fabric.properties')
    if (!propertiesFile.exists()) {
        def commentMessage = "This is autogenerated fabric property from system environment to prevent key from being committed to source control."
        ant.propertyfile(file: "fabric.properties", comment: commentMessage) {
            entry(key: "apiSecret", value: API_SECRET)
            entry(key: "apiKey", value: API_KEY)
        }
    }
}
  1. Chạy app. Bạn sẽ thấy là mặc dù chúng ta không có key trong manifest nhưng project sẽ vẫn compile và file fabric.properties sẽ được tạo ra dưới đường dẫn app/fabric.properties. Trong file này bạn sẽ thấy apiKey và apiSecret được định nghĩa.

  2. Cho app/fabric.properties vào .gitignore để git không track file này.

Ứng dụng với CI

Trong bài viết này tôi sẽ nói về cách để chúng ta có thể build thành công project trên Travis CI mà không cung cấp API key lên git. Travis CI là 1 trong những CI service nổi tiếng và được sử dụng nhiều nhất vì nó rất dễ để cài đặt. Nếu bạn chưa biết Travis là gì thì hãy search Google nhé.

Tiếp tục vấn đề ở trên. Sau khi tôi đã tích hợp Fabric vào app, Travis sẽ không thể build thành công project. Tại sao lại như vậy, đó là do Fabric sẽ check key valid trong khi compile project, nếu key không đúng thì project sẽ không thể build được. Travis không hề biết về thông tin API key hay API secret vì tôi đã giấu nó khỏi Github.

Cách giải quyết vấn đề này thật ra khá đơn giản. Travis hỗ trợ chúng ta trong việc định nghĩa các environment variable để sử dụng khi build:

alt text

Ở đây chúng ta sẽ định nghĩa API key và API secret với prefix là ORG_GRADLE_PROJECT_ + tên của biến. Sở dĩ chúng ta cần prefix này là bởi vì Gradle sẽ đọc ra từ environment variable những biến bắt đầu bằng prefix đó và sẽ add những biến đó vào properties của project của bạn (cũng giống như việc chúng ta thêm vào file gradle.properties vậy).

Ở cột Name, thêm ORG_GRADLE_PROJECT_API_SECRET và thêm key vào cột Value, sau đó ấn add. Làm tương tự với API_SECRET. Sau khi xong thì hãy thử restart build, Travis chắc chắn sẽ build thành công :)

Đây là 1 project tôi đã thử và thành công, các bạn có thể tham khảo tại https://github.com/b1acKr0se/bridddle-for-dribbble

Hi vọng các thủ thuật này sẽ giúp team của bạn cooperate tốt hơn trong việc phát triển ứng dụng cũng như đóng góp cho các open source project.

0