Skip to content

Simplify app naming#205

Merged
cy245 merged 1 commit into
mainfrom
update-dc-name
Jun 25, 2026
Merged

Simplify app naming#205
cy245 merged 1 commit into
mainfrom
update-dc-name

Conversation

@cy245

@cy245 cy245 commented Jun 25, 2026

Copy link
Copy Markdown
Collaborator

No description provided.

@cy245 cy245 requested a review from niharika2810 as a code owner June 25, 2026 18:00

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

Copy link
Copy Markdown

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 a sample Android application demonstrating the integration of the Digital Credentials API using Credential Manager, including custom CBOR and SD-JWT parsing utilities and a Jetpack Compose UI. The review feedback highlights several critical improvements: resolving potential memory leaks caused by capturing the Activity context in viewModelScope by shifting coroutine launching to the Composable's lifecycle, replacing unsafe casts on untrusted CBOR data with safe casts to prevent crashes, and addressing robustness issues in the CBOR encoder/decoder such as unhandled Float types, unsafe double-bang operators on nullable map entries, and integer overflow vulnerabilities for large array/map sizes.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +67 to +86
MainContent(
uiState = uiState,
onGetDigitalCredentialClick = {
val activity = context.findActivity()
if (activity != null) {
viewModel.getDigitalCredential {
CredentialManagerUtil.getDigitalCredential(activity)
}
}
},
onGetVerifiedEmailClick = {
val activity = context.findActivity()
if (activity != null) {
viewModel.getVerifiedEmailCredential {
CredentialManagerUtil.getVerifiedEmailCredential(activity)
}
}
},
modifier = modifier
)

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

Capturing the Activity context inside a lambda passed to the ViewModel's viewModelScope causes a memory leak. Since the ViewModel survives configuration changes (like screen rotation) but the Activity is destroyed and recreated, the running coroutine in viewModelScope will hold a strong reference to the destroyed Activity instance.

To fix this, launch the coroutine within the Composable's lifecycle using rememberCoroutineScope(). This ensures that the coroutine is cancelled when the Composable is disposed, preventing any memory leaks.

    val coroutineScope = rememberCoroutineScope()

    MainContent(
        uiState = uiState,
        onGetDigitalCredentialClick = {
            val activity = context.findActivity()
            if (activity != null) {
                coroutineScope.launch {
                    viewModel.updateUiState(MainUiState.Loading)
                    val result = CredentialManagerUtil.getDigitalCredential(activity)
                    viewModel.updateUiState(result)
                }
            }
        },
        onGetVerifiedEmailClick = {
            val activity = context.findActivity()
            if (activity != null) {
                coroutineScope.launch {
                    viewModel.updateUiState(MainUiState.Loading)
                    val result = CredentialManagerUtil.getVerifiedEmailCredential(activity)
                    viewModel.updateUiState(result)
                }
            }
        },
        modifier = modifier
    )

Comment on lines +31 to +53
/**
* Orchestrates a Digital Credential (e.g., mDL) request.
*
* @param requestBlock A suspend lambda that performs the retrieval and returns a [MainUiState].
*/
fun getDigitalCredential(requestBlock: suspend () -> MainUiState) {
viewModelScope.launch {
_uiState.value = MainUiState.Loading
_uiState.value = requestBlock()
}
}

/**
* Orchestrates a Verified Email credential request.
*
* @param requestBlock A suspend lambda that performs the retrieval and returns a [MainUiState].
*/
fun getVerifiedEmailCredential(requestBlock: suspend () -> MainUiState) {
viewModelScope.launch {
_uiState.value = MainUiState.Loading
_uiState.value = requestBlock()
}
}

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

To prevent memory leaks from capturing Activity contexts in viewModelScope, simplify the ViewModel to only manage the UI state. The actual coroutines should be launched from the Composable's lifecycle.

    /**
     * Updates the current UI state.
     */
    fun updateUiState(state: MainUiState) {
        _uiState.value = state
    }

Comment on lines +275 to +293
if (mdlNamespace is List<*>) {
for (item in mdlNamespace) {
val decodedItem = if (item is CborTag && item.tag == 24L) {
cborDecode(item.item as ByteArray)
} else {
item
}
(decodedItem as? Map<*, *>)?.let { extractClaim(it, claims) }
}
} else if (mdlNamespace is Map<*, *>) {
mdlNamespace.forEach { (k, v) ->
val value = if (v is CborTag && v.tag == 24L) {
cborDecode(v.item as ByteArray)
} else {
v
}
claims.add(CredentialClaim(formatLabel(k.toString()), value.toString()))
}
}

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

Unsafe casts (as ByteArray) on untrusted CBOR data can cause the application to crash with a ClassCastException if the parsed structure does not match expectations. Use safe casts (as? ByteArray) instead.

Suggested change
if (mdlNamespace is List<*>) {
for (item in mdlNamespace) {
val decodedItem = if (item is CborTag && item.tag == 24L) {
cborDecode(item.item as ByteArray)
} else {
item
}
(decodedItem as? Map<*, *>)?.let { extractClaim(it, claims) }
}
} else if (mdlNamespace is Map<*, *>) {
mdlNamespace.forEach { (k, v) ->
val value = if (v is CborTag && v.tag == 24L) {
cborDecode(v.item as ByteArray)
} else {
v
}
claims.add(CredentialClaim(formatLabel(k.toString()), value.toString()))
}
}
if (mdlNamespace is List<*>) {
for (item in mdlNamespace) {
val decodedItem = if (item is CborTag && item.tag == 24L) {
(item.item as? ByteArray)?.let { cborDecode(it) } ?: item
} else {
item
}
(decodedItem as? Map<*, *>)?.let { extractClaim(it, claims) }
}
} else if (mdlNamespace is Map<*, *>) {
mdlNamespace.forEach { (k, v) ->
val value = if (v is CborTag && v.tag == 24L) {
(v.item as? ByteArray)?.let { cborDecode(it) } ?: v
} else {
v
}
claims.add(CredentialClaim(formatLabel(k.toString()), value.toString()))
}
}

Comment on lines +134 to +147
if (data is Number) {
if (data is Double) {
throw IllegalArgumentException("Don't support doubles yet")
} else {
val value = data.toLong()
if (value >= 0) {
out.write(createArg(TYPE_UNSIGNED_INT, value))
return
} else {
out.write(createArg(TYPE_NEGATIVE_INT, -1 - value))
return
}
}
}

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

Since floats are not supported by this CBOR encoder, passing a Float will currently fall through to the else block, convert the float to a Long via toLong(), and silently encode it as an integer. This causes silent data loss. Explicitly throw an exception for Float as well.

Suggested change
if (data is Number) {
if (data is Double) {
throw IllegalArgumentException("Don't support doubles yet")
} else {
val value = data.toLong()
if (value >= 0) {
out.write(createArg(TYPE_UNSIGNED_INT, value))
return
} else {
out.write(createArg(TYPE_NEGATIVE_INT, -1 - value))
return
}
}
}
if (data is Number) {
if (data is Double || data is Float) {
throw IllegalArgumentException("Don't support floats or doubles yet")
} else {
val value = data.toLong()
if (value >= 0) {
out.write(createArg(TYPE_UNSIGNED_INT, value))
return
} else {
out.write(createArg(TYPE_NEGATIVE_INT, -1 - value))
return
}
}
}

Comment on lines +166 to +173
if (data is Map<*, *>) {
out.write(createArg(TYPE_MAP, data.size.toLong()))
for (i in data) {
encodeInternal(i.key!!, out, depth + 1)
encodeInternal(i.value!!, out, depth + 1)
}
return
}

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

