Mật mã học

Tài liệu này hướng dẫn cách sử dụng đúng các phương tiện mã hoá của Android cùng một số ví dụ về cách sử dụng các phương tiện đó. Nếu ứng dụng yêu cầu bảo mật khoá cao hơn, hãy sử dụng hệ thống Kho khoá Android.

Chỉ chỉ định nhà cung cấp bằng hệ thống Kho khóa Android

Nếu đang sử dụng hệ thống Kho khoá Android, bạn phải chỉ định một nhà cung cấp.

Tuy nhiên trong các trường hợp khác, Android không đảm bảo nhà cung cấp cụ thể cho một thuật toán nhất định. Việc chỉ định nhà cung cấp mà không sử dụng hệ thống Kho khoá Android có thể gây ra sự cố về khả năng tương thích trong các bản phát hành sau này.

Chọn một thuật toán đề xuất

Nếu bạn có quyền chọn sử dụng thuật toán (chẳng hạn như khi không yêu cầu khả năng tương thích với hệ thống của bên thứ ba), bạn nên sử dụng các thuật toán sau:

Lớp Đề xuất
Thuật toán mật mã AES ở chế độ CBC hoặc DKIM với khóa 256 bit (chẳng hạn như AES/GCM/NoPadding)
MessageDigest Nhóm SHA-2 (chẳng hạn như SHA-256)
Mac HMAC nhóm SHA-2 (chẳng hạn như HMACSHA256)
Chữ ký Nhóm SHA-2 có ECDSA (chẳng hạn như SHA256withECDSA)

Thực hiện thao tác mã hoá phổ biến

Những phần sau bao gồm các đoạn mã thể hiện cách thức hoàn tất các thao tác mã hoá phổ biến trong ứng dụng.

Đọc tệp

Kotlin

// Although you can define your own key generation parameter specification, it's
// recommended that you use the value specified here.
val keyGenParameterSpec = MasterKeys.AES256_GCM_SPEC
val mainKeyAlias = MasterKeys.getOrCreate(keyGenParameterSpec)

val fileToRead = "my_sensitive_data.txt"
val encryptedFile = EncryptedFile.Builder(
    File(DIRECTORY, fileToRead),
    applicationContext,
    mainKeyAlias,
    EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB
).build()

val inputStream = encryptedFile.openFileInput()
val byteArrayOutputStream = ByteArrayOutputStream()
var nextByte: Int = inputStream.read()
while (nextByte != -1) {
    byteArrayOutputStream.write(nextByte)
    nextByte = inputStream.read()
}

val plaintext: ByteArray = byteArrayOutputStream.toByteArray()

Java

Context context = getApplicationContext();

// Although you can define your own key generation parameter specification, it's
// recommended that you use the value specified here.
KeyGenParameterSpec keyGenParameterSpec = MasterKeys.AES256_GCM_SPEC;
String mainKeyAlias = MasterKeys.getOrCreate(keyGenParameterSpec);

String fileToRead = "my_sensitive_data.txt";
EncryptedFile encryptedFile = new EncryptedFile.Builder(
        new File(DIRECTORY, fileToRead),
        context,
        mainKeyAlias,
        EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB
).build();

InputStream inputStream = encryptedFile.openFileInput();
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
int nextByte = inputStream.read();
while (nextByte != -1) {
    byteArrayOutputStream.write(nextByte);
    nextByte = inputStream.read();
}

byte[] plaintext = byteArrayOutputStream.toByteArray();

Viết tệp

Kotlin

// Although you can define your own key generation parameter specification, it's
// recommended that you use the value specified here.
val keyGenParameterSpec = MasterKeys.AES256_GCM_SPEC
val mainKeyAlias = MasterKeys.getOrCreate(keyGenParameterSpec)

// Create a file with this name or replace an entire existing file
// that has the same name. Note that you cannot append to an existing file,
// and the filename cannot contain path separators.
val fileToWrite = "my_sensitive_data.txt"
val encryptedFile = EncryptedFile.Builder(
    File(DIRECTORY, fileToWrite),
    applicationContext,
    mainKeyAlias,
    EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB
).build()

val fileContent = "MY SUPER-SECRET INFORMATION"
        .toByteArray(StandardCharsets.UTF_8)
encryptedFile.openFileOutput().apply {
    write(fileContent)
    flush()
    close()
}

Java

Context context = getApplicationContext();

// Although you can define your own key generation parameter specification, it's
// recommended that you use the value specified here.
KeyGenParameterSpec keyGenParameterSpec = MasterKeys.AES256_GCM_SPEC;
String mainKeyAlias = MasterKeys.getOrCreate(keyGenParameterSpec);

// Create a file with this name or replace an entire existing file
// that has the same name. Note that you cannot append to an existing file,
// and the filename cannot contain path separators.
String fileToWrite = "my_sensitive_data.txt";
EncryptedFile encryptedFile = new EncryptedFile.Builder(
        new File(DIRECTORY, fileToWrite),
        context,
        mainKeyAlias,
        EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB
).build();

byte[] fileContent = "MY SUPER-SECRET INFORMATION"
        .getBytes(StandardCharsets.UTF_8);
OutputStream outputStream = encryptedFile.openFileOutput();
outputStream.write(fileContent);
outputStream.flush();
outputStream.close();

Mã hoá thông báo

Kotlin

val plaintext: ByteArray = ...
val keygen = KeyGenerator.getInstance("AES")
keygen.init(256)
val key: SecretKey = keygen.generateKey()
val cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING")
cipher.init(Cipher.ENCRYPT_MODE, key)
val ciphertext: ByteArray = cipher.doFinal(plaintext)
val iv: ByteArray = cipher.iv

Java

byte[] plaintext = ...;
KeyGenerator keygen = KeyGenerator.getInstance("AES");
keygen.init(256);
SecretKey key = keygen.generateKey();
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
cipher.init(Cipher.ENCRYPT_MODE, key);
byte[] ciphertext = cipher.doFinal(plaintext);
byte[] iv = cipher.getIV();

Tạo chuỗi đại diện thông báo

Kotlin

val message: ByteArray = ...
val md = MessageDigest.getInstance("SHA-256")
val digest: ByteArray = md.digest(message)

Java

byte[] message = ...;
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] digest = md.digest(message);

Tạo chữ ký số

Bạn cần có đối tượng PrivateKey chứa khoá ký, bạn có thể tạo khoá này trong thời gian chạy, đọc khoá từ một tệp đi kèm với ứng dụng hoặc lấy từ một số nguồn khác tuỳ vào nhu cầu.

Kotlin

val message: ByteArray = ...
val key: PrivateKey = ...
val s = Signature.getInstance("SHA256withECDSA")
        .apply {
            initSign(key)
            update(message)
        }
val signature: ByteArray = s.sign()

Java

byte[] message = ...;
PrivateKey key = ...;
Signature s = Signature.getInstance("SHA256withECDSA");
s.initSign(key);
s.update(message);
byte[] signature = s.sign();

Xác minh chữ ký số

Bạn cần có đối tượng PublicKey chứa khoá công khai của người ký, bạn có thể đọc khoá này từ một tệp đi kèm với ứng dụng, trích xuất từ một chứng chỉ hoặc lấy từ một số nguồn khác tuỳ vào nhu cầu.

Kotlin

val message: ByteArray = ...
val signature: ByteArray = ...
val key: PublicKey = ...
val s = Signature.getInstance("SHA256withECDSA")
        .apply {
            initVerify(key)
            update(message)
        }
val valid: Boolean = s.verify(signature)

Java

byte[] message = ...;
byte[] signature = ...;
PublicKey key = ...;
Signature s = Signature.getInstance("SHA256withECDSA");
s.initVerify(key);
s.update(message);
boolean valid = s.verify(signature);

Sự phức tạp khi triển khai

Việc triển khai mật mã Android có vài chi tiết bất thường nhưng nó vẫn xảy ra do những bất cập trong khả năng tương thích. Phần này thảo luận về những tính năng sẽ gặp nhiều nhất.

Chuỗi đại diện thông báo OAEP MGF1

Thuật toán mật mã RSA OAEP được thông số hoá bằng 2 chuỗi đại diện thông báo: chuỗi đại diện "chính" và chuỗi đại diện MGF1. Các giá trị nhận dạng Cipher bao gồm tên chuỗi đại diện, chẳng hạn như Cipher.getInstance("RSA/ECB/OAEPwithSHA-256andMGF1Padding"), xác định chuỗi đại diện chính và không chỉ định chuỗi đại diện MGF1. Đối với Kho khoá Android, SHA-1 được sử dụng cho chuỗi đại diện MGF1, trong khi đối với các nhà cung cấp mật mã Android khác, 2 chuỗi đại diện này giống nhau.

