Skip to content

Add niap-android-cert-ext and sdp-file-encrypt libraries to niap-cc#58

Open
KVVat wants to merge 1 commit into
android:masterfrom
KVVat:niap-libararies-updates
Open

Add niap-android-cert-ext and sdp-file-encrypt libraries to niap-cc#58
KVVat wants to merge 1 commit into
android:masterfrom
KVVat:niap-libararies-updates

Conversation

@KVVat
Copy link
Copy Markdown
Contributor

@KVVat KVVat commented May 28, 2026

Added new 2 librarariees for NIAP validations.

  • niap-android-cert-ext
    • TLS/Cert Validator and X509 cert with EST client framework which compliat to the NIAP definitons.
    • docker files for mock server
  • sdp-file-encrypt
    • Encrypted file i/o libray which compliant to the NIAP definitions

@KVVat KVVat requested a review from mpgroover May 28, 2026 08:20
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +82 to +87
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")
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

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.

Suggested change
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")
}

Comment on lines +39 to +46
}
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)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-critical critical

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.

Suggested change
}
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)
}

Comment on lines +50 to +52
private var service: INiapCertManager? = null
private var latch = CountDownLatch(1)
private var isBinding = false
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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.

Suggested change
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

Comment on lines +118 to +120
if (deleteOriginal) {
sourceFile.delete()
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

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.")
                }
            }

Comment on lines +104 to +117
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)

}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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
        }
    }

Comment on lines +130 to +134
} 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")
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
} 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")
}

Comment on lines +45 to +52
return object : ByteArrayOutputStream() {
override fun close() {
val ciphertext = aead.encrypt(toByteArray(), providerHeader)
fileOutputStream.write(ciphertext)
fileOutputStream.close()
super.close()
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
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()
}
}
}

Comment on lines +72 to +77

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)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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
        }
    }

Comment on lines +449 to +450
val keyPair = ephemeralKpg.generateKeyPair()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The import of Asn1Helper is unused and can be safely removed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant