Hệ thống Kho khoá Android

Hệ thống Kho khoá Android cho phép bạn lưu trữ các khoá mã hoá trong một vùng chứa để khiến việc trích xuất khoá từ thiết bị trở nên khó khăn hơn. Khi các khoá đã nằm trong kho khoá, bạn có thể sử dụng các khoá đó cho hoạt động mã hoá với nội dung khoá không thể xuất được này. Ngoài ra, hệ thống kho khoá cho phép bạn hạn chế thời điểm và cách thức sử dụng khoá, chẳng hạn như yêu cầu người dùng xác thực để sử dụng khoá hoặc hạn chế việc sử dụng khoá chỉ ở một số chế độ mã hoá nhất định. Hãy xem phần Tính năng bảo mật để biết thêm thông tin.

Hệ thống kho khoá được sử dụng bởi API KeyChain, ra mắt trong Android 4.0 (API cấp 14), cũng như bởi tính năng trình cung cấp Kho khoá Android, ra mắt trong Android 4.3 (API cấp 18). Tài liệu này sẽ trình bày thời điểm và cách sử dụng hệ thống Kho khoá Android.

Các tính năng bảo mật

Hệ thống Kho khoá Android bảo vệ nội dung khoá khỏi hoạt động sử dụng trái phép theo 2 cách. Thứ nhất, hệ thống này giúp giảm nguy cơ sử dụng trái phép nội dung khoá ở bên ngoài thiết bị Android bằng cách ngăn việc trích xuất nội dung khoá từ các quy trình của ứng dụng và từ thiết bị Android nói chung. Thứ hai, hệ thống kho khoá giúp giảm nguy cơ sử dụng trái phép nội dung khoá trong phạm vi thiết bị Android bằng cách buộc các ứng dụng chỉ định những trường hợp sử dụng khoá được phép rồi thực thi những hạn chế đó bên ngoài các quy trình của ứng dụng.

Ngăn chặn hoạt động trích xuất

Nội dung khoá của các khoá trong Kho khoá Android được bảo vệ khỏi hoạt động trích xuất thông qua 2 biện pháp bảo mật sau:

  • Nội dung khoá không bao giờ được đưa vào quy trình của ứng dụng. Khi một ứng dụng thực hiện hoạt động mã hoá bằng một khoá trong Kho khoá Android, ở đằng sau đó, văn bản thuần tuý, đoạn mật mã và các thông báo cần được ký hoặc xác minh sẽ được đưa vào một quy trình hệ thống thực hiện các hoạt động mã hoá này. Nếu quy trình của ứng dụng bị xâm phạm, kẻ tấn công có thể sử dụng các khoá của ứng dụng nhưng không thể trích xuất nội dung khoá đó (ví dụ: để dùng bên ngoài thiết bị Android).
  • Nội dung khoá có thể được liên kết với phần cứng bảo mật của thiết bị Android, chẳng hạn như Môi trường thực thi đáng tin cậy (TEE) hoặc Secure Element (SE). Khi tính năng này được bật cho một khoá, nội dung khoá của khoá đó sẽ không bao giờ bị lộ ra bên ngoài phần cứng bảo mật. Nếu hệ điều hành Android bị xâm phạm hoặc một kẻ tấn công có thể đọc bộ nhớ trong của thiết bị, thì kẻ tấn công đó có thể dùng khoá của bất kỳ ứng dụng nào trong Kho khoá Android trên thiết bị Android nhưng không thể trích xuất khoá đó từ thiết bị. Tính năng này chỉ được bật nếu phần cứng bảo mật của thiết bị hỗ trợ một tổ hợp cụ thể gồm thuật toán khoá, chế độ khối, lược đồ khoảng đệm và chuỗi đại diện mà khoá được phép sử dụng cùng.

    Để kiểm tra xem tính năng này đã được bật cho một khoá hay chưa, hãy lấy KeyInfo cho khoá đó. Bước tiếp theo sẽ tuỳ thuộc vào phiên bản SDK mục tiêu của ứng dụng:

    • Nếu ứng dụng của bạn nhắm đến Android 10 (API cấp 29) trở lên, hãy kiểm tra giá trị trả về của getSecurityLevel(). Giá trị trả về khớp với KeyProperties.SecurityLevelEnum.TRUSTED_ENVIRONMENT hoặc KeyProperties.SecurityLevelEnum.STRONGBOX cho biết khoá nằm trong phạm vi phần cứng bảo mật.
    • Nếu ứng dụng của bạn nhắm đến Android 9 (API cấp 28) trở xuống, hãy kiểm tra giá trị boolean trả về của KeyInfo.isInsideSecurityHardware().

Mô-đun bảo mật phần cứng

Các thiết bị được hỗ trợ chạy Android 9 (API cấp 28) trở lên có thể có StrongBox Keymaster. Đây là cách triển khai Keymaster hoặc Lớp trừu tượng phần cứng Keymint (HAL Keymint) nằm trong một Secure Element giống như mô-đun bảo mật phần cứng. Mặc dù các mô-đun bảo mật phần cứng có thể tham chiếu đến nhiều cách triển khai giải pháp lưu trữ khoá khác nhau mà theo đó hành vi xâm phạm nhân hệ điều hành Linux sẽ không thể làm lộ các khoá này, chẳng hạn như TEE, nhưng StrongBox tham chiếu rõ ràng đến các thiết bị như Secure Element được nhúng (eSE) hoặc bộ xử lý bảo mật trên SoC (iSE).

Mô-đun này có các thành phần sau:

  • CPU riêng
  • Bộ nhớ bảo mật
  • Một trình tạo số ngẫu nhiên thực sự
  • Các cơ chế bổ sung để chống lại hành vi can thiệp vào gói và cài đặt trái phép ứng dụng (không qua cửa hàng ứng dụng)
  • Một bộ hẹn giờ bảo mật
  • Một chấu thông báo khởi động lại (hoặc tương đương), chẳng hạn như chấu đầu vào/đầu ra đa năng (GPIO)

Để hỗ trợ hoạt động triển khai StrongBox công suất thấp, một tập hợp con gồm các thuật toán và kích thước khoá được hỗ trợ:

  • RSA 2048
  • AES 128 và 256
  • ECDSA, ECDH P-256
  • HMAC-SHA256 (hỗ trợ các kích thước khoá nằm trong khoảng từ 8 byte đến 64 byte, bao gồm cả kích thước 8 byte và 64 byte)
  • DES 3 lần
  • Các APDU có độ dài mở rộng
  • Chứng thực khoá
  • Hỗ trợ bản sửa đổi H để nâng cấp

Khi tạo hoặc nhập khoá bằng lớp KeyStore, bạn cho biết lựa chọn ưu tiên để lưu trữ khoá trong StrongBox Keymaster bằng cách truyền true vào phương thức setIsStrongBoxBacked().

Mặc dù StrongBox chậm hơn một chút và hạn chế về tài nguyên (tức là hỗ trợ ít hoạt động đồng thời hơn) so với TEE, nhưng StrongBox đảm bảo khả năng bảo mật tốt hơn trước các cuộc tấn công thực tế và tấn công kênh bên. Trong trường hợp muốn ưu tiên mức đảm bảo bảo mật cao hơn so với khả năng của tài nguyên ứng dụng, nếu được, bạn nên sử dụng StrongBox trên các thiết bị. Bất cứ khi nào StrongBox không có sẵn, ứng dụng của bạn luôn có thể quay lại sử dụng TEE để lưu trữ các nội dung khoá.

Hoạt động uỷ quyền sử dụng khoá

Để tránh hoạt động sử dụng trái phép khoá trên thiết bị Android, Kho khoá Android cho phép các ứng dụng chỉ định trường hợp sử dụng được phép cho khoá của ứng dụng khi tạo hoặc nhập khoá. Sau khi một khoá được tạo hoặc nhập, bạn không thể thay đổi các hoạt động uỷ quyền của khoá đó. Sau đó, các hoạt động uỷ quyền sẽ được Kho khoá Android thực thi bất cứ khi nào khoá được sử dụng. Đây là một tính năng bảo mật nâng cao, thường chỉ hữu ích nếu yêu cầu của bạn là: một hành vi xâm phạm quy trình của ứng dụng sau khi (chứ không phải trước hoặc trong khi) tạo/nhập khoá không được phép dẫn đến việc sử dụng trái phép khoá.

Sau đây là danh mục các hoạt động uỷ quyền sử dụng khoá được hỗ trợ:

  • Mật mã học: bạn chỉ có thể dùng khoá với các thuật toán, hoạt động/mục đích (mã hoá, giải mã, ký, xác minh), lược đồ khoảng đệm, chế độ khối hoặc chuỗi đại diện được phép sử dụng khoá.
  • Khoảng thời gian hợp lệ tạm thời: khoá chỉ được phép sử dụng trong một khoảng thời gian xác định.
  • Xác thực người dùng: bạn chỉ có thể sử dụng khoá nếu gần đây người dùng đã được xác thực đầy đủ. Hãy xem phần Yêu cầu xác thực người dùng để sử dụng khoá.

Là biện pháp bảo mật bổ sung cho các khoá mà nội dung khoá nằm trong phần cứng bảo mật (xem KeyInfo.isInsideSecurityHardware() hoặc đối với ứng dụng nhắm đến Android 10 (API cấp 29) trở lên, KeyInfo.getSecurityLevel()), một số hoạt động uỷ quyền sử dụng khoá có thể được phần cứng bảo mật thực thi, tuỳ thuộc vào thiết bị Android. Phần cứng bảo mật thường thực thi các hoạt động uỷ quyền mã hoá và xác thực người dùng. Tuy nhiên, phần cứng bảo mật thường không thực thi các hoạt động uỷ quyền trong khoảng thời gian hợp lệ vì phần cứng này thường không có đồng hồ thời gian thực hoạt động theo cơ chế bảo mật, độc lập.

Bạn có thể truy vấn xem phần cứng bảo mật có thực thi hoạt động uỷ quyền xác thực người dùng của một khoá hay không bằng KeyInfo.isUserAuthenticationRequirementEnforcedBySecureHardware().

Chọn giữa chuỗi khoá và trình cung cấp Kho khoá Android

Sử dụng API KeyChain khi bạn muốn có thông tin đăng nhập trên toàn hệ thống. Khi một ứng dụng yêu cầu sử dụng bất kỳ thông tin đăng nhập nào qua API KeyChain, người dùng có thể chọn thông tin xác thực đã cài đặt nào mà ứng dụng có thể truy cập (thông qua một giao diện người dùng do hệ thống cung cấp). Điều này cho phép một số ứng dụng dùng cùng một bộ thông tin đăng nhập khi có sự đồng ý của người dùng.

Sử dụng trình cung cấp Kho khoá Android để cho phép một ứng dụng riêng lẻ lưu trữ thông tin đăng nhập của riêng mình mà chỉ ứng dụng đó mới có thể truy cập. Đây là một cách để các ứng dụng quản lý thông tin đăng nhập mà chỉ các ứng dụng đó mới có thể dùng, đồng thời đem lại các lợi ích bảo mật tương tự mà API KeyChain cung cấp cho thông tin đăng nhập trên toàn hệ thống. Phương thức này không yêu cầu người dùng chọn thông tin đăng nhập.

Dùng trình cung cấp Kho khoá Android

Để dùng tính năng này, bạn sử dụng các lớp KeyStoreKeyPairGenerator hoặc KeyGenerator tiêu chuẩn cùng với trình cung cấp AndroidKeyStore ra mắt trong Android 4.3 (API cấp 18).

AndroidKeyStore được đăng ký dưới dạng một loại KeyStore để sử dụng với phương thức KeyStore.getInstance(type) và làm trình cung cấp để sử dụng với phương thức KeyPairGenerator.getInstance(algorithm, provider)KeyGenerator.getInstance(algorithm, provider).

Tạo một khoá riêng tư hoặc khoá bí mật mới

Để tạo một KeyPair mới chứa PrivateKey, bạn phải chỉ định các thuộc tính X.509 ban đầu của chứng chỉ. Sau này, bạn có thể sử dụng KeyStore.setKeyEntry() để thay thế chứng chỉ này bằng chứng chỉ do một tổ chức phát hành chứng chỉ (CA) ký.

Để tạo cặp khoá, hãy dùng KeyPairGenerator với KeyGenParameterSpec:

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();

Nhập các khoá đã mã hoá vào phần cứng bảo mật

Android 9 (API cấp 28) trở lên cho phép bạn nhập các khoá đã mã hoá một cách an toàn vào kho khoá bằng cách sử dụng định dạng khoá được mã hoá ASN.1. Sau đó, Keymaster sẽ giải mã các khoá trong kho khoá để nội dung của các khoá không bao giờ xuất hiện dưới dạng văn bản thuần tuý trong bộ nhớ máy chủ của thiết bị. Quy trình này tăng cường khả năng bảo mật cho quy trình giải mã khoá.

Để hỗ trợ tính năng nhập an toàn các khoá đã mã hoá vào kho khoá, hãy hoàn tất những bước sau:

  1. Tạo một cặp khoá sử dụng mục đích PURPOSE_WRAP_KEY. Bạn cũng nên thêm chứng thực vào cặp khoá này.

  2. Trên một máy chủ hoặc máy mà bạn tin tưởng, hãy tạo thông báo ASN.1 cho SecureKeyWrapper.

    Trình bao bọc chứa giản đồ sau đây:

       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. Tạo một đối tượng WrappedKeyEntry, truyền thông báo ASN.1 dưới dạng một mảng byte.

  4. Truyền đối tượng WrappedKeyEntry này vào phương thức nạp chồng của setEntry() chấp nhận đối tượng Keystore.Entry.

Thao tác với các mục trong kho khoá

Bạn có thể truy cập vào trình cung cấp AndroidKeyStore thông qua tất cả các API KeyStore tiêu chuẩn.

Liệt kê các mục

Liệt kê các mục trong kho khoá bằng cách gọi phương thức aliases():

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();

Ký và xác minh dữ liệu

Ký dữ liệu bằng cách tìm nạp KeyStore.Entry từ kho khoá và sử dụng các API Signature, chẳng hạn như 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();

Tương tự, hãy xác minh dữ liệu bằng phương thức 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);

Yêu cầu xác thực người dùng để sử dụng khoá

Khi tạo hoặc nhập một khoá vào AndroidKeyStore, bạn có thể chỉ định rằng chỉ được phép sử dụng khoá này nếu người dùng đã được xác thực. Người dùng được xác thực bằng một tập hợp con thông tin đăng nhập màn hình khoá bảo mật (hình mở khoá/mã PIN/mật khẩu, thông tin xác thực sinh trắc học).

Đây là một tính năng bảo mật nâng cao, thường chỉ hữu ích nếu yêu cầu của bạn là: một hành vi xâm phạm quy trình của ứng dụng sau khi (chứ không phải trước hoặc trong khi) tạo/nhập khoá không được phép bỏ qua yêu cầu mà theo đó, người dùng phải được xác thực để sử dụng khoá.

Trong trường hợp một khoá chỉ được phép sử dụng nếu người dùng được xác thực, bạn có thể gọi setUserAuthenticationParameters() để định cấu hình khoá đó hoạt động ở một trong những chế độ sau:

Cho phép trong một khoảng thời gian
Tất cả các khoá đều được phép sử dụng ngay khi người dùng xác thực bằng một trong những thông tin đăng nhập đã chỉ định.
Cho phép trong khoảng thời gian của một thao tác mã hoá cụ thể

Mỗi thao tác liên quan đến một khoá cụ thể phải được người dùng cho phép riêng.

Ứng dụng bắt đầu tiến trình này bằng cách gọi authenticate() trên một thực thể của BiometricPrompt.

Đối với mỗi khoá mà bạn tạo, bạn có thể chọn hỗ trợ thông tin xác thực sinh trắc học (mạnh), thông tin đăng nhập màn hình khoá hoặc cả hai kiểu thông tin đăng nhập. Để xác định xem người dùng đã thiết lập thông tin đăng nhập mà khoá của ứng dụng dựa vào hay chưa, hãy gọi canAuthenticate().

Nếu một khoá chỉ hỗ trợ thông tin xác thực sinh trắc học, thì theo mặc định, khoá đó sẽ không hợp lệ mỗi khi bạn thêm các lượt đăng ký sinh trắc học mới. Bạn có thể định cấu hình khoá này để duy trì tính hợp lệ của khoá khi các lượt đăng ký sinh trắc học mới được thêm vào. Để thực hiện việc này, hãy truyền false vào setInvalidatedByBiometricEnrollment().

Vui lòng tìm hiểu thêm về cách bổ sung các tính năng xác thực bằng sinh trắc học vào ứng dụng của bạn, bao gồm cả cách hiển thị hộp thoại xác thực bằng sinh trắc học.

Các thuật toán được hỗ trợ

Bài viết trên blog

Hãy xem bài viết Hợp nhất quyền truy cập vào kho khoá trong ICS trên blog.