Để kiểm soát sâu hơn với các chuỗi đại diện mà ứng dụng dùng, bạn nên yêu cầu thuật toán mật mã bằng tính năng OAEPPadding, như trong Cipher.getInstance("RSA/ECB/OAEPPadding"), đồng thời cung cấp OAEPParameterSpec để init() chọn rõ ràng cả hai chuỗi đại diện này. Thông tin này xuất hiện trong mã sau:

Kotlin

val key: Key = ...
val cipher = Cipher.getInstance("RSA/ECB/OAEPPadding")
        .apply {
            // To use SHA-256 the main digest and SHA-1 as the MGF1 digest
            init(Cipher.ENCRYPT_MODE, key, OAEPParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA1, PSource.PSpecified.DEFAULT))
            // To use SHA-256 for both digests
            init(Cipher.ENCRYPT_MODE, key, OAEPParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA256, PSource.PSpecified.DEFAULT))
        }

Java

Key key = ...;
Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPPadding");
// To use SHA-256 the main digest and SHA-1 as the MGF1 digest
cipher.init(Cipher.ENCRYPT_MODE, key, new OAEPParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA1, PSource.PSpecified.DEFAULT));
// To use SHA-256 for both digests
cipher.init(Cipher.ENCRYPT_MODE, key, new OAEPParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA256, PSource.PSpecified.DEFAULT));

Chức năng không dùng nữa

Các phần sau đây mô tả chức năng không dùng nữa. Đừng dùng chức năng này trong ứng dụng của bạn.

Các thuật toán của Bouncy Castle

Triển khai Lâu đài Bouncy trong nhiều thuật toán đã ngừng hoạt động. Điều này chỉ ảnh hưởng đến các trường hợp bạn ra yêu cầu rõ ràng cho nhà cung cấp Lâu đài Bouncy, như trong ví dụ sau:

Kotlin

Cipher.getInstance("AES/CBC/PKCS7PADDING", "BC")
// OR
Cipher.getInstance("AES/CBC/PKCS7PADDING", Security.getProvider("BC"))

Java

Cipher.getInstance("AES/CBC/PKCS7PADDING", "BC");
// OR
Cipher.getInstance("AES/CBC/PKCS7PADDING", Security.getProvider("BC"));

Như đã nêu trong phần chỉ định nhà cung cấp chỉ bằng hệ thống Kho khoá Android, bạn không nên yêu cầu một nhà cung cấp cụ thể. Việc ngừng sử dụng sẽ không ảnh hưởng đến bạn chiếu theo nguyên tắc đó.

Thuật toán mật mã mã hoá dựa trên mật khẩu không có vectơ khởi động

Thuật toán mật mã mã hoá dựa trên mật khẩu (PBE) yêu cầu vectơ khởi động (IV) lấy dữ liệu từ khoá nếu cấu trúc đó được tạo đúng cách hoặc từ một IV chuyển đi một cách rõ ràng. Khi bạn chuyển khoá PBE không chứa IV và không chuyển mã IV rõ ràng, thuật toán mật mã PBE trên Android hiện giả định là IV bằng 0.

Khi sử dụng thuật toán mật mã PBE, hãy luôn chuyển mã IV rõ ràng, như hiển thị trong đoạn mã sau:

Kotlin

val key: SecretKey = ...
val cipher = Cipher.getInstance("PBEWITHSHA256AND256BITAES-CBC-BC")
val iv = ByteArray(16)
SecureRandom().nextBytes(iv)
cipher.init(Cipher.ENCRYPT_MODE, key, IvParameterSpec(iv))

Java

SecretKey key = ...;
Cipher cipher = Cipher.getInstance("PBEWITHSHA256AND256BITAES-CBC-BC");
byte[] iv = new byte[16];
new SecureRandom().nextBytes(iv);
cipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(iv));

Nhà cung cấp mã hoá

Kể từ Android 9 (API cấp 28), nhà cung cấp Crypto Java Cryptography Architecture (JCA) đã bị xoá. Nếu ứng dụng của bạn yêu cầu một bản sao của nhà cung cấp Tiền mã hóa, chẳng hạn như bằng cách gọi phương thức sau thì NoSuchProviderException sẽ xảy ra.

Kotlin

SecureRandom.getInstance("SHA1PRNG", "Crypto")

Java

SecureRandom.getInstance("SHA1PRNG", "Crypto");

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

Dưới đây là các giá trị nhận dạng thuật toán JCA được hỗ trợ trên Android: