Minh họa ứng dụng Android ký request bằng private key trong Android Keystore và backend xác minh bằng public key
Engineering

Android Keystore và EC P-256: Hiểu đúng bảo mật giữa ứng dụng Android và Backend

Giải thích từ nền tảng về private key, public key, chữ ký số, ECDSA P-256 và Android Keystore; kèm luồng triển khai thực tế, ví dụ Kotlin, các lớp bảo mật cần bổ sung và những hiểu lầm phổ biến.

Bui Quang Minh

Aug 05, 2026 26 mins read

Published

Aug 05, 2026

Category

Engineering

Android Keystore và EC P-256: Hiểu đúng bảo mật giữa ứng dụng Android và Backend

Khi mới làm bảo mật cho ứng dụng Android, chúng ta thường bắt gặp một loạt thuật ngữ như:

  • Android Keystore
  • EC P-256
  • ECDSA
  • Private key
  • Public key
  • Digital signature
  • SHA-256
  • Nonce
  • Timestamp
  • Attestation
  • Hardware-backed key

Nhìn riêng lẻ, chúng giống như một đống công thức mật mã khó hiểu. Nhưng khi ghép lại, toàn bộ hệ thống chỉ đang giải quyết một bài toán tương đối rõ ràng:

Làm thế nào để backend kiểm tra rằng một request không bị sửa đổi và được ký bởi private key đã đăng ký với thiết bị?

Bài viết này giải thích kiến thức đó từ góc nhìn của một Android Developer, không yêu cầu bạn phải có nền tảng toán học về mật mã.


1. Đống kiến thức này thuộc lĩnh vực nào?

Những thứ như Android Keystore, EC P-256 và chữ ký số nằm ở giao điểm của bốn mảng kiến thức:

Applied Cryptography — Mật mã học ứng dụng

Đây là cách chúng ta dùng các thuật toán mật mã vào hệ thống thật, chẳng hạn:

  • Ký request.
  • Xác minh dữ liệu.
  • Mã hóa dữ liệu.
  • Bảo vệ khóa.
  • Chống request giả mạo.
  • Chống replay attack.

Bạn không cần tự phát minh thuật toán. Công việc của lập trình viên là sử dụng đúng thuật toán, đúng thư viện và đúng quy trình.

Public-key Cryptography — Mật mã khóa công khai

Đây là mô hình sử dụng một cặp khóa:

  • Private key: khóa bí mật.
  • Public key: khóa công khai.

Hai khóa có quan hệ toán học với nhau, nhưng không thể thực tế suy ngược private key từ public key.

Digital Signature — Chữ ký số

Chữ ký số cho phép một bên ký dữ liệu bằng private key và một bên khác xác minh chữ ký bằng public key.

Nó giúp kiểm tra hai điều:

  1. Dữ liệu có bị thay đổi hay không.
  2. Chữ ký có được tạo bởi private key tương ứng hay không.

Secure Key Management — Quản lý khóa an toàn

Thuật toán mạnh nhưng lưu private key sai cách thì hệ thống vẫn yếu.

Android Keystore giải quyết phần quản lý khóa trên thiết bị Android.


2. Trước tiên: ký dữ liệu không giống mã hóa dữ liệu

Đây là điểm rất dễ nhầm.

Mã hóa — Encryption

Mục tiêu của mã hóa là làm cho người khác không đọc được nội dung.

Ví dụ:

text
Dữ liệu gốc
    ↓ Encrypt
Dữ liệu đã mã hóa
    ↓ Decrypt
Dữ liệu gốc

Chữ ký số — Digital Signature

Mục tiêu của chữ ký số không phải che giấu nội dung.

Nó dùng để chứng minh rằng dữ liệu không bị sửa đổi và được ký bởi một private key cụ thể.

text
Request
   ↓ Sign bằng private key
Signature

Request + Signature
   ↓ Verify bằng public key
Hợp lệ hoặc không hợp lệ

Khi dùng EC P-256 để ký request, thuật toán thường được sử dụng là:

text
SHA256withECDSA

Trong đó:

  • SHA-256 tạo bản tóm tắt của dữ liệu.
  • ECDSA dùng private key trên đường cong elliptic để tạo chữ ký.
  • P-256 là loại đường cong elliptic được sử dụng.

Vì vậy, nói chính xác hơn:

EC P-256 là nền tảng toán học của cặp khóa. ECDSA là thuật toán dùng cặp khóa đó để ký và xác minh dữ liệu.


3. Private key và public key hoạt động như thế nào?

Hãy tưởng tượng bạn có một con dấu đặc biệt.

Private key

Private key giống như khuôn dấu thật.

Nó phải được giữ bí mật và không được gửi lên server, ghi log, lưu trong SharedPreferences hoặc hard-code trong APK.

Private key được dùng để:

  • Ký request.
  • Chứng minh thiết bị đang sở hữu khóa.
  • Xác thực một thao tác quan trọng.

Public key

Public key giống như mẫu mà backend dùng để kiểm tra dấu.

Nó có thể được gửi lên backend và lưu trong database.

Public key được dùng để:

  • Xác minh chữ ký.
  • Nhận diện cặp khóa đã đăng ký.
  • Kiểm tra request có được ký bởi private key tương ứng hay không.

Luồng cơ bản

text
Android Device                          Backend
---------------                         -------
Tạo key pair
Private key
Public key  --------------------------> Lưu public key

Tạo request
Ký bằng private key
Request + Signature ------------------> Verify bằng public key

                                        Hợp lệ → xử lý
                                        Sai → từ chối

Backend không cần và cũng không nên có private key của thiết bị.


4. EC P-256 thực chất là gì?

EC là viết tắt của Elliptic Curve, tức mật mã đường cong elliptic.

P-256 là tên một đường cong elliptic tiêu chuẩn, còn thường được gọi là:

text
secp256r1
prime256v1
NIST P-256

Các tên trên thường chỉ cùng một đường cong, tùy thư viện hoặc nền tảng.

Vì sao dùng P-256?

So với RSA, khóa elliptic curve có kích thước nhỏ hơn nhưng vẫn đạt mức bảo mật mạnh.

Một cách so sánh gần đúng:

text
ECC 256-bit ≈ RSA 3072-bit

Điều đó giúp ECC phù hợp với thiết bị di động vì:

  • Khóa nhỏ hơn.
  • Chữ ký nhỏ hơn.
  • Xử lý nhanh.
  • Ít tốn tài nguyên hơn.
  • Được Android Keystore hỗ trợ rộng rãi.

P-256 không phải mật khẩu 256 ký tự

Con số 256 mô tả kích thước tham số toán học của đường cong, không phải độ dài chuỗi private key mà bạn tự tạo.

Private key phải được sinh bởi API mật mã an toàn. Không tự tạo private key bằng UUID, random string hoặc hash của device ID.


5. Android Keystore là gì?

Android Keystore là hệ thống quản lý khóa do Android cung cấp.

Nó cho phép ứng dụng:

  • Sinh private key.
  • Sử dụng private key để ký hoặc giải mã.
  • Giới hạn mục đích sử dụng của khóa.
  • Yêu cầu người dùng xác thực trước khi dùng khóa.
  • Ngăn private key bị export trực tiếp ra khỏi Keystore.

Điểm quan trọng nhất:

Ứng dụng thường có thể yêu cầu Keystore sử dụng private key, nhưng không thể lấy raw private key ra thành một chuỗi để gửi đi nơi khác.

Ví dụ, ứng dụng có thể gọi:

kotlin
signature.initSign(privateKey)
signature.update(data)
val result = signature.sign()

Nhưng không nên và thường không thể làm:

kotlin
val rawPrivateKey = privateKey.encoded

Với khóa không cho export, encoded có thể trả về null.


6. Android Keystore có luôn được bảo vệ bằng phần cứng không?

Không.

Đây là một hiểu lầm phổ biến.

Android Keystore là API quản lý khóa. Việc private key có nằm trong phần cứng bảo mật hay không còn tùy thiết bị.

Các mức bảo vệ có thể bao gồm:

  • Software-backed.
  • Trusted Execution Environment — TEE.
  • StrongBox Secure Element.

Thiết bị cao cấp có thể hỗ trợ StrongBox. Một số thiết bị chỉ hỗ trợ TEE. Thiết bị cũ hoặc cấu hình đặc biệt có thể sử dụng lớp bảo vệ phần mềm.

Bạn có thể kiểm tra thông tin của khóa bằng KeyInfo.

Ví dụ khái niệm:

kotlin
val factory = KeyFactory.getInstance(
    privateKey.algorithm,
    "AndroidKeyStore"
)

val keyInfo = factory.getKeySpec(
    privateKey,
    KeyInfo::class.java
)

Trên các phiên bản Android mới, có thể kiểm tra securityLevel. Trên API cũ, thường sử dụng isInsideSecureHardware.

Vì vậy, câu mô tả chính xác hơn là:

Android Keystore có thể sử dụng phần cứng bảo mật nếu thiết bị hỗ trợ và khóa được tạo với cấu hình phù hợp.

Không nên mặc định mọi thiết bị đều có StrongBox.


7. Luồng đăng ký thiết bị

Để backend xác minh chữ ký, public key của thiết bị phải được đăng ký trước.

Bước 1: Android sinh cặp khóa

Ứng dụng sử dụng AndroidKeyStore để tạo cặp khóa EC P-256.

kotlin
private const val KEY_ALIAS = "device_signing_key"

fun generateKeyPair(): KeyPair {
    val generator = KeyPairGenerator.getInstance(
        KeyProperties.KEY_ALGORITHM_EC,
        "AndroidKeyStore"
    )

    val spec = KeyGenParameterSpec.Builder(
        KEY_ALIAS,
        KeyProperties.PURPOSE_SIGN or KeyProperties.PURPOSE_VERIFY
    )
        .setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1"))
        .setDigests(KeyProperties.DIGEST_SHA256)
        .build()

    generator.initialize(spec)
    return generator.generateKeyPair()
}

Sau bước này:

  • Private key nằm trong Android Keystore.
  • Public key có thể được export.
  • Ứng dụng gửi public key lên backend.

Bước 2: Export public key

kotlin
fun getPublicKeyBase64(keyPair: KeyPair): String {
    return Base64.encodeToString(
        keyPair.public.encoded,
        Base64.NO_WRAP
    )
}

public.encoded thường ở định dạng X.509 SubjectPublicKeyInfo.

Bước 3: Gửi public key lên backend

Ví dụ request:

json
{
  "installationId": "c8e4c7f7-6b9c-4b3e-a3d1-2f9d3ef4d810",
  "publicKey": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE..."
}

Backend lưu:

text
installation_id
public_key
created_at
status
key_version
device_metadata

Public key không phải bí mật, nhưng endpoint đăng ký khóa vẫn cần được bảo vệ để tránh bị thay thế trái phép.


8. Luồng ký request

Giả sử ứng dụng gọi API:

http
POST /v1/palm/analyze

Request có body:

json
{
  "imageId": "img_123",
  "mode": "palm_reading"
}

Không nên chỉ ký raw JSON trực tiếp vì thứ tự field, khoảng trắng hoặc cách serialize có thể khác nhau giữa Android và backend.

Thay vào đó, hai bên cần thống nhất một canonical string.

Ví dụ:

text
POST
/v1/palm/analyze
application/json
1719811200
550e8400-e29b-41d4-a716-446655440000
3f86f5b4fbb8...

Canonical string có thể gồm:

text
HTTP_METHOD
REQUEST_PATH
CONTENT_TYPE
TIMESTAMP
NONCE
SHA256_BODY

Sau đó Android ký toàn bộ chuỗi trên.

Kotlin: tạo hash body

kotlin
fun sha256Hex(data: ByteArray): String {
    val digest = MessageDigest.getInstance("SHA-256")
        .digest(data)

    return digest.joinToString(separator = "") {
        "%02x".format(it)
    }
}

Kotlin: tạo canonical string

kotlin
fun buildCanonicalRequest(
    method: String,
    path: String,
    contentType: String,
    timestamp: Long,
    nonce: String,
    bodyHash: String
): String {
    return listOf(
        method.uppercase(),
        path,
        contentType.lowercase(),
        timestamp.toString(),
        nonce,
        bodyHash
    ).joinToString("\n")
}

Kotlin: ký bằng ECDSA

kotlin
fun sign(data: ByteArray): String {
    val keyStore = KeyStore.getInstance("AndroidKeyStore").apply {
        load(null)
    }

    val privateKey = keyStore.getKey(
        KEY_ALIAS,
        null
    ) as PrivateKey

    val signature = Signature.getInstance("SHA256withECDSA")
    signature.initSign(privateKey)
    signature.update(data)

    return Base64.encodeToString(
        signature.sign(),
        Base64.NO_WRAP
    )
}

Headers gửi lên backend

http
X-Installation-Id: c8e4c7f7-6b9c-4b3e-a3d1-2f9d3ef4d810
X-Key-Version: 1
X-Timestamp: 1719811200
X-Nonce: 550e8400-e29b-41d4-a716-446655440000
X-Signature: MEUCIQD...
Content-Type: application/json

Backend tạo lại canonical string theo đúng quy tắc rồi verify chữ ký.


9. Backend xác minh chữ ký như thế nào?

Backend thực hiện các bước:

  1. Đọc installationId.
  2. Lấy public key tương ứng từ database.
  3. Kiểm tra timestamp.
  4. Kiểm tra nonce đã được sử dụng chưa.
  5. Tính lại hash của body.
  6. Tạo lại canonical string.
  7. Xác minh chữ ký bằng public key.
  8. Chỉ xử lý nghiệp vụ nếu mọi bước hợp lệ.

Pseudo-code:

python
def verify_request(request):
    installation_id = request.headers["X-Installation-Id"]
    timestamp = request.headers["X-Timestamp"]
    nonce = request.headers["X-Nonce"]
    signature = request.headers["X-Signature"]

    reject_if_timestamp_expired(timestamp)
    reject_if_nonce_already_used(installation_id, nonce)

    public_key = load_public_key(installation_id)

    body_hash = sha256(request.raw_body)

    canonical = build_canonical_request(
        method=request.method,
        path=request.path,
        content_type=request.content_type,
        timestamp=timestamp,
        nonce=nonce,
        body_hash=body_hash,
    )

    verify_ecdsa(
        public_key=public_key,
        data=canonical,
        signature=signature,
    )

    store_nonce(installation_id, nonce)

Nếu body bị thay đổi chỉ một ký tự, hash sẽ khác và chữ ký không còn hợp lệ.


10. Vì sao cần timestamp và nonce?

Chữ ký hợp lệ chưa đủ để chống replay attack.

Replay attack là gì?

Kẻ tấn công không cần sửa request. Họ chỉ cần ghi lại một request hợp lệ rồi gửi lại nhiều lần.

Ví dụ:

text
POST /transfer
amount=1.000.000
signature=valid

Nếu backend chỉ kiểm tra chữ ký, request cũ vẫn có thể hợp lệ.

Timestamp

Timestamp giới hạn thời gian sống của request.

Ví dụ backend chỉ nhận request lệch tối đa 5 phút:

text
abs(server_time - request_timestamp) <= 300 seconds

Nonce

Nonce là một giá trị duy nhất cho mỗi request.

Backend lưu nonce trong một khoảng thời gian ngắn. Nếu nonce đã từng xuất hiện, request bị từ chối.

Ví dụ nonce:

text
550e8400-e29b-41d4-a716-446655440000

Timestamp giúp giới hạn cửa sổ tấn công. Nonce giúp chặn việc gửi lại request trong chính cửa sổ đó.

Cả hai nên được sử dụng cùng nhau.


11. Chữ ký nên bao phủ những dữ liệu nào?

Một lỗi phổ biến là chỉ ký request body.

Ví dụ:

text
signature = sign(body)

Nếu attacker giữ nguyên body nhưng đổi URL từ:

text
/v1/order/preview

sang:

text
/v1/order/confirm

và backend không đưa path vào dữ liệu ký, chữ ký có thể vẫn hợp lệ.

Nên ký tối thiểu:

  • HTTP method.
  • Request path.
  • Content type.
  • Timestamp.
  • Nonce.
  • Hash của raw request body.
  • Installation ID hoặc key ID, nếu kiến trúc yêu cầu.

Với multipart upload, cần quy định rõ phần nào được hash. Không nên tự nghĩ một cách serialize multipart khác nhau giữa FE và BE.

Một hướng đơn giản là:

  • Hash nguyên raw HTTP body nếu cả hai phía truy cập được đúng byte.
  • Hoặc hash từng phần theo thứ tự cố định và tài liệu hóa rõ canonical format.

12. Encoding chữ ký ECDSA: điểm rất dễ lỗi giữa Android và backend

Chữ ký ECDSA thường gồm hai số lớn:

text
r
s

Có hai cách biểu diễn thường gặp:

  • ASN.1 DER.
  • IEEE P1363 dạng r || s.

Trên Android, SHA256withECDSA thường trả về chữ ký dạng DER.

Nếu backend mong đợi raw 64-byte r || s, quá trình verify sẽ thất bại dù cả hai bên dùng đúng P-256.

Do đó cần thống nhất:

text
Algorithm: SHA256withECDSA
Curve: secp256r1 / P-256
Signature encoding: ASN.1 DER
Transport encoding: Base64
Public key encoding: X.509 SubjectPublicKeyInfo

Đây là loại thông tin nên được ghi rõ trong tài liệu API, không chỉ ghi chung chung là “EC P-256”.


13. Android Keystore bảo vệ được gì?

