
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:
- Dữ liệu có bị thay đổi hay không.
- 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ụ:
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ể.
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à:
SHA256withECDSA
Trong đó:
SHA-256tạo bản tóm tắt của dữ liệu.ECDSAdùng private key trên đường cong elliptic để tạo chữ ký.P-256là 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
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à:
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:
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:
signature.initSign(privateKey)
signature.update(data)
val result = signature.sign()
Nhưng không nên và thường không thể làm:
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:
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.
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
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:
{
"installationId": "c8e4c7f7-6b9c-4b3e-a3d1-2f9d3ef4d810",
"publicKey": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE..."
}
Backend lưu:
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:
POST /v1/palm/analyze
Request có body:
{
"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ụ:
POST
/v1/palm/analyze
application/json
1719811200
550e8400-e29b-41d4-a716-446655440000
3f86f5b4fbb8...
Canonical string có thể gồm:
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
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
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
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
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:
- Đọc
installationId. - Lấy public key tương ứng từ database.
- Kiểm tra timestamp.
- Kiểm tra nonce đã được sử dụng chưa.
- Tính lại hash của body.
- Tạo lại canonical string.
- Xác minh chữ ký bằng public key.
- Chỉ xử lý nghiệp vụ nếu mọi bước hợp lệ.
Pseudo-code:
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ụ:
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:
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:
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ụ:
signature = sign(body)
Nếu attacker giữ nguyên body nhưng đổi URL từ:
/v1/order/preview
sang:
/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:
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:
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ể:
- Sinh private key riêng.
- Gửi public key lên server.
- Ký request hợp lệ bằng private key đó.
- Đượ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:
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:
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:
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:
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:
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ụ:
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ý
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
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
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:
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ể:
- Đọc request body thành bytes.
- Tính body hash.
- Tạo nonce và timestamp.
- Tạo canonical string.
- Ký.
- 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:
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:
[ ] 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:
{
"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:
- Private key dùng để ký và phải ở lại thiết bị.
- Public key gửi lên backend để xác minh.
- P-256 là loại đường cong elliptic của cặp khóa.
- ECDSA là thuật toán tạo và kiểm tra chữ ký.
- Android Keystore quản lý và hạn chế việc export private key.
- 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:
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.