Using the double-bang operator (!!) on map keys and values will throw a NullPointerException if any key or value is null. Since CBOR supports null values (encoded as major type 7), use safe destructuring and pass nullable values directly to encodeInternal.

Suggested change
if (data is Map<*, *>) {
out.write(createArg(TYPE_MAP, data.size.toLong()))
for (i in data) {
encodeInternal(i.key!!, out, depth + 1)
encodeInternal(i.value!!, out, depth + 1)
}
return
}
if (data is Map<*, *>) {
out.write(createArg(TYPE_MAP, data.size.toLong()))
for ((key, value) in data) {
encodeInternal(key, out, depth + 1)
encodeInternal(value, out, depth + 1)
}
return
}

Comment on lines +285 to +310
TYPE_ARRAY -> {
val ret = mutableListOf<Any?>()
var consumed = arg.len
for (i in 0 until arg.arg.toInt()) {
if (offset + consumed >= data.size) throw IllegalArgumentException("Unexpected end of data for array")
val item = parseItem(data, offset + consumed, depth + 1)
ret.add(item.item)
consumed += item.len
}
return Item(ret.toList(), consumed, TYPE_ARRAY)
}

TYPE_MAP -> {
val ret = mutableMapOf<Any?, Any?>()
var consumed = arg.len
for (i in 0 until arg.arg.toInt()) {
if (offset + consumed >= data.size) throw IllegalArgumentException("Unexpected end of data for map key")
val key = parseItem(data, offset + consumed, depth + 1)
consumed += key.len
if (offset + consumed >= data.size) throw IllegalArgumentException("Unexpected end of data for map value")
val value = parseItem(data, offset + consumed, depth + 1)
consumed += value.len
ret[key.item] = value.item
}
return Item(ret.toMap(), consumed, TYPE_MAP)
}

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 the CBOR data specifies an array or map size (arg.arg) larger than Int.MAX_VALUE, casting it to an Int via toInt() will overflow and become negative. This results in an empty range 0 until negative_value, causing the parser to silently return an empty list or map instead of throwing an error. Explicitly validate that the size does not exceed Int.MAX_VALUE.

            TYPE_ARRAY -> {
                if (arg.arg > Int.MAX_VALUE) throw IllegalArgumentException("Array size too large")
                val ret = mutableListOf<Any?>()
                var consumed = arg.len
                for (i in 0 until arg.arg.toInt()) {
                    if (offset + consumed >= data.size) throw IllegalArgumentException("Unexpected end of data for array")
                    val item = parseItem(data, offset + consumed, depth + 1)
                    ret.add(item.item)
                    consumed += item.len
                }
                return Item(ret.toList(), consumed, TYPE_ARRAY)
            }

            TYPE_MAP -> {
                if (arg.arg > Int.MAX_VALUE) throw IllegalArgumentException("Map size too large")
                val ret = mutableMapOf<Any?, Any?>()
                var consumed = arg.len
                for (i in 0 until arg.arg.toInt()) {
                    if (offset + consumed >= data.size) throw IllegalArgumentException("Unexpected end of data for map key")
                    val key = parseItem(data, offset + consumed, depth + 1)
                    consumed += key.len
                    if (offset + consumed >= data.size) throw IllegalArgumentException("Unexpected end of data for map value")
                    val value = parseItem(data, offset + consumed, depth + 1)
                    consumed += value.len
                    ret[key.item] = value.item
                }
                return Item(ret.toMap(), consumed, TYPE_MAP)
            }

@cy245 cy245 merged commit 29ba75b into main Jun 25, 2026
9 checks passed
@cy245 cy245 deleted the update-dc-name branch June 25, 2026 18:47
@cy245 cy245 restored the update-dc-name branch June 25, 2026 18:48
@cy245 cy245 deleted the update-dc-name branch June 25, 2026 18:49
@cy245 cy245 removed the request for review from niharika2810 June 25, 2026 18:53
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