Android Keystore giúp giảm rủi ro:

  • Private key bị lưu dạng plaintext.
  • Private key bị đọc trực tiếp từ app storage.
  • Private key bị export bằng code ứng dụng thông thường.
  • Malware cấp ứng dụng đọc được raw private key.
  • Backup ứng dụng vô tình chứa private key.

Nếu khóa được hardware-backed, private key có thể được xử lý bên trong TEE hoặc Secure Element.

Tuy nhiên, Android Keystore không biến thiết bị thành bất khả xâm phạm.

Trên thiết bị bị root, hook hoặc compromise, attacker có thể không lấy được raw private key nhưng vẫn có khả năng:

  • Hook vào luồng trước khi ký.
  • Yêu cầu ứng dụng hoặc Keystore ký dữ liệu do attacker kiểm soát.
  • Gọi lại logic ký trong app.
  • Thay đổi code hoặc runtime.
  • Đánh cắp token phiên.
  • Điều khiển thiết bị từ môi trường đã bị xâm nhập.

Nói cách khác:

Keystore bảo vệ vật liệu khóa rất tốt, nhưng không tự bảo vệ toàn bộ logic ứng dụng.


14. Request signing có chứng minh app là app chính chủ không?

Không hoàn toàn.

Giả sử app của bạn được decompile. Một attacker có thể viết app giả rồi tự tạo một cặp khóa mới trong Android Keystore của thiết bị họ.

Nếu backend cho phép bất kỳ client nào đăng ký public key mà không có bước xác thực bổ sung, app giả vẫn có thể:

  1. Sinh private key riêng.
  2. Gửi public key lên server.
  3. Ký request hợp lệ bằng private key đó.
  4. Được backend xem như một thiết bị hợp lệ.

Vì thế request signing chủ yếu chứng minh:

Request được ký bởi private key tương ứng với public key đã đăng ký.

Nó không tự chứng minh:

Request chắc chắn đến từ APK chính chủ, chưa bị chỉnh sửa và đang chạy trên thiết bị an toàn.

Để tăng mức tin cậy về ứng dụng và thiết bị, có thể kết hợp thêm:

  • Google Play Integrity API.
  • Key attestation.
  • App signing certificate digest.
  • Backend-issued registration challenge.
  • User authentication.
  • Device risk scoring.
  • Rate limiting.
  • Certificate pinning trong một số threat model phù hợp.

Không có một lớp bảo mật duy nhất giải quyết mọi vấn đề.


15. Key attestation là gì?

Key attestation cho phép backend nhận thêm bằng chứng về cách một khóa được tạo.

Thay vì chỉ gửi public key, Android có thể gửi một certificate chain chứa thông tin liên quan đến khóa và môi trường tạo khóa.

Backend có thể xác minh các thuộc tính như:

  • Khóa có được tạo trong Android Keystore không.
  • Khóa có hardware-backed không.
  • Mức bảo mật là TEE hay StrongBox.
  • Package hoặc certificate digest nào yêu cầu tạo khóa.
  • Trạng thái verified boot, tùy loại attestation và thiết bị.

Luồng khái niệm:

text
Backend tạo challenge ngẫu nhiên
        ↓
Android tạo key với attestation challenge
        ↓
Android gửi certificate chain
        ↓
Backend verify chain và challenge
        ↓
Backend chấp nhận hoặc từ chối public key

Challenge phải do backend sinh và chỉ dùng một lần để tránh dùng lại attestation cũ.

Key attestation mạnh hơn việc app tự khai rằng “đây là khóa hardware-backed”.


16. Google Play Integrity bổ sung điều gì?

Play Integrity giúp backend đánh giá một request dựa trên tín hiệu về:

  • App có đúng package hay không.
  • App có được cài đặt hoặc nhận diện qua Google Play hay không.
  • Binary có khớp phiên bản được Play biết tới hay không.
  • Thiết bị có đáp ứng một số mức integrity hay không.
  • Request có gắn với challenge hoặc nội dung mong đợi hay không.

Play Integrity không thay thế chữ ký request.

Hai lớp giải quyết hai câu hỏi khác nhau:

text
Request signing:
Request có được ký bằng private key đã đăng ký không?

Play Integrity:
Môi trường app và thiết bị có đạt mức tin cậy mong muốn không?

Một kiến trúc thực tế có thể dùng:

text
TLS
+ User authentication
+ Play Integrity khi đăng ký hoặc tại action rủi ro cao
+ Hardware-backed device key
+ Request signing
+ Timestamp và nonce
+ Rate limiting
+ Server-side authorization

17. TLS vẫn bắt buộc

Một số người nghĩ rằng đã ký request thì không cần HTTPS.

Điều đó sai.

Chữ ký request không che giấu dữ liệu. Nếu sử dụng HTTP:

  • Attacker vẫn đọc được request.
  • Token vẫn có thể bị lộ.
  • Dữ liệu riêng tư vẫn bị quan sát.
  • Response từ server có thể bị sửa nếu không có cơ chế bảo vệ tương ứng.
  • Metadata mạng vẫn bị khai thác.

HTTPS/TLS cung cấp:

  • Mã hóa dữ liệu trên đường truyền.
  • Xác thực server.
  • Bảo vệ tính toàn vẹn của kênh truyền.

Request signing là lớp bổ sung, không phải sự thay thế TLS.


18. Certificate pinning có cần thiết không?

Certificate pinning có thể giảm rủi ro khi thiết bị tin cậy một CA độc hại hoặc bị cài chứng chỉ proxy.

Tuy nhiên, pinning cũng tạo chi phí vận hành:

  • Có thể làm app mất kết nối khi đổi certificate.
  • Cần pin backup key.
  • Cần kế hoạch rotation.
  • Có thể bị bypass trên thiết bị đã bị hook.
  • Làm debugging khó hơn.

Vì vậy không nên thêm pinning chỉ để tạo cảm giác an toàn.

Hãy dùng khi threat model thực sự cần và đội ngũ có khả năng vận hành key rotation an toàn.


19. Những thứ không nên dùng làm bí mật trong APK

Không nên hard-code các giá trị sau và xem chúng là secret:

kotlin
const val API_SECRET = "super-secret"
const val HMAC_KEY = "abc123"
const val PRIVATE_KEY = "..."

APK được phân phối cho người dùng. Attacker có thể:

  • Decompile APK.
  • Tìm string.
  • Hook method.
  • Dump memory.
  • Theo dõi network.
  • Patch logic.

Một API key nằm trong APK có thể dùng để nhận diện ứng dụng hoặc giới hạn quota, nhưng không nên được xem là bí mật tuyệt đối.

HMAC dùng một shared secret. Nếu cùng một secret được nhúng vào mọi bản cài app, chỉ cần một attacker lấy được secret là có thể giả mạo toàn bộ client.

Mô hình asymmetric tốt hơn ở điểm:

  • Mỗi thiết bị có private key riêng.
  • Backend chỉ giữ public key.
  • Lộ một thiết bị không làm lộ private key của toàn bộ thiết bị khác.
  • Có thể thu hồi từng key riêng biệt.

20. Vòng đời của khóa

Một thiết kế hoàn chỉnh cần xử lý cả lúc khóa bị mất hoặc không còn dùng được.

Các trường hợp có thể xảy ra:

  • Người dùng xóa dữ liệu ứng dụng.
  • Người dùng gỡ và cài lại app.
  • Thiết bị được factory reset.
  • Khóa bị invalidated.
  • Người dùng thay đổi khóa màn hình.
  • Thiết bị bị đánh dấu rủi ro.
  • Backend cần rotate thuật toán hoặc policy.
  • Người dùng đổi thiết bị.

Nên lưu các trường:

text
key_id
installation_id
user_id
public_key
algorithm
key_version
created_at
last_used_at
revoked_at
revocation_reason
attestation_status

Backend cần hỗ trợ:

  • Đăng ký key mới.
  • Thu hồi key cũ.
  • Chấp nhận nhiều key trong thời gian migration.
  • Theo dõi key version.
  • Không dùng public key làm user identity duy nhất.

21. Private key có tồn tại vĩnh viễn không?

Không nên hiểu theo nghĩa tuyệt đối.

Private key có thể tồn tại lâu trong Keystore, nhưng có thể mất khi:

  • Gỡ ứng dụng.
  • Xóa dữ liệu ứng dụng.
  • Factory reset.
  • Keystore bị reset.
  • Cấu hình yêu cầu xác thực bị invalidated.
  • Chính sách bảo mật hoặc hệ thống thay đổi.

Ứng dụng cần coi việc không tìm thấy key là một trạng thái bình thường và có flow đăng ký lại an toàn.

Ví dụ:

kotlin
fun getOrCreateKeyPair(): KeyPair {
    val existing = loadExistingKeyPair()

    return existing ?: generateKeyPair()
}

Backend không nên chấp nhận việc thay public key chỉ bằng một request không được xác thực.


22. Một kiến trúc bảo mật thực tế

Một luồng tương đối đầy đủ có thể như sau.

Giai đoạn đăng ký

