Android KeyStore 系統

您可以使用 Android KeyStore 系統將加密編譯金鑰儲存至容器,這樣裝置中的金鑰就更不容易遭到擷取。金鑰儲存在 KeyStore 後,您就能使用金鑰執行加密編譯作業,金鑰內容也同樣不會開放匯出。此外,KeyStore 系統可用來限制金鑰的使用時機和方式,例如規定須驗證使用者才能使用金鑰,或是限制金鑰只能用於特定加密編譯模式。詳情請參閱「安全防護功能」一節。

Android 4.0 (API 級別 14) 推出的 KeyChain API,以及 Android 4.3 (API 級別 18) 推出的 Android KeyStore 供應器功能,皆使用 KeyStore 系統。本文說明 Android KeyStore 系統的使用時機和方式。

安全防護功能

Android KeyStore 系統提供兩種做法,用來防止金鑰內容在未經授權的情況下遭到使用。第一,KeyStore 系統禁止從應用程式程序和整部 Android 裝置擷取金鑰內容,藉此降低金鑰內容在 Android 裝置「外部」遭到未經授權使用的風險。第二,KeyStore 系統會讓應用程式指定金鑰的授權用途,然後在應用程式程序外部強制執行這些限制,進而降低金鑰內容在 Android 裝置「內部」遭到未經授權使用的風險。

擷取預防措施

系統採取以下兩種安全措施,防止 Android KeyStore 金鑰內容遭到擷取:

  • 金鑰內容一律不會進入應用程式程序。應用程式使用 Android KeyStore 金鑰執行加密編譯作業時,幕後的明文、密文和要簽署或驗證的訊息都會傳送至執行加密編譯作業的系統程序。如果應用程式程序遭到入侵,攻擊者或許可以使用應用程式的金鑰,但無法擷取金鑰內容,例如無法用於 Android 裝置以外的環境。
  • 金鑰內容可以繫結至 Android 裝置的安全硬體,例如受信任的執行環境 (TEE) 或安全元件 (SE)。為金鑰啟用這項功能後,金鑰內容絕不會洩漏至安全硬體外。如果 Android 作業系統遭到入侵,或是攻擊者可以讀取裝置的內部儲存空間,攻擊者或許就能在 Android 裝置上使用任何應用程式的 Android KeyStore 金鑰,但無法從裝置擷取這些金鑰。裝置安全硬體必須支援金鑰演算法、區塊模式、填充配置的特定組合,並摘要授權使用的金鑰,才能啟用這項功能。

    如要檢查是否已為金鑰啟用這項功能,請取得金鑰的 KeyInfo。下一個步驟取決於應用程式的目標 SDK 版本:

    • 如果應用程式指定 Android 10 (API 級別 29) 以上版本,請檢查 getSecurityLevel() 的回傳值。若回傳值符合 KeyProperties.SecurityLevelEnum.TRUSTED_ENVIRONMENTKeyProperties.SecurityLevelEnum.STRONGBOX,表示金鑰位於安全硬體中。
    • 如果應用程式指定 Android 9 (API 級別 28) 以下版本,請檢查 KeyInfo.isInsideSecurityHardware() 的布林回傳值。

硬體安全性模組

搭載 Android 9 (API 級別 28) 以上版本的支援裝置可具有 StrongBox Keymaster,這是 Keymaster 或 Keymint HAL 的實作項目,位於類似硬體安全性模組的安全元件中。雖然硬體安全性模組可能會參照許多不同的金鑰儲存空間實作項目 (例如 TEE),確保在 Linux kernel 遭到入侵時不會洩漏金鑰,但 StrongBox 會明確參照裝置,如內嵌安全元件 (eSE) 或 SoC 端安全處理器 (iSE)。

模組包含以下內容:

  • 專屬 CPU
  • 安全儲存空間
  • 真正的隨機號碼產生器
  • 防止套件遭到竄改和未經授權側載應用程式的其他機制
  • 安全計時器
  • 重新啟動通知引腳 (或同等功能),例如通用型輸入輸出 (GPIO)

為支援低功耗 StrongBox 實作項目,系統支援部分演算法和金鑰大小:

  • RSA 2048
  • AES 128 和 256
  • ECDSA、ECDH P-256
  • HMAC-SHA256 (支援 8 到 64 (含) 個位元組之間的金鑰大小)
  • 3DES
  • 延長的 APDU
  • 金鑰認證
  • 升級項目的修訂條款 H 支援功能

使用 KeyStore 類別產生或匯入金鑰時,請將 true 傳遞至 setIsStrongBoxBacked() 方法,指出在 StrongBox Keymaster 儲存金鑰的偏好設定。

與 TEE 相比,StrongBox 雖然速度較慢且資源受限 (意即支援的並行作業較少),但可以提供更完善的安全保證,防範實體攻擊和旁路攻擊。如果想優先提升安全性保證,而非應用程式資源效率,建議您在適用裝置上使用 StrongBox。如果無法使用 StrongBox,應用程式隨時可以改回使用 TEE 儲存金鑰內容。

金鑰使用授權

為避免金鑰在 Android 裝置遭到未經授權使用,Android KeyStore 可在產生或匯入金鑰時,讓應用程式指定金鑰的使用授權。一旦產生或匯入金鑰,就無法變更授權。每次使用金鑰時,Android KeyStore 就會強制執行授權。這是進階安全防護功能,通常只有您要求在金鑰產生/匯入之後 (並非之前或作業期間),遭到入侵的應用程式程序不會導致金鑰遭到未經授權使用時,這項功能才會派上用場。

支援的金鑰使用授權分成以下類別:

  • 加密編譯:金鑰只能用於授權的金鑰演算法、作業或用途 (加密、解密、簽署、驗證)、填充配置、區塊模式或摘要。
  • 暫時有效間隔:只在定義的一段時間內授權使用金鑰。
  • 使用者驗證:使用者必須在最近完成驗證,才能使用金鑰。請參閱「要求驗證使用者才能使用金鑰」。

為增添額外的安全措施,對於將金鑰內容存放在安全硬體中的金鑰 (請參閱 KeyInfo.isInsideSecurityHardware() 相關說明,若是指定 Android 10 (API 級別 29) 以上版本的應用程式,則請參考 KeyInfo.getSecurityLevel() 的說明),安全硬體可能會強制執行某些金鑰使用授權,具體取決於 Android 裝置。安全硬體通常會強制執行加密編譯作業和使用者驗證授權。不過,安全硬體通常不會強制執行暫時有效間隔授權,因為這類授權通常沒有獨立又安全的即時時鐘。

您可以使用 KeyInfo.isUserAuthenticationRequirementEnforcedBySecureHardware(),查詢安全硬體是否強制執行金鑰的使用者驗證授權。

選擇鑰匙圈或 Android KeyStore 供應器

如需全系統憑證,請使用 KeyChain API。當應用程式透過 KeyChain API 要求使用憑證時,使用者可運用系統提供的 UI,選擇應用程式可存取的已安裝憑證。如此一來,多個應用程式就能取得使用者同意,使用同一組憑證。

如果使用 Android KeyStore 供應器,個別應用程式商店即可自行儲存憑證,並只供特定應用程式存取。這樣應用程式就能管理只有自身能使用的憑證,同時帶來等同於 KeyChain API 為全系統憑證提供的安全性優勢。使用這個方法時,使用者不必選取憑證。

使用 Android KeyStore 供應器

如要使用這項功能,請使用標準 KeyStore,搭配 KeyPairGeneratorKeyGenerator 類別,以及在 Android 4.3 (API 級別 18) 中導入的 AndroidKeyStore 供應器。

AndroidKeyStore 已註冊為 KeyStore 類型,可與 KeyStore.getInstance(type) 方法一併使用,並做為搭配 KeyPairGenerator.getInstance(algorithm, provider)KeyGenerator.getInstance(algorithm, provider) 方法的供應器。

產生新的私密金鑰或密鑰

如要產生含有 PrivateKey 的新 KeyPair,您必須指定憑證的初始 X.509 屬性。之後可以使用 KeyStore.setKeyEntry(),將憑證替換為由憑證授權單位 (CA) 簽署的憑證。

如要產生金鑰組,請搭配使用 KeyPairGeneratorKeyGenParameterSpec

Kotlin

/*
 * Generate a new EC key pair entry in the Android Keystore by
 * using the KeyPairGenerator API. The private key can only be
 * used for signing or verification and only with SHA-256 or
 * SHA-512 as the message digest.
 */
val kpg: KeyPairGenerator = KeyPairGenerator.getInstance(
        KeyProperties.KEY_ALGORITHM_EC,
        "AndroidKeyStore"
)
val parameterSpec: KeyGenParameterSpec = KeyGenParameterSpec.Builder(
        alias,
        KeyProperties.PURPOSE_SIGN or KeyProperties.PURPOSE_VERIFY
).run {
    setDigests(KeyProperties.DIGEST_SHA256, KeyProperties.DIGEST_SHA512)
    build()
}

kpg.initialize(parameterSpec)

val kp = kpg.generateKeyPair()

Java

/*
 * Generate a new EC key pair entry in the Android Keystore by
 * using the KeyPairGenerator API. The private key can only be
 * used for signing or verification and only with SHA-256 or
 * SHA-512 as the message digest.
 */
KeyPairGenerator kpg = KeyPairGenerator.getInstance(
        KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore");
kpg.initialize(new KeyGenParameterSpec.Builder(
        alias,
        KeyProperties.PURPOSE_SIGN | KeyProperties.PURPOSE_VERIFY)
        .setDigests(KeyProperties.DIGEST_SHA256,
            KeyProperties.DIGEST_SHA512)
        .build());

KeyPair kp = kpg.generateKeyPair();

將已加密的金鑰匯入安全硬體

在 Android 9 (API 級別 28) 以上版本,您可以使用 ASN.1 編碼的金鑰格式,安全地將已加密的金鑰匯入 KeyStore。接著,Keymaster 會解密 KeyStore 中的金鑰,這樣在裝置主機記憶體中,金鑰內容就一律不會顯示為明文。這項程序可進一步提升金鑰解密安全性。

如要安全地將已加密金鑰匯入 KeyStore,請完成下列步驟:

  1. 產生使用 PURPOSE_WRAP_KEY 用途的金鑰組。建議您一併為這個金鑰組新增認證。

  2. 在信任的伺服器或電腦上,為 SecureKeyWrapper 產生 ASN.1 訊息。

    包裝函式內含下列結構定義:

       KeyDescription ::= SEQUENCE {
           keyFormat INTEGER,
           authorizationList AuthorizationList
       }
    
       SecureKeyWrapper ::= SEQUENCE {
           wrapperFormatVersion INTEGER,
           encryptedTransportKey OCTET_STRING,
           initializationVector OCTET_STRING,
           keyDescription KeyDescription,
           secureKey OCTET_STRING,
           tag OCTET_STRING
       }
    
  3. 建立 WrappedKeyEntry 物件,以位元組陣列的形式傳入 ASN.1 訊息。

  4. 將此 WrappedKeyEntry 物件傳遞至接受 Keystore.Entry 物件的 setEntry() 超載。

使用 KeyStore 項目

您可以透過所有標準 KeyStore API 存取 AndroidKeyStore 供應器。

列出項目

呼叫 aliases() 方法,即可列出 KeyStore 中的項目:

Kotlin

/*
 * Load the Android KeyStore instance using the
 * AndroidKeyStore provider to list the currently stored entries.
 */
val ks: KeyStore = KeyStore.getInstance("AndroidKeyStore").apply {
   load(null)
}
val aliases: Enumeration<String> = ks.aliases()

Java

/*
 * Load the Android KeyStore instance using the
 * AndroidKeyStore provider to list the currently stored entries.
 */
KeyStore ks = KeyStore.getInstance("AndroidKeyStore");
ks.load(null);
Enumeration<String> aliases = ks.aliases();

簽署及驗證資料

從 KeyStore 擷取 KeyStore.Entry,並使用 Signature API (例如 sign()),即可簽署資料:

Kotlin

/*
 * Use a PrivateKey in the KeyStore to create a signature over
 * some data.
 */
val ks: KeyStore = KeyStore.getInstance("AndroidKeyStore").apply {
    load(null)
}
val entry: KeyStore.Entry = ks.getEntry(alias, null)
if (entry !is KeyStore.PrivateKeyEntry) {
    Log.w(TAG, "Not an instance of a PrivateKeyEntry")
    return null
}
val signature: ByteArray = Signature.getInstance("SHA256withECDSA").run {
    initSign(entry.privateKey)
    update(data)
    sign()
}

Java

/*
 * Use a PrivateKey in the KeyStore to create a signature over
 * some data.
 */
KeyStore ks = KeyStore.getInstance("AndroidKeyStore");
ks.load(null);
KeyStore.Entry entry = ks.getEntry(alias, null);
if (!(entry instanceof PrivateKeyEntry)) {
    Log.w(TAG, "Not an instance of a PrivateKeyEntry");
    return null;
}
Signature s = Signature.getInstance("SHA256withECDSA");
s.initSign(((PrivateKeyEntry) entry).getPrivateKey());
s.update(data);
byte[] signature = s.sign();

同樣地,請使用 verify(byte[]) 方法驗證資料:

Kotlin

/*
 * Verify a signature previously made by a private key in the
 * KeyStore. This uses the X.509 certificate attached to the
 * private key in the KeyStore to validate a previously
 * generated signature.
 */
val ks = KeyStore.getInstance("AndroidKeyStore").apply {
    load(null)
}
val entry = ks.getEntry(alias, null) as? KeyStore.PrivateKeyEntry
if (entry == null) {
    Log.w(TAG, "Not an instance of a PrivateKeyEntry")
    return false
}
val valid: Boolean = Signature.getInstance("SHA256withECDSA").run {
    initVerify(entry.certificate)
    update(data)
    verify(signature)
}

Java

/*
 * Verify a signature previously made by a private key in the
 * KeyStore. This uses the X.509 certificate attached to the
 * private key in the KeyStore to validate a previously
 * generated signature.
 */
KeyStore ks = KeyStore.getInstance("AndroidKeyStore");
ks.load(null);
KeyStore.Entry entry = ks.getEntry(alias, null);
if (!(entry instanceof PrivateKeyEntry)) {
    Log.w(TAG, "Not an instance of a PrivateKeyEntry");
    return false;
}
Signature s = Signature.getInstance("SHA256withECDSA");
s.initVerify(((PrivateKeyEntry) entry).getCertificate());
s.update(data);
boolean valid = s.verify(signature);

要求驗證使用者才能使用金鑰

產生金鑰或將金鑰匯入 AndroidKeyStore 時,您可以指定只有在驗證使用者後才能使用金鑰。使用者可以使用一系列安全螢幕鎖定憑證完成驗證,例如解鎖圖案、PIN 碼、密碼、生物特徵辨識憑證。

這是進階安全性功能,通常只有您要求在金鑰產生/匯入之後 (並非之前或作業期間),遭到入侵的應用程式程序不會規避須驗證使用者才能使用金鑰的規定時,這項功能才會派上用場。

如果只有使用者通過驗證時才能授權使用金鑰,您可以呼叫 setUserAuthenticationParameters(),將金鑰設為在以下其中一種模式下運作:

一段時間的授權
只要使用者運用任一指定憑證完成驗證,所有金鑰都會獲得使用授權。
特定加密編譯作業期間的授權

涉及特定金鑰的每個作業都必須由使用者個別授權。

應用程式會在 BiometricPrompt 例項上呼叫 authenticate(),啟動這項程序。

針對每個建立的金鑰,您可以選擇支援高強度生物特徵辨識憑證和/或螢幕鎖定憑證。如要確認使用者是否已設定應用程式金鑰所需的憑證,請呼叫 canAuthenticate()

如果金鑰只支援生物特徵辨識憑證,則根據預設,每次註冊新的生物特徵辨識資料時,金鑰就會失效。您可以調整設定,金鑰就能在註冊新生物特徵辨識資料時保持有效。如要這麼做,請將 false 傳遞至 setInvalidatedByBiometricEnrollment()

進一步瞭解如何在應用程式中加入生物特徵辨識驗證功能,包括如何顯示生物特徵辨識驗證對話方塊

支援的演算法

網誌文章

請參閱網誌文章「在 ICS 中統合 Key Store 存取權」。