Add niap-android-cert-ext and sdp-file-encrypt libraries to niap-cc#58
Add niap-android-cert-ext and sdp-file-encrypt libraries to niap-cc#58KVVat wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces two major security modules: niap-android-cert-ext, which implements strict X.509 certificate validation and secure lifecycle management using the EST protocol, and sdp-file-encrypt, which provides robust file encryption strategies compliant with NIAP MDF PP requirements. The code review identified several critical issues, including a namespace mismatch in NiapCertHelper that prevents configuration loading, sensitive cryptographic key material being logged to Logcat, potential file descriptor leaks in TinkEncryptionProvider, and thread-safety concerns in NiapCertManager. Additionally, optimizations were suggested to prevent performance bottlenecks during binder buffer flushing and to avoid potential null pointer exceptions in NiapCertValidator.
| val libResId = context.resources.getIdentifier("niap_security_config", "xml", "com.android.niap.cert.validator") | ||
| if (libResId != 0) { | ||
| context.resources.getXml(libResId) | ||
| } else { | ||
| throw IllegalStateException("niap_security_config.xml not found") | ||
| } |
There was a problem hiding this comment.
The package name passed to getIdentifier is "com.android.niap.cert.validator", but the actual namespace defined in the library's build.gradle.kts is "com.example.niap.cert.ext.validator". This mismatch will cause getIdentifier to return 0, leading to an IllegalStateException when trying to load the default configuration.
| val libResId = context.resources.getIdentifier("niap_security_config", "xml", "com.android.niap.cert.validator") | |
| if (libResId != 0) { | |
| context.resources.getXml(libResId) | |
| } else { | |
| throw IllegalStateException("niap_security_config.xml not found") | |
| } | |
| val libResId = context.resources.getIdentifier("niap_security_config", "xml", "com.example.niap.cert.ext.validator") | |
| if (libResId != 0) { | |
| context.resources.getXml(libResId) | |
| } else { | |
| throw IllegalStateException("niap_security_config.xml not found") | |
| } |
| } | ||
| fun logKeyMaterial(tag: String= TAG, name: String, bytes: ByteArray?) { | ||
| if (!isAuditLogEnabled || bytes== null) { | ||
| Log.d("${name} KMD", "returns null (error or hardware backend)") | ||
| return | ||
| } | ||
| Log.d("${name} KMD", bytes.toHexString()) | ||
| bytes.fill(0) |
There was a problem hiding this comment.
Logging raw cryptographic key material (such as private keys, DEKs, or shared secrets) to Logcat is a critical security vulnerability (CWE-532). Even if it is intended for debugging or audit purposes, key material must be masked or completely excluded from logs to prevent sensitive data exposure.
| } | |
| fun logKeyMaterial(tag: String= TAG, name: String, bytes: ByteArray?) { | |
| if (!isAuditLogEnabled || bytes== null) { | |
| Log.d("${name} KMD", "returns null (error or hardware backend)") | |
| return | |
| } | |
| Log.d("${name} KMD", bytes.toHexString()) | |
| bytes.fill(0) | |
| fun logKeyMaterial(tag: String= TAG, name: String, bytes: ByteArray?) { | |
| if (!isAuditLogEnabled || bytes== null) { | |
| Log.d("${name} KMD", "returns null (error or hardware backend)") | |
| return | |
| } | |
| Log.d("${name} KMD", "[MASKED - KEY MATERIAL NOT EXPOSED]") | |
| bytes.fill(0) | |
| } |
| private var service: INiapCertManager? = null | ||
| private var latch = CountDownLatch(1) | ||
| private var isBinding = false |
There was a problem hiding this comment.
In a multi-threaded environment, mutable fields like service, latch, and isBinding can suffer from thread visibility and race condition issues. Since these methods are designed to be safe to call from any thread, these fields should be marked as @Volatile to ensure visibility across threads.
| private var service: INiapCertManager? = null | |
| private var latch = CountDownLatch(1) | |
| private var isBinding = false | |
| @Volatile private var service: INiapCertManager? = null | |
| @Volatile private var latch = CountDownLatch(1) | |
| @Volatile private var isBinding = false |
| if (deleteOriginal) { | ||
| sourceFile.delete() | ||
| } |
There was a problem hiding this comment.
If deleteOriginal is requested, the failure of sourceFile.delete() is silently ignored. This can leave unencrypted sensitive files on disk without raising any error, which is a significant security risk. The return value of delete() should be checked, and an exception should be thrown if the deletion fails.
if (deleteOriginal) {
if (!sourceFile.delete()) {
throw java.io.IOException("Failed to delete original plaintext file after encryption.")
}
}| companion object { | ||
| private const val ANDROID_KEYSTORE = "AndroidKeyStore" | ||
| private const val KEY_PUBLIC_KEY_PREF = "master_public_key" | ||
| private const val EC_KEY_ALGORITHM = KeyProperties.KEY_ALGORITHM_EC | ||
| private const val KEY_AGREEMENT_ALGORITHM = "ECDH" | ||
| private const val DEK_ALGORITHM = "AES" | ||
| private const val DEK_WRAPPING_CIPHER = "AES/GCM/NoPadding" | ||
| private const val DEK_SIZE_BITS = 256 | ||
| private const val DATA_CIPHER = "AES/GCM/NoPadding" | ||
| private const val GCM_TAG_LENGTH_BITS = 128 | ||
| const val MAGIC_BYTE_ASYMMETRIC: Byte = 0x01 // Temporary storage while locked (current Hybrid scheme) | ||
| const val MAGIC_BYTE_SYMMETRIC: Byte = 0x02 // Re-encrypted after unlock (symmetric UDR scheme) | ||
|
|
||
| } |
There was a problem hiding this comment.
Generating a new EC P-521 key pair on every decryption operation to flush the binder buffer is extremely CPU-intensive and causes a severe performance bottleneck. We can optimize this by caching a single dummyPubKey lazily in the companion object and reusing it.
companion object {
private const val ANDROID_KEYSTORE = "AndroidKeyStore"
private const val KEY_PUBLIC_KEY_PREF = "master_public_key"
private const val EC_KEY_ALGORITHM = KeyProperties.KEY_ALGORITHM_EC
private const val KEY_AGREEMENT_ALGORITHM = "ECDH"
private const val DEK_ALGORITHM = "AES"
private const val DEK_WRAPPING_CIPHER = "AES/GCM/NoPadding"
private const val DEK_SIZE_BITS = 256
private const val DATA_CIPHER = "AES/GCM/NoPadding"
private const val GCM_TAG_LENGTH_BITS = 128
const val MAGIC_BYTE_ASYMMETRIC: Byte = 0x01 // Temporary storage while locked (current Hybrid scheme)
const val MAGIC_BYTE_SYMMETRIC: Byte = 0x02 // Re-encrypted after unlock (symmetric UDR scheme)
val dummyPubKey: PublicKey by lazy {
val kpg = KeyPairGenerator.getInstance("EC")
kpg.initialize(java.security.spec.ECGenParameterSpec("secp521r1"))
kpg.generateKeyPair().public
}
}| } else if (pubKey is ECPublicKey) { | ||
| // Check field size (P-384 has field size 384) | ||
| if (pubKey.params.curve.field.fieldSize < 384) { | ||
| throw CertificateException("EC key size must be 384 bits or greater") | ||
| } |
There was a problem hiding this comment.
For ECPublicKey, pubKey.params can theoretically return null if the parameters are implicitly defined. Accessing pubKey.params.curve directly without a null check could trigger a NullPointerException. Adding a null check makes the validator more robust.
| } else if (pubKey is ECPublicKey) { | |
| // Check field size (P-384 has field size 384) | |
| if (pubKey.params.curve.field.fieldSize < 384) { | |
| throw CertificateException("EC key size must be 384 bits or greater") | |
| } | |
| } else if (pubKey is ECPublicKey) { | |
| // Check field size (P-384 has field size 384) | |
| val params = pubKey.params | |
| if (params == null || params.curve.field.fieldSize < 384) { | |
| throw CertificateException("EC key size must be 384 bits or greater") | |
| } |
| return object : ByteArrayOutputStream() { | ||
| override fun close() { | ||
| val ciphertext = aead.encrypt(toByteArray(), providerHeader) | ||
| fileOutputStream.write(ciphertext) | ||
| fileOutputStream.close() | ||
| super.close() | ||
| } | ||
| } |
There was a problem hiding this comment.
If aead.encrypt throws an exception (e.g., due to a locked Keystore or cryptographic error), fileOutputStream.close() is never called, leading to a file descriptor leak. Wrapping the write and close operations in a try-finally block ensures the stream is always closed safely.
| return object : ByteArrayOutputStream() { | |
| override fun close() { | |
| val ciphertext = aead.encrypt(toByteArray(), providerHeader) | |
| fileOutputStream.write(ciphertext) | |
| fileOutputStream.close() | |
| super.close() | |
| } | |
| } | |
| return object : ByteArrayOutputStream() { | |
| override fun close() { | |
| try { | |
| val ciphertext = aead.encrypt(toByteArray(), providerHeader) | |
| fileOutputStream.write(ciphertext) | |
| } finally { | |
| fileOutputStream.close() | |
| super.close() | |
| } | |
| } | |
| } |
|
|
||
| override fun encryptStream(file: File): OutputStream { | ||
| val streamingAead = keyProvider.getStreamingAead() ?: throw UnsupportedOperationException("Streaming not supported") | ||
| val fileOutputStream = FileOutputStream(file) | ||
| fileOutputStream.write(providerHeader) | ||
| return streamingAead.newEncryptingStream(fileOutputStream, providerHeader) |
There was a problem hiding this comment.
If streamingAead.newEncryptingStream throws an exception, fileOutputStream is leaked. Wrapping the stream initialization in a try-catch block ensures that the stream is closed if the setup fails.
override fun encryptStream(file: File): OutputStream {
val streamingAead = keyProvider.getStreamingAead() ?: throw UnsupportedOperationException("Streaming not supported")
val fileOutputStream = FileOutputStream(file)
try {
fileOutputStream.write(providerHeader)
return streamingAead.newEncryptingStream(fileOutputStream, providerHeader)
} catch (e: Exception) {
fileOutputStream.close()
throw e
}
}| val keyPair = ephemeralKpg.generateKeyPair() | ||
|
|
There was a problem hiding this comment.
The generated ephemeral key pair is never assigned to ephemeralKeyPair in encryptSymmetric. This prevents the ephemeral private key from being cleared in the finally block, leaving sensitive key material in memory.
| val keyPair = ephemeralKpg.generateKeyPair() | |
| val ephemeralKpg = KeyPairGenerator.getInstance(EC_KEY_ALGORITHM).apply { initialize(ECGenParameterSpec("secp521r1")) } | |
| val keyPair = ephemeralKpg.generateKeyPair() | |
| ephemeralKeyPair = keyPair |
| import android.util.Base64 | ||
| import android.util.Log | ||
| import androidx.core.content.edit | ||
| import com.android.niapsec.encryption.tools.Asn1Helper |
Added new 2 librarariees for NIAP validations.