text
1. App gọi backend xin registration challenge.
2. Backend trả challenge ngẫu nhiên, có thời hạn ngắn.
3. App tạo EC P-256 key trong Android Keystore.
4. App tạo key attestation với challenge.
5. App lấy Play Integrity token gắn với request.
6. App gửi public key, attestation chain và integrity token.
7. Backend xác minh tất cả bằng chứng.
8. Backend tạo installation record và cấp key ID.

Giai đoạn gửi request

text
1. App tạo request body.
2. App tính SHA-256 của raw body.
3. App tạo timestamp và nonce.
4. App tạo canonical string.
5. App ký bằng private key trong Keystore.
6. App gửi request qua HTTPS.
7. Backend kiểm tra timestamp.
8. Backend kiểm tra nonce.
9. Backend lấy public key theo key ID.
10. Backend tạo lại canonical string.
11. Backend verify chữ ký.
12. Backend kiểm tra authentication và authorization.
13. Backend xử lý nghiệp vụ.

Giai đoạn thu hồi

text
1. Backend phát hiện key đáng ngờ.
2. Backend đánh dấu key là revoked.
3. Mọi request từ key đó bị từ chối.
4. App phải thực hiện flow đăng ký mới.

23. Ví dụ cấu trúc module Android

Có thể tách thành các thành phần:

text
security/
├── DeviceKeyManager.kt
├── RequestSigner.kt
├── CanonicalRequestBuilder.kt
├── BodyHasher.kt
├── NonceGenerator.kt
├── IntegrityTokenProvider.kt
└── model/
    ├── SignedHeaders.kt
    └── KeyRegistrationRequest.kt

DeviceKeyManager

Chịu trách nhiệm:

  • Tạo key.
  • Đọc public key.
  • Kiểm tra key tồn tại.
  • Xóa key.
  • Đọc security level.

RequestSigner

Chịu trách nhiệm:

  • Nhận canonical bytes.
  • Dùng private key ký.
  • Trả signature Base64.

CanonicalRequestBuilder

Chịu trách nhiệm:

  • Chuẩn hóa method.
  • Chuẩn hóa path.
  • Ghép timestamp, nonce và body hash.
  • Đảm bảo format giống backend.

Retrofit/OkHttp interceptor

Interceptor có thể:

  1. Đọc request body thành bytes.
  2. Tính body hash.
  3. Tạo nonce và timestamp.
  4. Tạo canonical string.
  5. Ký.
  6. Thêm headers.

Cần cẩn thận với:

  • Streaming request.
  • Multipart file lớn.
  • One-shot body.
  • Request body không thể ghi lại nhiều lần.
  • Đồng bộ thời gian thiết bị.
  • Retry tạo lại nonce hay dùng nonce cũ.

Không nên đưa toàn bộ crypto logic trực tiếp vào một interceptor khổng lồ.


24. Những lỗi triển khai thường gặp

Lỗi 1: Gọi EC P-256 là thuật toán mã hóa request

Nếu đang dùng SHA256withECDSA, bạn đang ký request, không phải mã hóa request.

Lỗi 2: Chỉ ký body

Cần cân nhắc ký cả method, path, timestamp, nonce và body hash.

Lỗi 3: Không có nonce

Request hợp lệ có thể bị replay.

Lỗi 4: Không kiểm tra timestamp ở backend

Attacker có thể dùng lại request cũ trong thời gian dài.

Lỗi 5: Hai bên canonicalize khác nhau

Chỉ một dấu xuống dòng khác nhau cũng làm chữ ký không hợp lệ.

Lỗi 6: Không thống nhất encoding chữ ký

Android trả DER nhưng backend lại mong raw r || s.

Lỗi 7: Cho phép thay public key quá dễ dàng

Attacker có thể đăng ký key của họ để chiếm installation record.

Lỗi 8: Nghĩ Keystore chứng minh APK chính chủ

Keystore chỉ chứng minh quyền sở hữu private key, trừ khi có thêm attestation và kiểm tra phù hợp.

Lỗi 9: Không dùng HTTPS

Request vẫn bị đọc dù có chữ ký.

Lỗi 10: Log dữ liệu nhạy cảm

Không log:

  • Access token.
  • Raw private key.
  • Full signature nếu không cần.
  • Attestation token.
  • Request body chứa dữ liệu cá nhân.
  • Public key kèm định danh nhạy cảm nếu log có phạm vi truy cập rộng.

25. Cách debug khi verify luôn thất bại

Khi Android ký được nhưng backend verify thất bại, hãy log có kiểm soát ở môi trường development:

text
algorithm
curve
signature_encoding
public_key_encoding
canonical_string_length
canonical_string_sha256
body_sha256
timestamp
nonce

Không cần log private key.

Checklist:

text
[ ] Android và backend cùng dùng SHA-256?
[ ] Cùng dùng ECDSA?
[ ] Cùng dùng P-256/secp256r1?
[ ] Signature là DER hay P1363?
[ ] Public key là X.509 SPKI hay raw point?
[ ] Base64 thường hay Base64 URL-safe?
[ ] Có padding "=" hay không?
[ ] Canonical string có cùng newline "\n"?
[ ] Path có bao gồm query string?
[ ] Content-Type có cùng chữ hoa/chữ thường?
[ ] Body hash tính trên raw bytes hay JSON serialize lại?
[ ] Timestamp dùng giây hay milliseconds?

Một kỹ thuật tốt là lưu một test vector dùng chung:

json
{
  "method": "POST",
  "path": "/v1/test",
  "timestamp": 1719811200,
  "nonce": "test-nonce-001",
  "body": "{\"hello\":\"world\"}",
  "bodySha256": "...",
  "canonicalBase64": "...",
  "signatureBase64": "...",
  "publicKeyBase64": "..."
}

Android và backend cùng chạy test trên bộ dữ liệu này để phát hiện sai khác format.


26. Khi nào thực sự cần request signing?

Không phải mọi ứng dụng đều cần tự xây request signing.

Với nhiều ứng dụng, các lớp sau đã đủ:

  • HTTPS.
  • OAuth hoặc token authentication.
  • Server-side authorization.
  • Rate limiting.
  • Secure session management.
  • Play Integrity tại các thao tác rủi ro.

Request signing hữu ích hơn khi:

  • API có thao tác tài chính hoặc dữ liệu giá trị cao.
  • Cần ràng buộc request với một device key.
  • Cần giảm rủi ro token bị copy sang thiết bị khác.
  • Cần audit rõ request được ký bởi key nào.
  • Có threat model liên quan đến replay hoặc request tampering ngoài TLS.
  • Cần xác thực thiết bị trong mô hình zero-trust hoặc enterprise.

Đừng thêm crypto tùy chỉnh nếu đội ngũ chưa thể vận hành:

  • Key lifecycle.
  • Rotation.
  • Revocation.
  • Clock skew.
  • Nonce storage.
  • Migration.
  • Incident response.
  • Cross-platform test vectors.

Một hệ thống bảo mật phức tạp nhưng triển khai sai có thể kém an toàn hơn một hệ thống đơn giản và đúng chuẩn.


27. Mô hình tư duy ngắn gọn

Bạn có thể nhớ toàn bộ hệ thống bằng sáu câu:

  1. Private key dùng để ký và phải ở lại thiết bị.
  2. Public key gửi lên backend để xác minh.
  3. P-256 là loại đường cong elliptic của cặp khóa.
  4. ECDSA là thuật toán tạo và kiểm tra chữ ký.
  5. Android Keystore quản lý và hạn chế việc export private key.
  6. Timestamp + nonce + TLS + backend authorization vẫn bắt buộc.

Câu quan trọng nhất:

Chữ ký hợp lệ chỉ chứng minh request được ký bởi private key tương ứng. Nó không tự chứng minh app chưa bị sửa, thiết bị không bị root, người dùng có quyền thực hiện hành động, hay request chưa từng được gửi trước đó.


28. Kết luận

Android Keystore và EC P-256 không phải một “phép thuật bảo mật”.

Chúng là các mảnh ghép trong một hệ thống:

text
Android Keystore
    ↓ giữ private key

EC P-256
    ↓ định nghĩa loại key

ECDSA + SHA-256
    ↓ tạo chữ ký

Public key trên backend
    ↓ xác minh chữ ký

Timestamp + nonce
    ↓ chống replay

TLS
    ↓ bảo vệ kênh truyền

Play Integrity + attestation
    ↓ tăng độ tin cậy về app, key và thiết bị

Authentication + authorization
    ↓ xác định người dùng là ai và được phép làm gì

Khi hiểu từng lớp giải quyết câu hỏi nào, các đoạn code mật mã sẽ không còn là những dòng khó hiểu phải copy-paste.

Bạn sẽ biết:

  • Vì sao phải có chúng.
  • Chúng bảo vệ điều gì.
  • Chúng không bảo vệ điều gì.
  • Khi nào nên sử dụng.
  • Và quan trọng nhất: backend phải kiểm tra những gì để hệ thống thực sự an toàn.