From 56c3290e1c6dda3028edaeb251a8dd31676513d9 Mon Sep 17 00:00:00 2001 From: lucas lelievre Date: Thu, 6 Nov 2025 01:07:26 +0100 Subject: [PATCH] Add digest to fw update files (#1616) Co-authored-by: gorbit99 --- .../firmware-tool/steps/SelectSourceStep.tsx | 2 +- .../firmware-update/FirmwareUpdate.tsx | 3 +- .../firmware-tool-api/firmwareToolSchemas.ts | 3 +- gui/src/hooks/firmware-tool.ts | 4 +- gui/src/hooks/firmware-update.ts | 12 ++- .../slimevr/firmware/FirmwareUpdateHandler.kt | 79 ++++++++++++------- solarxr-protocol | 2 +- 7 files changed, 68 insertions(+), 37 deletions(-) diff --git a/gui/src/components/firmware-tool/steps/SelectSourceStep.tsx b/gui/src/components/firmware-tool/steps/SelectSourceStep.tsx index cbf1120cd..e15e6d761 100644 --- a/gui/src/components/firmware-tool/steps/SelectSourceStep.tsx +++ b/gui/src/components/firmware-tool/steps/SelectSourceStep.tsx @@ -143,7 +143,7 @@ export function SelectSourceSetep({ !source.availableBoards.includes(partialBoard.board) || source.source !== partialBoard.source, name: source.version, - isBranch: !!source.branch, + isBranch: source.branch == source.version, }); return curr; diff --git a/gui/src/components/firmware-update/FirmwareUpdate.tsx b/gui/src/components/firmware-update/FirmwareUpdate.tsx index 42fb7e7d6..c8aeb80e0 100644 --- a/gui/src/components/firmware-update/FirmwareUpdate.tsx +++ b/gui/src/components/firmware-update/FirmwareUpdate.tsx @@ -222,7 +222,8 @@ export function FirmwareUpdate() { { isFirmware: true, firmwareId: '', - filePath: firmwareFile, + filePath: firmwareFile.url, + digest: firmwareFile.digest, offset: 0, }, ], diff --git a/gui/src/firmware-tool-api/firmwareToolSchemas.ts b/gui/src/firmware-tool-api/firmwareToolSchemas.ts index bb6f91644..8c498e7ac 100644 --- a/gui/src/firmware-tool-api/firmwareToolSchemas.ts +++ b/gui/src/firmware-tool-api/firmwareToolSchemas.ts @@ -26,7 +26,6 @@ export type DefaultsFile = { export type BoardDefaults = { values: void; - editable: string[]; flashingRules: { applicationOffset: number; needBootPress: boolean; @@ -59,6 +58,7 @@ export type BuildStatusDone = { files: { filePath: string; offset: number; + digest: string; isFirmware: boolean; firmwareId: string; }[]; @@ -94,6 +94,7 @@ export type FirmwareWithFiles = { files: { filePath: string; offset: number; + digest: string; isFirmware: boolean; firmwareId: string; }[]; diff --git a/gui/src/hooks/firmware-tool.ts b/gui/src/hooks/firmware-tool.ts index 836f72312..165d92f3c 100644 --- a/gui/src/hooks/firmware-tool.ts +++ b/gui/src/hooks/firmware-tool.ts @@ -122,6 +122,7 @@ export const getFlashingRequests = ( const part = new FirmwarePartT(); part.offset = 0; part.url = firmware.filePath; + part.digest = firmware.digest; const method = new OTAFirmwareUpdateT(); method.deviceId = dId; @@ -147,10 +148,11 @@ export const getFlashingRequests = ( method.needManualReboot = defaultConfig?.flashingRules.needManualReboot ?? false; - method.firmwarePart = firmwareFiles.map(({ offset, filePath }) => { + method.firmwarePart = firmwareFiles.map(({ offset, filePath, digest }) => { const part = new FirmwarePartT(); part.offset = offset; part.url = filePath; + part.digest = digest; return part; }); diff --git a/gui/src/hooks/firmware-update.ts b/gui/src/hooks/firmware-update.ts index 6d20f1f1f..bba5a1b61 100644 --- a/gui/src/hooks/firmware-update.ts +++ b/gui/src/hooks/firmware-update.ts @@ -8,7 +8,7 @@ export interface FirmwareRelease { name: string; version: string; changelog: string; - firmwareFiles: Partial>; + firmwareFiles: Partial>; userCanUpdate: boolean; } @@ -108,8 +108,14 @@ export async function fetchCurrentFirmwareRelease(): Promise, UpdateStatusEvent<*>> = ConcurrentHashMap() private val listeners: MutableList = CopyOnWriteArrayList() - private val firmwareCache = - InMemoryKache>(maxSize = 5 * 1024 * 1024) { - strategy = KacheStrategy.LRU - sizeCalculator = { _, parts -> parts.sumOf { it.firmware.size }.toLong() } - } private val mainScope: CoroutineScope = CoroutineScope(SupervisorJob()) private var clearJob: Deferred? = null @@ -297,21 +293,27 @@ class FirmwareUpdateHandler(private val server: VRServer) : ) try { - // We add the firmware to an LRU cache val toDownloadParts = getFirmwareParts(request) - val firmwareParts = - firmwareCache.getOrPut(toDownloadParts.joinToString("|") { "${it.url}#${it.offset}" }) { - withTimeoutOrNull(30_000) { - toDownloadParts.map { - val firmware = downloadFirmware(it.url) - ?: error("unable to download firmware part") - DownloadedFirmwarePart( - firmware, - it.offset, - ) - }.toTypedArray() - } + val firmwareParts = try { + withTimeoutOrNull(30_000) { + toDownloadParts.map { + val firmware = downloadFirmware(it.url, it.digest) + DownloadedFirmwarePart( + firmware, + it.offset, + ) + }.toTypedArray() } + } catch (e: Exception) { + onStatusChange( + UpdateStatusEvent( + deviceId, + FirmwareUpdateStatus.ERROR_DOWNLOAD_FAILED, + ), + ) + LogManager.severe("[FirmwareUpdateHandler] Unable to download firmware", e) + return@coroutineScope + } val job = launch { withTimeout(2 * 60 * 1000) { @@ -485,19 +487,38 @@ class FirmwareUpdateHandler(private val server: VRServer) : } } -fun downloadFirmware(url: String): ByteArray? { +fun downloadFirmware(url: String, expectedDigest: String): ByteArray { val outputStream = ByteArrayOutputStream() - try { - val chunk = ByteArray(4096) - var bytesRead: Int - val stream: InputStream = URL(url).openStream() - while (stream.read(chunk).also { bytesRead = it } > 0) { - outputStream.write(chunk, 0, bytesRead) - } - } catch (e: IOException) { - error("Cant download firmware $url") + val chunk = ByteArray(4096) + var bytesRead: Int + val stream: InputStream = URL(url).openStream() + while (stream.read(chunk).also { bytesRead = it } > 0) { + outputStream.write(chunk, 0, bytesRead) } - return outputStream.toByteArray() + val downloadedData = outputStream.toByteArray() + + if (!verifyChecksum(downloadedData, expectedDigest)) { + error("Checksum verification failed for $url") + } + + return downloadedData +} + +fun verifyChecksum(data: ByteArray, expectedDigest: String): Boolean { + val parts = expectedDigest.split(":", limit = 2) + if (parts.size != 2) { + error("Invalid digest format. Expected 'algorithm:hash' got $expectedDigest") + } + + val algorithm = parts[0].uppercase().replace("-", "") + val expectedHash = parts[1].lowercase() + + val messageDigest = MessageDigest.getInstance(algorithm) + val actualHash = messageDigest.digest(data).joinToString("") { + "%02x".format(it) + } + + return actualHash == expectedHash } diff --git a/solarxr-protocol b/solarxr-protocol index eed73567f..2fe6f9cf8 160000 --- a/solarxr-protocol +++ b/solarxr-protocol @@ -1 +1 @@ -Subproject commit eed73567f77df0c3b556206803f17ea0748bc761 +Subproject commit 2fe6f9cf8d775174336e559b3bc8e948f3da12c2