mirror of
https://github.com/sudosylabs/vnidrop.git
synced 2026-08-05 02:29:55 +02:00
fix(security): harden provider ACL, tickets, and local secrets
Close high-severity findings from the security review: default-deny blob gets for unmapped hashes, map collection members for ACL, fail closed on unknown access modes, require vnd1 tickets only, redact tickets from events, exclude Android app-data backups, and tighten secret-file creation.
This commit is contained in:
@@ -128,7 +128,7 @@
|
||||
<string name="transfer_event_saving">Saving files</string>
|
||||
<string name="transfer_event_connecting">Connecting to sender</string>
|
||||
<string name="ticket_details_title">Ticket details</string>
|
||||
<string name="ticket_no_metadata">This ticket does not include VniDrop metadata.</string>
|
||||
|
||||
<string name="settings_title">Settings</string>
|
||||
<string name="settings_subtitle">Configure the local node and app appearance.</string>
|
||||
<string name="node_title">Node</string>
|
||||
|
||||
@@ -77,8 +77,7 @@ data class TransferMetadataModel(
|
||||
|
||||
data class TicketInspectionModel(
|
||||
val kind: String,
|
||||
val blobTicket: String,
|
||||
val metadata: TransferMetadataModel?,
|
||||
val metadata: TransferMetadataModel,
|
||||
)
|
||||
|
||||
data class ReceiverRequestModel(
|
||||
|
||||
@@ -335,8 +335,7 @@ private fun ShareResult.toModel(): Share = Share(
|
||||
|
||||
private fun TicketInspection.toModel(): TicketInspectionModel = TicketInspectionModel(
|
||||
kind = kind,
|
||||
blobTicket = blobTicket,
|
||||
metadata = metadata?.toModel(),
|
||||
metadata = metadata.toModel(),
|
||||
)
|
||||
|
||||
private fun TransferMetadata.toModel(): TransferMetadataModel = TransferMetadataModel(
|
||||
|
||||
@@ -255,8 +255,8 @@ private fun InvitationReviewPanel(
|
||||
val metadata = inspection.metadata
|
||||
Surface(shape = RoundedCornerShape(14.dp), color = LocalVniDropColors.current.backgroundSurface200) {
|
||||
Column(Modifier.fillMaxWidth().padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text(metadata?.transferName ?: stringResource(Res.string.receive_unknown_transfer), fontWeight = FontWeight.Bold, maxLines = 2, overflow = TextOverflow.Ellipsis)
|
||||
if (metadata != null) Text("${metadata.fileCount} ${stringResource(Res.string.metadata_files).lowercase()} · ${formatBytes(metadata.totalSize)}", color = LocalVniDropColors.current.foregroundLighter)
|
||||
Text(metadata.transferName, fontWeight = FontWeight.Bold, maxLines = 2, overflow = TextOverflow.Ellipsis)
|
||||
Text("${metadata.fileCount} ${stringResource(Res.string.metadata_files).lowercase()} · ${formatBytes(metadata.totalSize)}", color = LocalVniDropColors.current.foregroundLighter)
|
||||
}
|
||||
}
|
||||
Field(state.receiverName, onReceiverNameChanged, stringResource(Res.string.field_receiver_name))
|
||||
|
||||
@@ -32,7 +32,6 @@ import vnidrop.shared.generated.resources.metadata_size
|
||||
import vnidrop.shared.generated.resources.metadata_transfer
|
||||
import vnidrop.shared.generated.resources.progress_title
|
||||
import vnidrop.shared.generated.resources.ticket_details_title
|
||||
import vnidrop.shared.generated.resources.ticket_no_metadata
|
||||
import vnidrop.shared.generated.resources.unknown_sender
|
||||
|
||||
@Composable
|
||||
@@ -60,15 +59,14 @@ fun ProgressSection(coreState: CoreState) {
|
||||
|
||||
@Composable
|
||||
fun TicketInspectionCard(inspection: TicketInspectionModel) {
|
||||
val metadata = inspection.metadata
|
||||
AppCard(title = stringResource(Res.string.ticket_details_title)) {
|
||||
MetadataRow(stringResource(Res.string.metadata_kind), inspection.kind)
|
||||
inspection.metadata?.let { metadata ->
|
||||
MetadataRow(stringResource(Res.string.metadata_transfer), metadata.transferName)
|
||||
MetadataRow(stringResource(Res.string.metadata_sender), metadata.senderName ?: stringResource(Res.string.unknown_sender))
|
||||
MetadataRow(stringResource(Res.string.metadata_files), metadata.fileCount.toString())
|
||||
MetadataRow(stringResource(Res.string.metadata_size), formatBytes(metadata.totalSize))
|
||||
MetadataRow(stringResource(Res.string.metadata_hash), metadata.contentHash)
|
||||
} ?: EmptyText(stringResource(Res.string.ticket_no_metadata))
|
||||
MetadataRow(stringResource(Res.string.metadata_transfer), metadata.transferName)
|
||||
MetadataRow(stringResource(Res.string.metadata_sender), metadata.senderName ?: stringResource(Res.string.unknown_sender))
|
||||
MetadataRow(stringResource(Res.string.metadata_files), metadata.fileCount.toString())
|
||||
MetadataRow(stringResource(Res.string.metadata_size), formatBytes(metadata.totalSize))
|
||||
MetadataRow(stringResource(Res.string.metadata_hash), metadata.contentHash)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -299,11 +299,7 @@ class ViewModelsTest {
|
||||
Dispatchers.setMain(StandardTestDispatcher(testScheduler))
|
||||
val core = FakeCoreGateway().apply {
|
||||
mutableState.value = mutableState.value.copy(isInitialized = true)
|
||||
inspectionResult = Result.success(com.vnidrop.app.core.TicketInspectionModel(
|
||||
kind = "vnidrop",
|
||||
blobTicket = "blob",
|
||||
metadata = com.vnidrop.app.core.TransferMetadataModel(1UL, "Photo", null, "hash", 1UL, 42UL),
|
||||
))
|
||||
inspectionResult = Result.success(sampleTicketInspection())
|
||||
}
|
||||
val viewModel = ReceiveViewModel(core, FakeFileSystemService(folder), preferences(), UiMessageController())
|
||||
advanceUntilIdle()
|
||||
@@ -379,13 +375,7 @@ class ViewModelsTest {
|
||||
Dispatchers.setMain(StandardTestDispatcher(testScheduler))
|
||||
val core = FakeCoreGateway().apply {
|
||||
mutableState.value = mutableState.value.copy(isInitialized = true)
|
||||
inspectionResult = Result.success(
|
||||
com.vnidrop.app.core.TicketInspectionModel(
|
||||
kind = "vnidrop",
|
||||
blobTicket = "blob",
|
||||
metadata = com.vnidrop.app.core.TransferMetadataModel(1UL, "Photo", null, "hash", 1UL, 42UL),
|
||||
),
|
||||
)
|
||||
inspectionResult = Result.success(sampleTicketInspection())
|
||||
}
|
||||
val viewModel = ReceiveViewModel(core, FakeFileSystemService(folder), preferences(), UiMessageController())
|
||||
advanceUntilIdle()
|
||||
@@ -408,13 +398,7 @@ class ViewModelsTest {
|
||||
Dispatchers.setMain(StandardTestDispatcher(testScheduler))
|
||||
val core = FakeCoreGateway().apply {
|
||||
mutableState.value = mutableState.value.copy(isInitialized = true)
|
||||
inspectionResult = Result.success(
|
||||
com.vnidrop.app.core.TicketInspectionModel(
|
||||
kind = "vnidrop",
|
||||
blobTicket = "blob",
|
||||
metadata = com.vnidrop.app.core.TransferMetadataModel(1UL, "Photo", null, "hash", 1UL, 42UL),
|
||||
),
|
||||
)
|
||||
inspectionResult = Result.success(sampleTicketInspection())
|
||||
receiveResult = Result.failure(IllegalStateException("sender refused"))
|
||||
}
|
||||
val viewModel = ReceiveViewModel(core, FakeFileSystemService(folder), preferences(), UiMessageController())
|
||||
@@ -489,13 +473,7 @@ class ViewModelsTest {
|
||||
Dispatchers.setMain(StandardTestDispatcher(testScheduler))
|
||||
val core = FakeCoreGateway().apply {
|
||||
mutableState.value = mutableState.value.copy(isInitialized = true)
|
||||
inspectionResult = Result.success(
|
||||
com.vnidrop.app.core.TicketInspectionModel(
|
||||
kind = "vnidrop",
|
||||
blobTicket = "blob",
|
||||
metadata = com.vnidrop.app.core.TransferMetadataModel(1UL, "Photo", null, "hash", 1UL, 42UL),
|
||||
),
|
||||
)
|
||||
inspectionResult = Result.success(sampleTicketInspection())
|
||||
// Keep receive suspended so dismiss can be asserted mid-transfer.
|
||||
receiveResult = Result.success(Unit)
|
||||
receiveSuspend = true
|
||||
@@ -544,6 +522,11 @@ class ViewModelsTest {
|
||||
updatedAt = 1L,
|
||||
)
|
||||
|
||||
private fun sampleTicketInspection() = com.vnidrop.app.core.TicketInspectionModel(
|
||||
kind = "vnidrop",
|
||||
metadata = com.vnidrop.app.core.TransferMetadataModel(1UL, "Photo", null, "hash", 1UL, 42UL),
|
||||
)
|
||||
|
||||
private companion object {
|
||||
val folder = ReceiveFolder(ReceiveFolderKind.FileSystemPath, "/tmp", "Downloads")
|
||||
}
|
||||
|
||||
@@ -61,7 +61,10 @@ class AppUiModelsTest {
|
||||
fun receiveStateExposesInspectAndReceiveEligibility() {
|
||||
val ready = ReceiveState(
|
||||
ticket = "ticket",
|
||||
inspection = com.vnidrop.app.core.TicketInspectionModel("vnidrop", "blob", null),
|
||||
inspection = com.vnidrop.app.core.TicketInspectionModel(
|
||||
kind = "vnidrop",
|
||||
metadata = com.vnidrop.app.core.TransferMetadataModel(1UL, "Photo", null, "hash", 1UL, 42UL),
|
||||
),
|
||||
folderAccessStatus = com.vnidrop.app.core.FolderAccessStatus.Writable,
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user