Index: denali.pro =================================================================== diff -u -r380136967ba230affe91f614a9805319688eb05b -r20b370a54d2737831b307a0de82aec9e06e2b772 --- denali.pro (.../denali.pro) (revision 380136967ba230affe91f614a9805319688eb05b) +++ denali.pro (.../denali.pro) (revision 20b370a54d2737831b307a0de82aec9e06e2b772) @@ -42,6 +42,19 @@ # You can also select to disable deprecated APIs only up to a certain version of Qt. #DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 + +# For Update shared code, we're compiling for the UI: +DEFINES += COMPILING_ON_UI + +# This is targeting Qt 5.15 which relies on being binary +# compatible with openssl 1.1.1, most older versions require +# 1.0.0c. 1.1.1 must be installed on the target. +# Use: +# sudo apt-get install openssl +# Or maybe: +# sudo apt-get install libssl-dev +LIBS += -lssl -lcrypto + INCLUDEPATH += \ common \ sources \ @@ -77,6 +90,7 @@ sources/model/dg/data/disinfect \ sources/model/dg/adjustment \ sources/model/dg/adjustment/settings \ + sources/update \ sources/view \ sources/view/confirm \ sources/view/settings \ @@ -263,6 +277,19 @@ sources/gui/GuiGlobals.h \ sources/gui/GuiView.h \ sources/gui/GuiController.h \ + \ # SW Update + sources/update/EventWait.h \ + sources/update/HalStdTypes.h \ + sources/update/IDataProvider.h \ + sources/update/MsgLink.h \ + sources/update/Obfuscate.h \ + sources/update/Package.h \ + sources/update/SignRsa.h \ + sources/update/UiProtocol.h \ + sources/update/UiSwUpdate.h \ + sources/update/UiUpdateStatus.h \ + sources/update/UpdateProtocol.h \ + sources/update/VSwUpdate.h \ \ # ---------- Views ---------- sources/view/VTreatmentCreate.h \ sources/view/VEventSpy.h \ @@ -522,6 +549,15 @@ sources/gui/GuiGlobals.cpp \ sources/gui/GuiView.cpp \ sources/gui/GuiController.cpp \ + \ # Update + sources/update/Obfuscate.cpp \ + sources/update/Package.cpp \ + sources/update/PackageItem.cpp \ + sources/update/SignRsa.cpp \ + sources/update/UiProtocol.cpp \ + sources/update/UiSwUpdate.cpp \ + sources/update/UpdateProtocol.c \ + sources/update/VSwUpdate.cpp \ \ # ---------- Views ---------- sources/view/VTreatmentCreate.cpp \ sources/view/VEventSpy.cpp \ Index: denali.qrc =================================================================== diff -u -r559dc64d12cf7647ec8c29ccbf4ca90f90e5e3a2 -r20b370a54d2737831b307a0de82aec9e06e2b772 --- denali.qrc (.../denali.qrc) (revision 559dc64d12cf7647ec8c29ccbf4ca90f90e5e3a2) +++ denali.qrc (.../denali.qrc) (revision 20b370a54d2737831b307a0de82aec9e06e2b772) @@ -17,6 +17,7 @@ sources/gui/qml/pages/settings/SettingsBluetoothCuff.qml sources/gui/qml/pages/settings/SettingsExportLogs.qml sources/gui/qml/pages/settings/SettingsDeviceRegistration.qml + sources/gui/qml/pages/settings/SettingsSWUpdate.qml sources/gui/qml/dialogs/PowerOff.qml Index: sources/ApplicationController.cpp =================================================================== diff -u -rc9f8f8cf3c6c37fc6460d8675c62c9442c4d4263 -r20b370a54d2737831b307a0de82aec9e06e2b772 --- sources/ApplicationController.cpp (.../ApplicationController.cpp) (revision c9f8f8cf3c6c37fc6460d8675c62c9442c4d4263) +++ sources/ApplicationController.cpp (.../ApplicationController.cpp) (revision 20b370a54d2737831b307a0de82aec9e06e2b772) @@ -364,24 +364,28 @@ { // DEBUG: return; #ifndef DISABLE_KEEP_ALIVE - QVariantList mData; - int mFakeDataLen = gFakeData.length(); - // coco begin validated: This is a fake data generator for CANBus missing/swapped frames Testing - // will never be executed on the product - // has been tested manually - if (mFakeDataLen) { - if (gFakeSeqAtBegin) { - createFakeSeqAtBeginLongMessage(mData, mFakeDataLen); + // Update can disable this message from being sent and + // the queueing up a possible response expectation. + if (_keepAlive) { + QVariantList mData; + int mFakeDataLen = gFakeData.length(); + // coco begin validated: This is a fake data generator for CANBus missing/swapped frames Testing + // will never be executed on the product + // has been tested manually + if (mFakeDataLen) { + if (gFakeSeqAtBegin) { + createFakeSeqAtBeginLongMessage(mData, mFakeDataLen); + } + else { + createFakeSequencedLongMessage (mData, mFakeDataLen); + } } + // disabled coco end else { - createFakeSequencedLongMessage (mData, mFakeDataLen); + mData += static_cast(GuiActionData::NoData); } + onActionTransmit(GuiActionType::ID_KeepAlive, mData); } - // disabled coco end - else { - mData += static_cast(GuiActionData::NoData); - } - onActionTransmit(GuiActionType::ID_KeepAlive, mData); #endif } @@ -514,6 +518,11 @@ emit didSettingsInit({}); } +void ApplicationController::enableKeepAlive(bool vTurnOn) { + _keepAlive = vTurnOn; +} + + /*! * \brief ApplicationController::onSettingsInit * \details The slot which will be called to start the settings initialization in Application thread. @@ -594,6 +603,7 @@ * \details Sends the POST Final message */ void ApplicationController::onPOSTDone(bool vPass) { + emit didPOSTDone(vPass); // Used by Update SW. AdjustUIPostFinalResultRequestData data; data.mResult = vPass; emit didAdjustment(data); Index: sources/ApplicationController.h =================================================================== diff -u -rc9f8f8cf3c6c37fc6460d8675c62c9442c4d4263 -r20b370a54d2737831b307a0de82aec9e06e2b772 --- sources/ApplicationController.h (.../ApplicationController.h) (revision c9f8f8cf3c6c37fc6460d8675c62c9442c4d4263) +++ sources/ApplicationController.h (.../ApplicationController.h) (revision 20b370a54d2737831b307a0de82aec9e06e2b772) @@ -59,9 +59,12 @@ ApplicationPost _post; // I may need to be put in a concurrent. + bool _keepAlive = true; ///< Keep alive messaging on or off. public: void initSettings(); + void enableKeepAlive(bool vTurnOn); + public slots: bool init(); bool init(QThread &vThread); @@ -119,7 +122,7 @@ void didPOSTWiFi (bool vPass); void didPOSTBluetooth (bool vPass); void didPOSTCloudSync (bool vPass); - + void didPOSTDone (bool vPass); signals: void didActionReceive (GuiActionType vAction, const QVariantList &vData); // UI <= HD/DG void didActionTransmit(GuiActionType vAction, const QVariantList &vData); // UI => HD/DG Index: sources/canbus/CanInterface.cpp =================================================================== diff -u -rc9f8f8cf3c6c37fc6460d8675c62c9442c4d4263 -r20b370a54d2737831b307a0de82aec9e06e2b772 --- sources/canbus/CanInterface.cpp (.../CanInterface.cpp) (revision c9f8f8cf3c6c37fc6460d8675c62c9442c4d4263) +++ sources/canbus/CanInterface.cpp (.../CanInterface.cpp) (revision 20b370a54d2737831b307a0de82aec9e06e2b772) @@ -25,6 +25,7 @@ #include "Logger.h" #include "MessageGlobals.h" #include "FrameInterface.h" +#include "UiSwUpdate.h" // namespace using namespace Can; @@ -230,6 +231,62 @@ } /*! + * \brief Turn CAN driver SW mode on/off. + * + * \param on Turn the mode on (true) or off (false). + */ +void CanInterface::setSWUpdateMode(bool on) +{ + _SWUpdateMode = on; +} + +/*! + * \brief Send from UI. + * + * \param msgId The CAN message Id. + * \param pData The payload data. + * \param length Length in bytes. + */ +bool CanInterface::sendSWUpdateMsg(uint16_t msgId, uint8_t* pData, uint32_t length) +{ + bool ok = false; + + if (_canDevice && _SWUpdateMode) + { + int32_t slength = (int32_t)length; // Use signed math. + ok = true; + std::size_t offset = 0; + while (ok && (slength > 0)) + { + QCanBusFrame frame; + + // Always 8 bytes, zero the remaining. + QByteArray qb((qsizetype)8, (char)0); + uint32_t limit = slength > 8 ? 8 : slength; + + // Fill in the data. + for (std::size_t ii = 0; ii < limit; ii++) { + qb[(uint)ii] = pData[offset + ii]; + } + frame.setFrameId(msgId); + frame.setPayload(qb); + offset += 8; + slength -= 8; + + ok = _canDevice->writeFrame(frame); + { + if (ok) { + _txFrameCount++; + } else { + _erFrameCount++; + } + } + } + } + return ok; +} + +/*! * \brief CanInterface status * \details Sets the Can interface status description * \param vDescription - Description about the CANBus Interface errors @@ -262,7 +319,11 @@ // disabled coco begin validated: Manually tested since required to disable and enable the CANBus if( !_canDevice ) return false; //disabled coco end - return _canDevice->writeFrame(vFrame); + if (_SWUpdateMode) { + return true; + } else { + return _canDevice->writeFrame(vFrame); + } } /*! @@ -410,10 +471,23 @@ const QCanBusFrame frame = _canDevice->readFrame(); rxCount(); // disabled coco begin validated: This code is only for debugging purposes and had been tested manually. - if ( _enableConsoleOut ) + if ( _enableConsoleOut ) { consoleOut(frame, QString("Rx:%1").arg(_rxFrameCount)); - // disabled coco end - emit didFrameReceive(frame); + } + + if (_SWUpdateMode) { + + uint16_t msgId = frame.frameId(); + QByteArray qb = frame.payload(); + + uint8_t* pData = (uint8 *)qb.data(); + + SwUpdate::UiSwUpdate::instance().receive(msgId, pData); + } else { + + // disabled coco end + emit didFrameReceive(frame); + } } } @@ -428,8 +502,9 @@ bool ok = transmit(vFrame); txCount(); // disabled coco begin validated: This code is only for debugging purposes and had been tested manually. - if ( _enableConsoleOut ) + if ( _enableConsoleOut ) { consoleOut(vFrame, QString("Tx:%1").arg(_txFrameCount)); + } // disabled coco end emit didFrameTransmit(ok); } Index: sources/canbus/CanInterface.h =================================================================== diff -u -raf8d98b36b427e2b5f4d6659fcf3b58ee79eab6a -r20b370a54d2737831b307a0de82aec9e06e2b772 --- sources/canbus/CanInterface.h (.../CanInterface.h) (revision af8d98b36b427e2b5f4d6659fcf3b58ee79eab6a) +++ sources/canbus/CanInterface.h (.../CanInterface.h) (revision 20b370a54d2737831b307a0de82aec9e06e2b772) @@ -69,6 +69,8 @@ FrameCount _txFrameCount = 0; FrameCount _erFrameCount = 0; + bool _SWUpdateMode = false; + public slots: bool init(); bool init(QThread &vThread); @@ -79,6 +81,10 @@ void enableConsoleOut(bool vEnabled); void quitDevice(); + + void setSWUpdateMode(bool on); + + bool sendSWUpdateMsg(uint16_t msgId, uint8_t* pData, uint32_t length); private: void initConnections(); Index: sources/canbus/MessageDispatcher.cpp =================================================================== diff -u -rd949be21f2a9badd0978dddaaf436f6805de28dc -r20b370a54d2737831b307a0de82aec9e06e2b772 --- sources/canbus/MessageDispatcher.cpp (.../MessageDispatcher.cpp) (revision d949be21f2a9badd0978dddaaf436f6805de28dc) +++ sources/canbus/MessageDispatcher.cpp (.../MessageDispatcher.cpp) (revision 20b370a54d2737831b307a0de82aec9e06e2b772) @@ -1000,6 +1000,14 @@ // disabled coco end /*! + * \brief Get the sequence number, used by SW update to enter boot loader. + * \return sequence number value. + */ +uint16_t MessageDispatcher::getSequenceNumber() { + return (uint16_t)txCount(); +} + +/*! * \brief MessageDispatcher::txCount * \details count transmitted messages up the size of the Sequence type size * \return message count Index: sources/canbus/MessageDispatcher.h =================================================================== diff -u -r53b262fea2d4b5bd78ff878fda0848f3737e7b74 -r20b370a54d2737831b307a0de82aec9e06e2b772 --- sources/canbus/MessageDispatcher.h (.../MessageDispatcher.h) (revision 53b262fea2d4b5bd78ff878fda0848f3737e7b74) +++ sources/canbus/MessageDispatcher.h (.../MessageDispatcher.h) (revision 20b370a54d2737831b307a0de82aec9e06e2b772) @@ -171,6 +171,8 @@ public: void enableConsoleOut(bool vEnable) { _builder.enableConsoleOut(vEnable); } + uint16_t getSequenceNumber(); + private: void initConnections(); Index: sources/gui/GuiGlobals.cpp =================================================================== diff -u -r6d76149dca70e879f33bf6ed44203d0d06bc523a -r20b370a54d2737831b307a0de82aec9e06e2b772 --- sources/gui/GuiGlobals.cpp (.../GuiGlobals.cpp) (revision 6d76149dca70e879f33bf6ed44203d0d06bc523a) +++ sources/gui/GuiGlobals.cpp (.../GuiGlobals.cpp) (revision 20b370a54d2737831b307a0de82aec9e06e2b772) @@ -121,6 +121,8 @@ #include "VDisinfectAdjustDisinfect.h" // ----- #include "VTreatmentAdjustmentEnd.h" +// ----- SW Update +#include "VSwUpdate.h" namespace Gui { Index: sources/gui/qml/main.qml =================================================================== diff -u -r7c86f3854db9ad02d95681203198d75a0d65c9fa -r20b370a54d2737831b307a0de82aec9e06e2b772 --- sources/gui/qml/main.qml (.../main.qml) (revision 7c86f3854db9ad02d95681203198d75a0d65c9fa) +++ sources/gui/qml/main.qml (.../main.qml) (revision 20b370a54d2737831b307a0de82aec9e06e2b772) @@ -115,6 +115,9 @@ // Confirm import VConfirm 0.1 +//SW Update +import VSWUpdate 0.1 + // Qml imports import "qrc:/globals" import "qrc:/pages" @@ -235,6 +238,9 @@ // --- Disinfection VDisinfectAdjustDisinfect { id: vDisinfectAdjustDisinfect } + // --- Update + VSWUpdate { id: vSWUpdate } + Background {} // ----- Follow the below Z order ----- Index: sources/gui/qml/pages/settings/SettingsSWUpdate.qml =================================================================== diff -u --- sources/gui/qml/pages/settings/SettingsSWUpdate.qml (revision 0) +++ sources/gui/qml/pages/settings/SettingsSWUpdate.qml (revision 20b370a54d2737831b307a0de82aec9e06e2b772) @@ -0,0 +1,244 @@ +/*! + * + * Copyright (c) 2023-2023 Diality Inc. - All Rights Reserved. + * \copyright + * THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN + * WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. + * + * \file SettingsSWUpdate.qml + * \author (last) Philip Braica + * \date (last) 25-Apr-2023 + * \author (original) Philip Braica + * \date (original) 25-Apr-2023 + * + */ + +// Qt +import QtQuick 2.12 + +// Project +import Gui.Actions 0.1 + +// Qml imports +import "qrc:/globals" +import "qrc:/components" +import "qrc:/compounds" +import "qrc:/pages" + +import Qt.labs.qmlmodels 1.0 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.3 +import QtQuick.Controls.Styles 1.4 + +/*! + * \brief SW Update screen + * The software update screen. + */ +SettingsBase { id : _root + itemIndex : SettingsStack.SWUpdate + confirmVisible : false + + // Note that this layout keeps the "start" button above + // where the alert bar would be shown. + // + // If a previous update failed in an odd way, alerts may + // be generated but the user can still select a new update + // target and try to fix things. + + readonly property int spacing : 20 // Spacing between controls. + readonly property int topMargin : 160 // Top margin. + readonly property int upperSpace : 160 // Height of the Table. + readonly property int lowerSpace : 200 // Height of the description. + readonly property int rowHeight : 40 // Row height. + readonly property int headerRowHeight : 30 // Header height. + readonly property int cellWidth1 : 240 // First cell width. + readonly property int cellWidth2 : 180 // Second cell width. + readonly property int cellWidth3 : 80 // Third cell width. + readonly property int cellWidth4 : 360 // Forth cell width. + readonly property int scrollWidth : 40 // Scrollbar width. + readonly property int totalWidth : cellWidth1 + cellWidth2 + cellWidth3 + cellWidth4 + scrollWidth + readonly property int buttonWidth : 350 // Button width. + readonly property int buttonHeight : 50 // Button height. + readonly property int progressHeight : 40 // Progress bar height. + + Column { id: _swu_column + spacing : 0 + y : _root.topMargin + anchors.horizontalCenter : parent.horizontalCenter + width : _root.totalWidth + height : _root.headerRowHeight + _root.upperSpace + + Row { + height : _root.headerRowHeight + 1 + width : _root.totalWidth + spacing : 0 + clip : true + + Rectangle { + id : _header1 + border.width : 1 + height : _root.headerRowHeight + width : _root.cellWidth1 + color : "#4290EC" + Text { + text : "File name" + anchors.centerIn : parent + } + } + + Rectangle { + id : _header2 + border.width : 1 + height : _root.headerRowHeight + width : _root.cellWidth2 + color : "#4290EC" + Text { + text : "Date" + anchors.centerIn : parent + } + } + + Rectangle { + id : _header3 + border.width : 1 + height : _root.headerRowHeight + width : _root.cellWidth3 + color : "#4290EC" + Text { + text : "Kind" + anchors.centerIn : parent + } + } + + Rectangle { + id : _header4 + border.width : 1 + height : _root.headerRowHeight + width : _root.cellWidth4 + _root.scrollWidth + color : "#4290EC" + Text { + text : "Update title" + anchors.centerIn : parent + } + } + } + + TableView { + id : _tableView + y : _root.topMargin + _root.headerRowHeight + width : _root.totalWidth + height : _root.upperSpace + columnSpacing : 0 + rowSpacing : 0 + clip : true + + columnWidthProvider : function (column) { + if (column === 0) return _root.cellWidth1; + if (column === 1) return _root.cellWidth2; + if (column === 2) return _root.cellWidth3; + if (column === 3) return _root.cellWidth4; + return _root.scrollWidth; + } + + rowHeightProvider : function (row) { return _root.rowHeight; } + + ScrollBar.vertical : ScrollBar { + width : _root.scrollWidth + height : _root.scrollWidth + policy : ScrollBar.AlwaysOn + } + + // ScrollIndicator.vertical : ScrollIndicator { } + + model: vSWUpdate + + delegate: Rectangle { + required property var model + + id : _cell + + Text { + text : displayRole + anchors.centerIn : parent + font.pixelSize : 18 + } + + color: colorRole + + MouseArea { + anchors.fill : parent + onClicked: { + vSWUpdate.rowClicked(row); + } + } + } + + } + + // Empty piece. + Item { + // spacer item + width: 1 + height: _root.spacing + } + + // Status text. + TextArea { + id : _information + width : _root.totalWidth + height : _root.lowerSpace + readOnly : true + selectByMouse : false + selectByKeyboard : false + wrapMode : TextEdit.WordWrap + text : vSWUpdate.stateData + font.pixelSize : 14 // 18 likely top limit. + } + + // Empty piece. + Item { + width: 1 + height: _root.spacing + 10 + } + + ProgressBarEx { + id : _progressBar + value : vSWUpdate.percent + height : _root.progressHeight + width : _root.totalWidth + //visible : vSWUpdate.progressVisible + unit : "%" + decimal : 2 + minimum : 0 + maximum : 100 + } + + // Empty piece. + Item { + width: 1 + height: _root.spacing * 2 + } + + // Button. + TouchRect { + id : _startStopButton + anchors.horizontalCenter: parent.horizontalCenter + width : _root.buttonWidth + height : _root.buttonHeight + enabled : true // ! device device registered + onClicked : vSWUpdate.startStopBtnClicked() + + text { + text : vSWUpdate.buttonText + font.bold : true + font.pixelSize : Fonts.fontPixelButton + } + + } + + onVisibleChanged: { + vSWUpdate.visibilityChanged(visible, this); + } + } + notificationText: "" +} Index: sources/gui/qml/pages/settings/SettingsStack.qml =================================================================== diff -u -r69b3aa965cd8187c66114a9753c162cc526d9678 -r20b370a54d2737831b307a0de82aec9e06e2b772 --- sources/gui/qml/pages/settings/SettingsStack.qml (.../SettingsStack.qml) (revision 69b3aa965cd8187c66114a9753c162cc526d9678) +++ sources/gui/qml/pages/settings/SettingsStack.qml (.../SettingsStack.qml) (revision 20b370a54d2737831b307a0de82aec9e06e2b772) @@ -85,7 +85,7 @@ false , // Language false , // Calibration true , // DeviceRegistration - false , // SWUpdate + true , // SWUpdate false , // FactoryReset ] property var itemsVisible : [ @@ -101,7 +101,7 @@ false /* serviceMode phase 1 */ , // Language false /* serviceMode phase 1 */ , // Calibration true /* serviceMode */ , // DeviceRegistration // FIXME: On the normal setting menu for now for development. - false /* serviceMode phase 1 */ , // SWUpdate + true /* serviceMode phase 1 */ , // SWUpdate false /* serviceMode phase 1 */ , // FactoryReset ] @@ -162,6 +162,10 @@ push( _settingsDeviceRegistration ) break + case SettingsStack.SWUpdate: + push ( _settingsSWUpdate ) + break + default: console.debug("Unknown Index", vIndex) break @@ -333,6 +337,8 @@ SettingsDeviceRegistration { id: _settingsDeviceRegistration } + SettingsSWUpdate { id: _settingsSWUpdate } + UserConfirmation { id: _servicePassword property bool isPassword_Accepted : false property bool isDefaultPasswordSet : (vSettings.servicePass != "") Index: sources/update/EventWait.h =================================================================== diff -u --- sources/update/EventWait.h (revision 0) +++ sources/update/EventWait.h (revision 20b370a54d2737831b307a0de82aec9e06e2b772) @@ -0,0 +1,93 @@ +/*! + * + * Copyright (c) 2023 Diality Inc. - All Rights Reserved. + * + * \copyright + * THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN + * WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. + * + * \file EventWait.h + * \author (last) Phil Braica + * \date (last) 23-Jan-2023 + * \author (original) Phil Braica + * \date (original) 23-Jan-2023 + * + */ + +#ifndef EVENT_WAIT_H_ +#define EVENT_WAIT_H_ + +#include +#include +#include + +/*! + * An object that waits till condition variable fired OR timeout. + */ +class EventWait { + +public: + /*! \brief Constructor. */ + EventWait(uint32 msec = 1) : + _msec(msec < MaxPeriodicMsec ? msec : MaxPeriodicMsec), + _fired(false) { + } + + /*! \brief Destructor. */ + virtual ~EventWait() { + this->fireEvent(); + } + + /** + *! \brief Re-arm for refire. + */ + virtual void re_arm() { + _fired = false; + } + + /*! + * \brief Wait. + * + * \return True if a message received, false if timed out. + */ + virtual bool wait() { + std::unique_lock lock(_mutex); + + // Note the current implimentation using wait_for could + // be replaced with a secondary event waking thread and then + // use a smaller wakeup, but the added overhead would reduce + // any benefit. This is suitable for events in the 1-2ms + // regime for retries such as for CAN bus traffic. + const std::chrono::duration ms_sleep(_msec); + return _cv.wait_for(lock, ms_sleep, [&] { return this->_fired; }); + } + + /*! + * \brief Sleep for a given number of milliseconds. + * + * \param msec Milliseconds, this is a uint8 so no more than 256ms. + */ + virtual void sleep(uint8 msec) { + const std::chrono::duration ms_sleep(msec); + std::this_thread::sleep_for(ms_sleep); + } + + /*! \brief Fire event. */ + virtual void fireEvent() { + std::lock_guard lock(_mutex); + _fired = true; + _cv.notify_all(); + } + +protected: + // This guards against any API / memory leaks causing a potential + // deadlocked or extremely slow thread response. + static const uint32 MaxPeriodicMsec = 1000; ///< Max allowed periodic. + + std::mutex _mutex; ///< Mutex. + std::condition_variable _cv; ///< Condition to fire. + uint32 _msec; ///< Msec to sleep. + bool _fired; ///< Event fired. +}; + +#endif // EVENT_WAIT_H_ Index: sources/update/HalStdTypes.h =================================================================== diff -u --- sources/update/HalStdTypes.h (revision 0) +++ sources/update/HalStdTypes.h (revision 20b370a54d2737831b307a0de82aec9e06e2b772) @@ -0,0 +1,197 @@ +/** @file hal_stdtypes.h +* @brief HALCoGen standard types header File +* @date 11-Dec-2018 +* @version 04.07.01 +* +* This file contains: +* - Type and Global definitions which are relevant for all drivers. +*/ + +/* +* Copyright (C) 2009-2018 Texas Instruments Incorporated - www.ti.com +* +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions +* are met: +* +* Redistributions of source code must retain the above copyright +* notice, this list of conditions and the following disclaimer. +* +* Redistributions in binary form must reproduce the above copyright +* notice, this list of conditions and the following disclaimer in the +* documentation and/or other materials provided with the +* distribution. +* +* Neither the name of Texas Instruments Incorporated nor the names of +* its contributors may be used to endorse or promote products derived +* from this software without specific prior written permission. +* +* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +* +*/ + + +#ifndef __HAL_STDTYPES_H__ +#define __HAL_STDTYPES_H__ + +#include +#include + +/* USER CODE BEGIN (0) */ +/* USER CODE END */ +/************************************************************/ +/* Type Definitions */ +/************************************************************/ +#ifndef _UINT64_DECLARED +typedef uint64_t uint64; +#define _UINT64_DECLARED +#endif + +#ifndef _UINT32_DECLARED +typedef uint32_t uint32; +#define _UINT32_DECLARED +#endif + +#ifndef _UINT16_DECLARED +typedef uint16_t uint16; +#define _UINT16_DECLARED +#endif + +#ifndef _UINT8_DECLARED +typedef uint8_t uint8; +#define _UINT8_DECLARED +#endif + +#ifndef _BOOLEAN_DECLARED +#ifdef __cplusplus +typedef bool boolean; +#else +typedef _Bool boolean; +#endif +#define _BOOLEAN_DECLARED +#endif + +#ifndef _SINT64_DECLARED +typedef int64_t sint64; +#define _SINT64_DECLARED +#endif + +#ifndef _SINT32_DECLARED +typedef int32_t sint32; +#define _SINT32_DECLARED +#endif + +#ifndef _SINT16_DECLARED +typedef int16_t sint16; +#define _SINT16_DECLARED +#endif + +#ifndef _SINT8_DECLARED +typedef int8_t sint8; +#define _SINT8_DECLARED +#endif + +#ifndef _FLOAT32_DECLARED +typedef float float32; +#define _FLOAT32_DECLARED +#endif + +#ifndef _FLOAT64_DECLARED +typedef double float64; +#define _FLOAT64_DECLARED +#endif + +typedef unsigned int BOOL; ///< 32-bit boolean type. + +typedef uint8 Std_ReturnType; + +typedef struct +{ + uint16 vendorID; + uint16 moduleID; + uint8 instanceID; + uint8 sw_major_version; + uint8 sw_minor_version; + uint8 sw_patch_version; +} Std_VersionInfoType; + +/*****************************************************************************/ +/* SYMBOL DEFINITIONS */ +/*****************************************************************************/ +#ifndef STATUSTYPEDEFINED + #define STATUSTYPEDEFINED + #define E_OK 0x00U + + typedef unsigned char StatusType; +#endif + +#ifndef E_NOT_OK +#define E_NOT_OK 0x01U +#endif + +#ifndef STD_ON +#define STD_ON 0x01U +#endif + +#ifndef STD_OFF +#define STD_OFF 0x00U +#endif + + +/************************************************************/ +/* Global Definitions */ +/************************************************************/ +/** @def NULL +* @brief NULL definition +*/ + +#ifndef NULL + /*SAFETYMCUSW 218 S MR:20.2 "Custom Type Definition." */ + #define NULL ((void *) 0U) +#endif + +/*****************************************************************************/ +/* Define: NULL_PTR */ +/* Description: Void pointer to 0 */ +/*****************************************************************************/ +#ifndef NULL_PTR + #define NULL_PTR ((void *)0x0) +#endif + +/** @def TRUE +* @brief definition for TRUE +*/ +#ifndef TRUE + #define TRUE true +#endif + +/** @def FALSE +* @brief BOOLEAN definition for FALSE +*/ +#ifndef FALSE + #define FALSE false +#endif + +/*****************************************************************************/ +/* Define: NULL_PTR */ +/* Description: Void pointer to 0 */ +/*****************************************************************************/ +#ifndef NULL_PTR +#define NULL_PTR ((void *)0x0U) +#endif + +/* USER CODE BEGIN (1) */ +/* USER CODE END */ + +#endif /* __HAL_STDTYPES_H__ */ Index: sources/update/IDataProvider.h =================================================================== diff -u --- sources/update/IDataProvider.h (revision 0) +++ sources/update/IDataProvider.h (revision 20b370a54d2737831b307a0de82aec9e06e2b772) @@ -0,0 +1,302 @@ +/*! + * + * Copyright (c) 2023 Diality Inc. - All Rights Reserved. + * + * \copyright + * THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN + * WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. + * + * \file IDataProvider.h + * \author (last) Phil Braica + * \date (last) 23-Jan-2023 + * \author (original) Phil Braica + * \date (original) 23-Jan-2023 + * + */ +#ifndef DATA_PROVIDER_H_ +#define DATA_PROVIDER_H_ + +#include "HalStdTypes.h" +#include "UpdateProtocol.h" + +#include +#include + +namespace SwUpdate { + +/*! + * \brief Provides a call to fetch data for update. + */ +class IDataProvider { +public: + /*! + * \brief Constructor. + * + * \param trgtName Target name: LINUX, UI, DG, HD, DGFPGA, HDGFPGA + * \param totalSize Total size in bytes. + * \param verifyToken Verification token value. + * \param versionToken Version token value. + * \param destination File destination. + */ + IDataProvider( + SwUpdateTargetEnum trgtName, + uint32 totalSize, + uint32 verifyToken, + uint32 versionToken, + const std::string & destination) : + + targetName(trgtName), + fileDestination(destination), + totalSize(totalSize), + verifyData(verifyToken), + versionData(versionToken) { + + } + + /*! \brief Virtual destructor. */ + virtual ~IDataProvider() { + ; // NOP. + } + + /*! + * \brief Read data for an update. + * + * Calls into a class that has a call like: + * std::size_t read(FILE* pFile, std::size_t cat_index, std::size_t offset, + * unsigned char* pBuffer, std::size_t size_bytes); + * but wraps it to only what the Protocol needs. + * + * Fetches can be any arbitrary byte offset and do not need to be in order. + * + * \param offset Offset into the image. + * \param pBuffer Buffer to use. + * \param size_bytes Max bytes to return. + * + * \return The amount of bytes fetched / ran out of data to fill the buffer. + */ + virtual std::size_t read(std::size_t offset, unsigned char* pBuffer, std::size_t size_bytes) = 0; + + /*! + * \brief Copy a file to a given destination. + * + * \param destination File path to copy the stream to. + * + * \return True on success. + */ + virtual bool copyFile(const std::string& destination = "") { + // Use whole pages for fast allocation and copies and such. + constexpr int MEMORY_PAGE_SIZE = 4096; + unsigned char pBuffer[MEMORY_PAGE_SIZE]; + + // First two error checks are done in sequence, is our source invalid? + // Then create the dest and check if that's invalid. + bool rv = true; + + // Use our fileDestination set at constructor if we don't override. + FILE* pDest = fopen( + destination.size() != 0 ? destination.c_str() : fileDestination.c_str(), + "wb"); + + std::size_t copied = 0; + if (pDest != nullptr) { + + rv = true; + while (rv) { + try { + std::size_t sz_r = read(copied, pBuffer, MEMORY_PAGE_SIZE); + if (sz_r == 0) { + break; + } else { + decrypt(pBuffer, sz_r); + std::size_t sz_w = fwrite(pBuffer, 1, sz_r, pDest); + if (sz_w != sz_r) { + rv = false; + } + copied += sz_w; + } + } catch (...) { + rv = false; + } + } + + // Close it. + rv &= (fclose(pDest) == 0); + } + + // Make sure we copied all of it. + rv &= copied == totalSize; + + return rv; + } + + SwUpdateTargetEnum targetName; ///< Either LINUX, UI, DG, HD, DGFPGA, HDGFPGA + std::string fileDestination; ///< File target destination. + uint32 totalSize; ///< Total size in bytes. + uint32 verifyData; ///< Verify security token. + uint32 versionData; ///< Version security token. + +protected: + + /*! + * \brief Decrypt data in place. + * + * \param pBuffer Buffer to use. + * \param size_bytes Max bytes to return. + */ + void decrypt(unsigned char* pBuffer, std::size_t size_bytes) { + const unsigned char obs = 71; // Randomly picked value. + for (uint32 ii = 0; ii < size_bytes; ii++) { + pBuffer[ii] -= obs; + } + } + +}; + +/*! + * \brief IDataProvider from a file. + */ +class DataProvider_File : public IDataProvider { +public: + /*! + * \brief Constructor. + * + * \param pFile File pointer. + * \param trgtName Target name: LINUX, UI, DG, HD, DGFPGA, HDGFPGA + * \param fileOffset Offset to where this data starts in the file. + * \param totalSize Total size in bytes. + * \param verifyToken Verification token value. + * \param versionToken Version token value. + */ + DataProvider_File( + FILE* pFile, + SwUpdateTargetEnum trgtName, + uint32 fileOffset, + uint32 totalSize, + uint32 verifyToken, + uint32 versionToken, + const std::string& destination) : + + IDataProvider(trgtName, totalSize, verifyToken, versionToken, destination), + _pFile(pFile), + _fileOffset(fileOffset) { + + ; // NOP. + } + + /*! \brief Virtual destructor. */ + virtual ~DataProvider_File() { + ; // NOP. + } + + /*! + * \brief Read data for an update. + * + * \param offset Offset into the image. + * \param pBuffer Buffer to use. + * \param size_bytes Max bytes to return. + * + * \return The amount of bytes fetched / ran out of data to fill the buffer. + */ + virtual std::size_t read(std::size_t offset, unsigned char* pBuffer, std::size_t size_bytes) { + + // Only let ONE thing use the file at once, that way seek and read can't interfer. + + static std::mutex mutex; + std::lock_guard lock(mutex); + + // Arg check. + if ((_pFile == nullptr) || (pBuffer == nullptr)) { + return 0; + } + + // If total_size <= offset then asking for something past the end + // of this stream in the file. + // + // Otherwise limit the size to no more than max_bytes - offset. + size_bytes = + totalSize <= offset ? 0 : + totalSize - offset > size_bytes ? size_bytes : totalSize - offset; + + // Fold in the offset from startData of this file and the startData position. + offset += _fileOffset; + + // Seek and read what we can. + fseek(_pFile, (long)offset, 0); + std::size_t rd_sz = fread(pBuffer, 1, size_bytes, _pFile); + decrypt(pBuffer, rd_sz); + return rd_sz; + } + +protected: + FILE * _pFile; ///< File pointer. + uint32 _fileOffset; ///< Offset to start in that file. +}; + + +/*! + * \brief IDataProvider from a buffer. + */ +class DataProvider_Buffer : public IDataProvider { +public: + /*! + * \brief Constructor. + * + * \param pData Buffer pointer. + * \param trgtName Target name: LINUX, UI, DG, HD, DGFPGA, HDGFPGA + * \param totalSize Total size in bytes. + * \param verifyToken Verification token value. + * \param versionToken Version token value. + */ + DataProvider_Buffer( + unsigned char* pData, + SwUpdateTargetEnum trgtName, + uint32 totalSize, + uint32 verifyToken, + uint32 versionToken, + const std::string& destination) : + + IDataProvider(trgtName, totalSize, verifyToken, versionToken, destination), + _pData(pData) { + + ; // NOP. + } + + /*! \brief Virtual destructor. */ + virtual ~DataProvider_Buffer() { + ; // NOP. + } + + /*! + * \brief Read data for an update. + * + * \param offset Offset into the image. + * \param pBuffer Buffer to use. + * \param size_bytes Max bytes to return. + * + * \return The amount of bytes fetched / ran out of data to fill the buffer. + */ + virtual std::size_t read(std::size_t offset, unsigned char* pBuffer, std::size_t size_bytes) { + + // If total_size <= offset then asking for something past the end + // of this stream in the file. + // + // Otherwise limit the size to no more than max_bytes - offset. + size_bytes = + totalSize <= offset ? 0 : + totalSize - offset > size_bytes ? size_bytes : totalSize - offset; + + // Fold in the offset from startData of this file and the startData position. + // Note that for internal tests we don't encrypt/decrypt the byte stream. + memcpy(pBuffer, &_pData[offset], size_bytes); + return size_bytes; + } + +protected: + unsigned char* _pData; ///< Data pointer. +}; + +} // namespace SwUpdate + +#endif // DATA_PROVIDER_H_ + + Index: sources/update/MsgLink.h =================================================================== diff -u --- sources/update/MsgLink.h (revision 0) +++ sources/update/MsgLink.h (revision 20b370a54d2737831b307a0de82aec9e06e2b772) @@ -0,0 +1,307 @@ +/*! + * + * Copyright (c) 2023 Diality Inc. - All Rights Reserved. + * + * \copyright + * THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN + * WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. + * + * \file MsgLink.h + * \author (last) Phil Braica + * \date (last) 23-Jan-2023 + * \author (original) Phil Braica + * \date (original) 23-Jan-2023 + * + */ + +#ifndef MSG_LINK_H_ +#define MSG_LINK_H_ + +#include "EventWait.h" +#include "UpdateProtocol.h" +#include "Logger.h" +#include "CanInterface.h" +#include + +#ifdef __cplusplus +extern "C" { +#endif + +// Send call. +extern bool sendPacketFromUi(uint16 msgId, uint8* pData, uint32 length); + +#ifdef __cplusplus +} +#endif + +namespace SwUpdate { + +/*! + * Heuristic tuned value, typically it is 2x or more the wire + * rate for most CAN systems. + * + * /note This is a tunable heuristic. + */ +const float BandwidthFactor = 2.0f; + +/*! + * The total amount of payload bytes to round trip data + response. + */ +const uint32 TotalRoundTripDataSizeBytes = + (sizeof(SwUpdateDataBuffer) + sizeof(SwUpdateResponse)); + +/*! + * For each 8 bytes of payload, 114 wire bits are + * used (header + payload + tail). + */ +const float BytesToCanBits = 114.0f / 8.0f; + +/*! + * Pause after a NACK before sending reset and retry. + */ +const std::chrono::duration g_ms_pause(20); + +/*! + * This is 2x the line speed transfer of a + * message relative to kbps of the line in milliseconds. + * + * Example: + * (sizeof(SwUpdateDataBuffer) + sizeof(SwUpdateResponse))/8) = 34. + * 34 packets * 114 bits per packet = 3,876 bits. + * / 250kpbs = 15.504 ms if full bandwidth for 250kbps. + * * 2 for collisions/normal stuff = + * Expected round trip within 31 ms else we should retry. + * + */ +const float RetryKbpsScaleFactor = + TotalRoundTripDataSizeBytes * BytesToCanBits * BandwidthFactor; + +/*! + * \brief Send a command and wait for response. + * + * Typical use is wait on bool sendOk(...), and if it failed then + * the effort timed out for retries. + * + * The numberRetries() call checks for too much cumulative failures + * which indicates the bus is improperly being used by other nodes + * while attempting the software update or cyber attack. + */ +class MsgLink { +public: + + /*! + * \brief Constructor. + */ + MsgLink(float kbps) : + _ew((uint32)(60 + (RetryKbpsScaleFactor / kbps))), + _expecting(false), + _retries(0), + _expectedId(0), + _msgIdSlot(0), + _wasData(false), + _dataBuf({0,0,0,0, {0}}) { + + } + + /*! + * \brief Virtual destructor. + */ + virtual ~MsgLink() { + _expecting = false; + } + + /*! + * \brief Get the number of message retries. + */ + uint32 numberRetries() { + return _retries; + } + + /*! + * \brief Reset the number of retries. + */ + void resetRetries() { + _retries = 0; + } + + /*! + * \brief Abort waiting. + */ + void abort() { + _expecting = false; + _ew.fireEvent(); + } + + /*! + * \brief Send a message, wait as needed for retry/response. + * + * \param msgId The message + * \param msg The message bytes, already with security. + * \param maxEffort Maximum number of 1ms retries. + * + * \return True on success, False on retry failed or timeout. + */ + bool sendOk( + uint16 msgId, + uint8 * msg, + uint16 maxEffort) { + + // We are expecting data. + _expecting = true; + + _wasData = + (msgId == SwUpdateCanMsgIds::DataBufferId_FW) || + (msgId == SwUpdateCanMsgIds::DataBufferId_FPGA); + + // Either command or data. if data use ResponseId, + // if a command and verify or version use VerifyId else ResponseId. + _expectedId = + _wasData ? + SwUpdateCanMsgIds::ResponseId : + SwUpdate_getCmd(msg[1]) == SwUpdateCmdEnum::Verify ? + SwUpdateCanMsgIds::VerifyId : + SwUpdate_getCmd(msg[1]) == SwUpdateCmdEnum::Version ? + SwUpdateCanMsgIds::VerifyId : ResponseId; + + _msgIdSlot = msg[0]; // For ack/nack matching. + + // Sending a command (8 bytes) or data (256). + uint32 length = _wasData ? + sizeof(SwUpdateDataBuffer) : sizeof(SwUpdateCommand); + + if (_wasData) { + memcpy(&_dataBuf, msg, sizeof(SwUpdateDataBuffer)); + } + + //const uint32 bytes_per_packet = 8; + for (uint32 ii = 0; ii < maxEffort; ii++) { + + // No longer expecting! + if (!_expecting) { + break; + } + + _ew.re_arm(); + + // Call indirectly to make this more testable. + this->sendPacket(msgId, msg, length); + + // Wait to either retry or success. + if (_ew.wait()) { + _expecting = false; + return true; + } + _retries++; + + // If we're still expecting then we do a retry. + // Before retry send a command that should clear us. + if (_expecting && _wasData) { + SwUpdateCommand resync; + resync.id = _msgIdSlot + 1; + SwUpdateTargetEnum target = (SwUpdateTargetEnum)(((SwUpdateDataBuffer *)msg)->kind); + resync.cmd = SwUpdate_FormCommand(target, SwUpdateCmdEnum::Resync); + resync.rand = 0; + SwUpdate_createSecurity((uint8 *)&resync, sizeof(SwUpdateCommand)); + + // Either we could wait for the retry ACK or not. + // For now do not, its unclear what would be ideal and either ought to work. + #if 0 + uint16 old_expectedId = _expectedId; + uint16 old_msgIdSlot = _msgIdSlot; + bool oldWasData = _wasData; + _expectedId = SwUpdateCanMsgIds::ResponseId; + _msgIdSlot = resync.id; + _wasData = false; + + // Sleep, arm, send. + std::this_thread::sleep_for(g_ms_pause); + _ew.re_arm(); + this->sendPacket(SwUpdateCanMsgIds::CommandId, (uint8*)&resync, 8); + _ew.wait(); + + // Reset state and then sleep a bit. + _expectedId = old_expectedId; + _msgIdSlot = old_msgIdSlot; + _wasData = oldWasData; + std::this_thread::sleep_for(g_ms_pause); + + #else + std::this_thread::sleep_for(g_ms_pause); + this->sendPacket(SwUpdateCanMsgIds::CommandId, (uint8*)&resync, 8); + std::this_thread::sleep_for(g_ms_pause); + #endif + + } + } + + _expecting = false; + // swUpdate_log("MsgLink::sendOk failed."); + return false; + } + + /*! + * \brief Send a packet. + * + * \param mstId Messge Id. + * \param pData Message data. + * \param length Message length. + */ + void sendPacket(uint16 msgId, uint8* pData, uint32 length) { + _CanInterface.sendSWUpdateMsg(msgId, pData, length); + } + + /*! + * \brief Receive and check if matched. + * + * \param msgId Can message ID. + * \param pData Data pointer. + * + * \return True if we received the expected message. + */ + bool checkReceived(uint16 msgId, uint8 * pData) { + bool rv = false; + + if (_expecting) { + if (_wasData) { + rv = SwUpdate_checkSecurityForData(pData, (uint8*)&_dataBuf); + } else { + if ((SwUpdate_verifySecurity(pData, sizeof(SwUpdateResponse))) && + (msgId == _expectedId)) { + + // msgId resp->id is ack | rv + // is resp match + // F * * | T Its a verify/version + // T F * | F Not for this thread + // T T F | F For the thread not an ack. + // T T T | T For this thread and an ack. + if (msgId == SwUpdateCanMsgIds::ResponseId) { + SwUpdateResponse *resp = (SwUpdateResponse *)pData; + rv = ((resp->id == _msgIdSlot) && (resp->ackNack == 1)); + } else { + rv = true; + } + } + } + // It's good? fire event. + if (rv) { + _ew.fireEvent(); + } + } + return rv; + } + +protected: + EventWait _ew; ///< Wait for time or event. + bool _expecting; ///< Expecting data. + uint32 _retries; ///< Total number of retries. + uint16 _expectedId; ///< Expected message Id. + uint8 _msgIdSlot; ///< Message slot. + + bool _wasData; ///< Was data. + SwUpdateDataBuffer _dataBuf; ///< Data buffer. + +}; + +} // namespace SwUpdate + +#endif // MSG_LINK_H_ Index: sources/update/Obfuscate.cpp =================================================================== diff -u --- sources/update/Obfuscate.cpp (revision 0) +++ sources/update/Obfuscate.cpp (revision 20b370a54d2737831b307a0de82aec9e06e2b772) @@ -0,0 +1,123 @@ +/*! + * \brief + * + * \copyright Copyright (c) 2023 Diality Inc. - All Rights Reserved. + * + * \file Obfuscate.cpp + * + * \author PBraica + * \date March 2023 + */ + +#include "Obfuscate.h" +#include /* time to seed secret. */ + +#define HEADER_SIZE (2) +#define COPRIME_1 (19) +#define COPRIME_2 (23) +#define ROT_VAL (211) // Change to 211. + +/*! + * \brief Constructor. + */ +Obfuscate::Obfuscate() { + _secret = (unsigned char)time(NULL) % 0xFF; +} + +/*! + * \brief Virtual destructor. + */ +Obfuscate::~Obfuscate() { + ; // NOP. +} + +/*! + * \brief Encode the bytes in data. + * + * \param data The data to encode. + * + * \return Encoded bytes in a string, unlikely printable. + */ +std::string Obfuscate::encode(const std::string& data) { + + unsigned char s = _secret; + std::string rv = std::string(data.size() + HEADER_SIZE, ' '); + std::size_t ii = 0; + rv[ii++] = s % COPRIME_1; + rv[ii++] = s % COPRIME_2; + + for (char c : data) { + rv[ii++] = (c + s) % 256; + s += ROT_VAL; + } + + return rv; +} + +/*! + * \brief Decode the bytes in data. + * + * \param data The string to decode. + * \param is_start True if we need to recover the secret. + * + * \return Decoded bytes in a string, printable if the original was. + */ +std::string Obfuscate::decode(const std::string & data, bool is_start) { + return decode(&data[0], data.size(), is_start); +} + +/*! + * \brief Decode the bytes in pdata. + * + * \return Decoded bytes in a string, printable if the original was. + */ +std::string Obfuscate::decode(const char* pData, std::size_t size_bytes, bool is_start) { + std::string rv; + std::size_t headerSize = (is_start ? HEADER_SIZE : 0); + if (size_bytes <= headerSize) { + // Either really no data OR not enough input data. + return rv; + } + + bool found = false; + int s = 0; + + if (is_start) { + // static int decoder[ROT_VAL / COPRIME_1]; + unsigned int a = (unsigned int)pData[0]; + unsigned int b = (unsigned int)pData[1]; + + unsigned short code = (a << 8) + b; + for (s = 0; s < 0xFF; s++) { + unsigned short test = ((s % COPRIME_1) << 8) + (s % COPRIME_2); + if (code == test) { + _secret = s; + found = true; + break; + } + } + } + + if (!found) { + // Given an invalid code pair. + return rv; + } + rv.resize(size_bytes - headerSize, ' '); + for (std::size_t ii = headerSize; ii < size_bytes; ii++) { + char d = pData[ii]; + rv[ii - headerSize] = (d - s) % 256; + s += ROT_VAL; + } + _secret = s % 256; + return rv; + +} + +/*! + * \brief Get the header size. + * + * \return Header size in bytes. + */ +std::size_t Obfuscate::headerSize() { + return HEADER_SIZE; +} Index: sources/update/Obfuscate.h =================================================================== diff -u --- sources/update/Obfuscate.h (revision 0) +++ sources/update/Obfuscate.h (revision 20b370a54d2737831b307a0de82aec9e06e2b772) @@ -0,0 +1,40 @@ +/*! + * \brief + * + * \copyright 2023 Diality Inc. - All Rights Reserved. + * + * \file Obfuscate.h + * + * \author PBraica + * \date March 2023 + */ + +#ifndef OBFUSCATE_H_ +#define OBFUSCATE_H_ + +#include + +/*! + * \brief A way to obfuscate data to make it harder to detect patterns / read it. + * + * It increases size by 2 bytes. That header is a random chosen simplistic encryption + * scheme. This can decode the whole thing at once or stream it in pieces. + */ +class Obfuscate { +public: + Obfuscate(); + + virtual ~Obfuscate(); + + std::string encode(const std::string& data); + + std::string decode(const std::string& data, bool is_start = true); + std::string decode(const char * pData, std::size_t size_bytes, bool is_start = true); + + static std::size_t headerSize(); + +protected: + unsigned char _secret; ///< Current encode secret. +}; + +#endif // OBFUSCATE_H_ Index: sources/update/Package.cpp =================================================================== diff -u --- sources/update/Package.cpp (revision 0) +++ sources/update/Package.cpp (revision 20b370a54d2737831b307a0de82aec9e06e2b772) @@ -0,0 +1,851 @@ +/*! + * + * Copyright (c) 2023 Diality Inc. - All Rights Reserved. + * + * \copyright + * THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN + * WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. + * + * \file Package.cpp + * \author (last) Phil Braica + * \date (last) 23-Jan-2023 + * \author (original) Phil Braica + * \date (original) 23-Jan-2023 + */ + +#include "Package.h" +#include "Obfuscate.h" +#include +#include "IDataProvider.h" +#include "UiSwUpdate.h" + +#include +#include +#include +#include +#include + +/*! + * Must be greater than a signature, and big enough to be good for streaming. + * + * A signature is base64'd and may be padded but it is 3/2 * key length in bytes plus the start/end text. + * We expect 344 + 34 * 2 = 412 for the buffer. + * + * For streaming it makes sense to use at least one memory block (4096) + */ +#define DEFAULT_BUF_SIZE (8192) + +#define MIN_SIGNATURE_SIZE (344) ///< Min size of a signature, can be a little bigger (padding, whitespace). +#define MAX_WHITESPACE_SIZE (40) ///< Max whitespace. + +#define MAX_RECURSION (3) ///< We assume someone might put the file within a few folders on the USB by accident. + +#define DELETE_RETRIES (4) ///< Upto 4 attempts at deleting a file if there is a race condition. + +const std::string g_sig_start = "SIGNATURE_START==================="; ///< Start signature. +const std::string g_sig_end = "SIGNATURE_END==================="; ///< End signature. + + +/*! + * \brief Constructor. + */ +Package::Package() : + _items(), + _rsa(), + _packageName(""), + _packageVers(""), + _signature(""), + _xml(""), + _info(""), + _startXml(0), + _startData(0), + _pFileUpdating(nullptr) { + + ; // NOP. +} + +/*! + * \brief Virtual destructor.. + */ +Package::~Package() { + this->abort(); +} + +/*! + * \brief Parse the file. + * + * Note that key_location default to "", which means use the already loaded key. + * This is useful for scanning files in multiple directories such as checking a USB file. + * + * \param fileName Filename. + * \param key_location The folder containing the public key, can contain that file's name. + */ +bool Package::parse(const std::string& fileName, const std::string& key_location) { + + bool verifiedOk = false; + FILE* fp = NULL; + _lastFileName = fileName; + try + { + clear(); + fp = fopen(fileName.c_str(), "rb"); + if (fp == NULL) { + _info = "Could not open binary file: " + fileName + "\n"; + return false; + } + if (key_location.length() != 0) { + _rsa.load_keys(key_location); + } + verifiedOk = parseInternal(fp); + } catch(...) { + ; // NOP. + } + + if (fp != NULL) { + fclose(fp); + } + return verifiedOk; +} + +/*! + * \brief What are the targets available for the just parsed file? + * + * \return Vector of files that are parsable. + */ +std::vector Package::targetsAvailable() { + std::vector rv; + for (PackageItem pi : _items) { + SwUpdateTargetEnum update_type = + pi._fileType == "HD" ? HD : + pi._fileType == "HDFPGA" ? HDFPGA : + pi._fileType == "DG" ? DG : + pi._fileType == "DGFPGA" ? DGFPGA : + pi._fileType == "UI" ? UI : LINUX; + + if (std::find(rv.begin(), rv.end(), update_type) == rv.end()) { + rv.push_back(update_type); + } + } + return rv; +} + +/*! + * \brief Start updating, key is a string. + * + * \param fileName File name to verify then use. + * \param public_key The public key file content. + * \param update The desired update targets, if empty use ALL. + * + * \return True on successfully started. + */ +bool Package::start_hasKey( + const std::string& fileName, + const std::vector& update) +{ + _lastFileName = fileName; + this->abort(); // Blocks till anything in progress is finished. + clear(); + + _pFileUpdating = NULL; + try { + // Open the file, keep it open during the update process. + // + // This prevents any other process or driver from altering the file + // during the update. It is a cyber security feature. + _pFileUpdating = fopen(fileName.c_str(), "rb"); + if (_pFileUpdating == NULL) { + int err = errno; + _info = "Could not open binary file: " + fileName + ", errno=" + std::to_string(err) + "\n"; + return false; + } + + // Now do a verify. This ensures that nothing altered the file between the time of + // the user selecting it from the UI and the application and made it invalid. + bool verifiedOk = parseInternal(_pFileUpdating); + if (verifiedOk) { + + // Go through the items, create data providers. + for (const PackageItem& pi : _items) { + SwUpdateTargetEnum update_type = + pi._fileType == "HD" ? HD : + pi._fileType == "HDFPGA" ? HDFPGA : + pi._fileType == "DG" ? DG : + pi._fileType == "DGFPGA" ? DGFPGA : + pi._fileType == "UI" ? UI : LINUX; + bool isFileCopy = (update_type == UI) || (update_type == LINUX); + if ((std::find(update.begin(), update.end(), update_type) != update.end()) || (update.size() == 0)) { + SwUpdate::DataProvider_File dp( + _pFileUpdating, + update_type, + (uint32)(this->_startData + pi._byteOffset), + pi._size, + pi._security, + pi._version, + isFileCopy ? pi._destPath : ""); + + _streams.push_back(dp); + } + } + } + } catch(...) { + if (_pFileUpdating != NULL) { + fclose(_pFileUpdating); + _pFileUpdating = NULL; + } + return false; + } + + std::vector sv; + for (std::size_t ii = 0; ii < _streams.size(); ii++) { + sv.push_back(&_streams[ii]); + } + return SwUpdate::UiSwUpdate::instance().start(sv); +} + +/*! + * \brief Start updating. + * + * \param fileName File name to verify then use. + * \param key_location The folder containing the public key, can contain that file's name. + * \param update The desired update targets, if empty use ALL. + * + * \return True on successfully started. + */ +bool Package::start( + const std::string & fileName, + const std::string& key_location, + const std::vector& update) { + + _lastFileName = fileName; + this->abort(); // Blocks till anything in progress is finished. + clear(); + + _pFileUpdating = fopen(fileName.c_str(), "rb"); + if (_pFileUpdating == NULL) { + int err = errno; + _info = "Could not open binary file: " + fileName + ", errno=" + std::to_string(err) + "\n"; + return false; + } + _rsa.load_keys(key_location); + bool verifiedOk = parseInternal(_pFileUpdating); + if (!verifiedOk) { + fclose(_pFileUpdating); + _pFileUpdating = NULL; + _info += "Could not verify update package integrity: " + fileName + "\n"; + return false; + } + for (const PackageItem& pi : _items) { + SwUpdateTargetEnum update_type = + pi._fileType == "HD" ? HD : + pi._fileType == "HDFPGA" ? HDFPGA : + pi._fileType == "DG" ? DG : + pi._fileType == "DGFPGA" ? DGFPGA : + pi._fileType == "UI" ? UI : LINUX; + bool isFileCopy = (update_type == UI) || (update_type == LINUX); + if ((std::find(update.begin(), update.end(), update_type) != update.end()) || (update.size() == 0)) { + SwUpdate::DataProvider_File dp( + _pFileUpdating, + update_type, + (uint32)(this->_startData + pi._byteOffset), + pi._size, + pi._security, + pi._version, + isFileCopy ? pi._destPath : ""); + + _streams.push_back(dp); + } + } + std::vector sv; + for (std::size_t ii = 0; ii < _streams.size(); ii++) { + sv.push_back(&_streams[ii]); + } + + return SwUpdate::UiSwUpdate::instance().start(sv); +} + +/*! + * \brief Is it completed? + * + * \return True if completed. + */ +bool Package::completedAll() { + bool completed_all = SwUpdate::UiSwUpdate::instance().completedAll(); + if (completed_all) { + _streams.clear(); + + // Close file pointer. + if (_pFileUpdating != nullptr) { + try { + fclose(_pFileUpdating); + } + catch (...) { + ; // NOP. + } + _pFileUpdating = nullptr; + } + } + return completed_all; +} + +/*! + * \brief Abort update. + */ +void Package::abort() { + SwUpdate::UiSwUpdate::instance().abort(); + + // Now safe to discard these stream objects. + _streams.clear(); + + // Close file pointer. + if (_pFileUpdating != nullptr) { + try { + fclose(_pFileUpdating); + } + catch (...) { + ; // NOP. + } + _pFileUpdating = nullptr; + } +} + +/*! + * \brief Current estimated progress. + * + * \return Vector of statuses. + */ +std::vector Package::progress() { + bool completed_all = SwUpdate::UiSwUpdate::instance().completedAll(); + + if (completed_all) { + _streams.clear(); + + // Close file pointer. + if (_pFileUpdating != nullptr) { + try { + fclose(_pFileUpdating); + } + catch (...) { + ; // NOP. + } + _pFileUpdating = nullptr; + } + } + + return SwUpdate::UiSwUpdate::instance().progress(); +} + +/*! + * \brief Parse the file. + * + * \param pFile File object. + */ +bool Package::parseInternal(FILE* pFile) { + + // Update status. + _info = "Reading ..\n"; + + // Read the key. + if (_rsa.public_exists() == false) { + _info += " Could not open key file.\n"; + return false; + } + else { + _info += " Read key file.\n"; + } + + // Get the signature. + std::string signature = get_signature(pFile); + if (signature.length() == 0) { + return false; + } + + // Verify the file. + fseek(pFile, (long)_startXml, 0); + bool verified_ok = _rsa.verify_file(pFile, signature); + + if (verified_ok) { + updateCatalog(pFile); + } + else { + + this->_info += " Verified failed.\n"; + } + return verified_ok; +} + +/*! + * \brief Clear our state out. + */ +void Package::clear() { + this->_items.clear(); + this->_packageName = ""; + this->_packageVers = ""; + this->_signature = ""; + this->_xml = ""; + this->_startXml = 0; + this->_startData = 0; +} + +/*! + * \brief Get the signature. + * + * \param pFile File pointer. + * + * \return Signature string. + */ +std::string Package::get_signature(FILE* pFile) { + char buf[DEFAULT_BUF_SIZE]; + std::size_t sz = fread(buf, 1, DEFAULT_BUF_SIZE, pFile); + std::string signature(buf, sz); + std::size_t sig_start = signature.find(g_sig_start); + + if (sig_start == std::string::npos) { + _info += " Could not find signature start\n."; + return ""; + } + sig_start += g_sig_start.size(); + std::size_t sig_end = signature.find(g_sig_end, sig_start); + if (sig_end == std::string::npos) { + _info += " Could not find signature end\n."; + return ""; + } + signature = signature.substr(sig_start, sig_end - sig_start); + if (signature.length() < MIN_SIGNATURE_SIZE) { + _info += " Signature was too small. \n"; + return ""; + } + if (signature.length() > MIN_SIGNATURE_SIZE + MAX_WHITESPACE_SIZE) { + _info += " Signature was too big. \n"; + return ""; + } + _info += " Signature found ok, was " + std::to_string(signature.length()) + " bytes.\n"; + _startXml = sig_end + g_sig_end.size(); + return signature; +} + + +/*! + * \brief Get the signature. + * + * \param pFile File pointer. + * + * \return Signature string. + */ +void Package::updateCatalog(FILE *pFile) { + Obfuscate ob; + bool isFirst = true; + // bool found = false; + std::string xml_str; + fseek(pFile, (long)_startXml, 0); + char pBuf[DEFAULT_BUF_SIZE]; + std::string endTag = ""; + while (true) { + std::size_t sz = fread(pBuf, 1, DEFAULT_BUF_SIZE, pFile); + if (sz == 0) + break; + xml_str += ob.decode(pBuf, sz, isFirst); + isFirst = false; + std::size_t xml_end = xml_str.find(endTag); + if (xml_end != std::string::npos) { + //found = true; + xml_str = xml_str.substr(0, xml_end + endTag.size()); + break; + } + if (sz < DEFAULT_BUF_SIZE) { + break; + } + } + + this->_packageName = PackageItem::unwrap(xml_str, "name"); + this->_packageVers = PackageItem::unwrap(xml_str, "version"); + this->_xml = xml_str; + this->_items = PackageItem::fromXml(xml_str); + this->_startData = this->_startXml + xml_str.length() + Obfuscate::headerSize(); + this->_info += " Found " + std::to_string(this->_items.size()) + " items.\n"; + this->_info += " Verified ok.\n"; +} + +/** + * \brief Get the list of available things in the directory. + * + * \param directory Directory to scan (not recursive). + * + * \return vector of tupple: filename, fullNameAndPath, packagename, version + */ +std::vector Package::get_available(const std::string& directory) +{ + std::vector rv; + this->get_availableInternal(directory, rv); + return rv; +} + +/** + * \brief Get the list of available things in many directories. + * + * \param directories Directories to scan (not recursive). + * + * \return vector of tupple: filename, fullNameAndPath, packagename, version + */ +std::vector Package::get_available(const std::vector& directories) +{ + std::vector rv; + for (const std::string directory : directories) { + this->get_availableInternal(directory, rv); + } + + return rv; +} + +/** + * \brief Copy / cache it (if not already in there). + * + * \param fullNamePath The package being installed. + * \param cacheDirectory Cache location. + * \param maxCacheSize Maximum number of cached packages in cacheDirectory. + * + * \return True on success. + */ +bool Package::copyUpdate(const std::string& fullNamePath, const std::string& cacheDirectory, uint32 maxCacheSize) +{ + // Ensure the cache directory exists, if not try to create it. + // If we can't create it, then just return false. + try + { + if (std::filesystem::is_directory(cacheDirectory) == false) + { + std::filesystem::create_directory(cacheDirectory); + } + } + catch (...) + { + return false; + } + + // Nothing to do? + if (fullNamePath.find(cacheDirectory) == 0) { + // Already there, nothing to do. + return true; + } + + // Find the name. + std::string fileName = ""; + for (const PackageInfo& pi : _packages) + { + // Try to find that entry. + if (pi.fullPath == fullNamePath) + { + fileName = pi.fileName; + break; + } + } + if (fileName.size() == 0) + { + // Race condition, no longer exists, nothing to do. + return false; + } + + // If inCache > maxCacheSize, remove the oldest till enough room. + // + // We allow upto 4 removals because in theory an old version of + // software may have allowed more or less than + // the latest limit. + for (std::size_t ii = 0; ii < 4; ii++) + { + uint inCache = 0; + std::vector::iterator oldest = _packages.end(); + + for (std::vector::iterator it = _packages.begin(); + it != _packages.end(); + it++) + { + // Sum up the entries. + if (it->fullPath.find(cacheDirectory) == 0) + { + if (inCache == 0) + { + oldest = it; + } else { + if (oldest->lastWritten > oldest->lastWritten) + { + oldest = it; + } + } + inCache++; + } + } + + if ((inCache >= maxCacheSize) && (oldest != _packages.end())) + { + // Remove the oldest, we already know it is not the same as the selected. + bool deleted = false; + for (std::size_t jj = 0; jj < DELETE_RETRIES; jj++) { + try + { + int deletedVal = std::remove(oldest->fullPath.c_str()); + if (deletedVal == 0) { + deleted = true; + break; + } + + // Sleep 250 ms hoping what ever had this open releases it. + using namespace std::chrono_literals; + std::this_thread::sleep_for(250ms); + } catch (...) { + // Fail as we might not have room for it! + return false; + } + } + if (deleted) { + _packages.erase(oldest); + } + else + { + break; + } + } else { + break; + } + } + + // Now copy it. + try + { + // There could be another file of the same name there, so we use a number to get a unique one. + // + // As there can't be more than maxCacheSize there (assuming delete worked) we scan through + // no more than maxCacheSize*2. + std::size_t dot = fileName.find('.'); + std::string baseName = fileName; + std::string suffix = ""; + if (dot > 1) { + baseName = fileName.substr(0, dot); + suffix = fileName.substr(dot); + } + std::string newName = cacheDirectory + "/" + fileName; + + for (std::size_t ii = 0; ii < maxCacheSize * 2; ii++) { + if (std::filesystem::exists(newName)) + { + newName = cacheDirectory + "/" + baseName + "_" + std::to_string(ii) + suffix; + } else { + break; + } + } + std::filesystem::copy_file(fullNamePath, newName); + } + catch (...) { + // Fail as we might not have room for it! + return false; + } + + // Update _packages. + for (PackageInfo& pi : _packages) + { + // Try to find that entry. + if (pi.fullPath == fullNamePath) + { + pi.fullPath = cacheDirectory + "/" + fileName; + pi.lastWritten = std::filesystem::last_write_time(pi.fullPath); + break; + } + } + return true; +} + +/*! + * \brief If a script exists, there can be but one, get it's destination. + * + * \return Destination location of the script. + */ +std::string Package::getScriptIfAny() { + for (const PackageItem& pi : _items) + { + if (pi._isScript) { + return pi._destPath; + } + } + return ""; +} + +/*! + * \brief Scan a directory recursively for regular files and avoid links. + * + * \param directory The directory to scan. + * \param depth Depth remaining to scan. + * \param values Structure to append into. + * + * \return List of files (full path) that are regular files. + */ +void Package::recurseDirectory( + const std::string& directory, + int depth, + std::vector& values) +{ + std::vector rv; + + if (depth < 0) { + return; + } + + depth--; + + // Use try to ignore things at the directory and file level. + // + // If one of the directories doesn't exist OR no permission, + // then that's odd but we could be trying to restore via update + // a semi-incompatible old/new version that has an odd directory + // layout. By ignoring anything thrown, we let the user + // recover cleanly from that situation. + try + { + if (std::filesystem::is_directory(directory)) { + for (const std::filesystem::directory_entry& entry : + std::filesystem::directory_iterator(directory)) { + + try + { + // Ignore symlinks, cyber threat avoidance. + if (entry.is_symlink()) { + continue; + } + + // Could be an update package, add it. + if (entry.is_regular_file()) { + values.push_back(entry); + } + + // If allowed, follow it. + if (entry.is_directory() && (depth > 0)) { + recurseDirectory(entry.path().string(), depth, values); + } + } + catch (...) { + ; // Allow the user to continue. + } + } + } + } + catch (...) { + ; // Allow the user to continue. + } +} + +/*! + * \brief Scan a vector for a file by full name and path. + * + * \param packages Vector to search. + * \param fullNamePath Full name and path. + * + * \return > 0 if found, else -1. + */ +int Package::findPackageVector( + const std::vector& packages, + const std::string& fullNamePath) +{ + for (int ii = 0; ii < (int)packages.size(); ii++) { + if (packages[ii].fullPath == fullNamePath) + { + return ii; + } + } + return -1; +} + +/** + * \brief Get the list of available things in a directory + * + * \param directories Directories to scan (not recursive). + * \param info Vector to append to. + */ +void Package::get_availableInternal(const std::string& directory, std::vector& info) +{ + if (_rsa.public_exists()) { + + std::vector< std::filesystem::directory_entry> entries; + this->recurseDirectory(directory, MAX_RECURSION, entries); + + for (const std::filesystem::directory_entry& entry : entries) { + // Public key exists, worth trying ... + + // Entry is a normal file, validate / check it. + std::string fileNamePath = entry.path().string(); + for (std::size_t ii = 0; ii < fileNamePath.size(); ii++) { + fileNamePath[ii] = fileNamePath[ii] == 0x5c ? 0x2f : fileNamePath[ii]; + } + + // In our cache? + int found = findPackageVector(_packages, fileNamePath); + if (found >= 0) + { + info.push_back(_packages[found]); + continue; + } + + if (std::find(_avoid.begin(), _avoid.end(), fileNamePath) != _avoid.end()) { + // in the don't care list, + continue; + } + + FILE* fp = NULL; + try + { + fp = fopen(fileNamePath.c_str(), "rb"); + if (fp != NULL) { + bool ok = parseInternal(fp); + if (ok) { + + std::filesystem::file_time_type ftime = std::filesystem::last_write_time(fileNamePath); + + // It's validated... + // Get the filename to show to the user, the path, + // the package name and the internal version info. + // + // known not to be in _packages so add it to both. + _packages.push_back({ + fileNamePath, + entry.path().filename().string(), + _packageName, + _packageVers, + ftime, + ""}); + + info.push_back({ + fileNamePath, + entry.path().filename().string(), + _packageName, + _packageVers, + ftime, + ""}); + } + else + { + if (std::find(_avoid.begin(), _avoid.end(), fileNamePath) == _avoid.end()) { + _avoid.push_back(fileNamePath); + qDebug() << "Not added " << QString::fromStdString(fileNamePath) << "\n"; + } + } + } + else + { + // Keep seeing errno = 0x18 = 24 which is EMFILE = ran out of file handles for the app. + // Which indicates an fclose didn't happen, which makes no sense. + int err = errno; + qDebug() << "Not opened " << QString::fromStdString(fileNamePath) << " errno = " << err << "\n"; + } + } catch (...) { + qDebug() << "Throw 1\n"; + ; // Allow the user to keep iterating across the rest of this directory. + } + + // Close it. + if (fp != NULL) { + int fclose_rv = fclose(fp); + if (fclose_rv != 0) { + int err = errno; + + qDebug() << "Not closed " << QString::fromStdString(fileNamePath) << " errno = " << err << " fcloserv = " << fclose_rv << "\n"; + } + } + } + } +} Index: sources/update/Package.h =================================================================== diff -u --- sources/update/Package.h (revision 0) +++ sources/update/Package.h (revision 20b370a54d2737831b307a0de82aec9e06e2b772) @@ -0,0 +1,290 @@ +/*! + * + * Copyright (c) 2023 Diality Inc. - All Rights Reserved. + * + * \copyright + * THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN + * WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. + * + * \file Package.h + * \author (last) Phil Braica + * \date (last) 23-Jan-2023 + * \author (original) Phil Braica + * \date (original) 23-Jan-2023 + */ + +#ifndef PACKAGE_H_ +#define PACKAGE_H_ + +#include "UpdateProtocol.h" +#include "UiUpdateStatus.h" +#include "IDataProvider.h" + +#include +#include +#include +#include +#include // C++ 17 + +#include "PackageItem.h" +#include "SignRsa.h" + +/*! + * \brief Utility container for UI + */ +struct PackageInfo { + std::string fullPath; ///< Full path and name. + std::string fileName; ///< Just the file name. + std::string packageName; ///< The release name. + std::string packageVersionInfo; ///< Release version information. + std::filesystem::file_time_type lastWritten; ///< Time it was last written. + std::string reserved; ///< Reserved for UI. + + /*! + * \brief Equals operator. + * + * \return True if all the strings are equal. + */ + bool operator == (const PackageInfo& rhs) const + { + return ( + (fullPath == rhs.fullPath) && + (fileName == rhs.fileName) && + (packageName == rhs.packageName) && + (packageVersionInfo == rhs.packageVersionInfo) && + (lastWritten == rhs.lastWritten) && + (reserved == rhs.reserved)); + } + + /*! + * \brief Not equals operator. + * + * \return True if any of the strings are not equal. + */ + bool operator !=(const PackageInfo& rhs) const + { + return !(*this == rhs); + } + + /*! Default constructor. */ + PackageInfo() = default; + + /*! + * \brief Copy constructor. + * \param rhs Right hand side. + */ + PackageInfo(const PackageInfo& rhs) { + fullPath = rhs.fullPath; + fileName = rhs.fileName; + packageName = rhs.packageName; + packageVersionInfo = rhs.packageVersionInfo; + lastWritten = rhs.lastWritten; + reserved = rhs.reserved; + } + + /*! + * \brief Full constructor. + * \param fullPathStr Full path string. + * \param fileNameStr Just the name string. + * \param packageNameStr The package description string. + * \param packageVersionInfoStr The version information string. + * \param lastWrittenTime Last time written file value. + * \param reservedStr Reserved for UI use. + */ + PackageInfo( + const std::string& fullPathStr, + const std::string& fileNameStr, + const std::string& packageNameStr, + const std::string& packageVersionInfoStr, + const std::filesystem::file_time_type& lastWrittenTime, + const std::string& reservedStr) { + + fullPath = fullPathStr; + fileName = fileNameStr; + packageName = packageNameStr; + packageVersionInfo = packageVersionInfoStr; + lastWritten = lastWrittenTime; + reserved = reservedStr; + } + + /*! + * \brief Assignment operator. + * \param rhs Right hand side. + * \return Reference to this. + */ + PackageInfo& operator= (const PackageInfo& rhs) + { + fullPath = rhs.fullPath; + fileName = rhs.fileName; + packageName = rhs.packageName; + packageVersionInfo = rhs.packageVersionInfo; + lastWritten = rhs.lastWritten; + reserved = rhs.reserved; + return *this; + } +}; + +/*! + * \brief Class to manage a package file. + * + *
+ * The key things are:
+ *   get_available(...) Given a directory get a list of updates 
+ *                      that pass validation.
+ *   
+ *   start()   Given file name (optional key file) do a partial 
+ *             or complete update.
+ * 
+ * UI workflow would typically be:
+ * 1. set_public(key in base 64 as a string)
+ * 2. get_available(directory) -> Show user their options.
+ *    2b. Use parse to scan a specific file as an option.
+ *    2c. Might use get_available against more than one directory 
+ *        Old existing + USB.
+ * 3. start_hasKey(filename)
+ *    3a. progress() array of structs of progress.
+ *    3b. abort() If user decides to stop.
+ * 4. If it completes successfully, on the next  
+ *    boot/powercycle U-Boot will update the UI processor (SOM).
+ * 
+ * The rest of the calls are mainly software development / FPGA 
+ * development / manufacturing support, the update software 
+ * can be run piecewise in different ways.
+ * 
+ * The proof of concept UI allows a user to select the encryption
+ * file, individually validate a file, update pieces of the system,
+ * etc. This allows firmware developers to test just the DG or HD
+ * update mechanisms without fully updating the UI or slowing things 
+ * down. It can also flash a test image of a DG, HD or one of the 
+ * FPGAs.
+ * 
+ * Files are always:
+ *     Signature (base64 text)
+ *     XML (mildly encrypted) - decoded as stream till finding 
+ *          Contains a catalog of PackageItems that map to different data blobs.
+ *     data - binary blobs of bytes, 1 blob per XML 
+ * 
+ */ +class Package { +public: + Package(); + virtual ~Package(); + + bool parse( + const std::string& filename, + const std::string & key_location = ""); + + /*! + * \brief Get the XML after the read if any. + * + * \return String with the recovered XML. + */ + std::string get_xml() { + return _xml; + } + + /*! + * \brief Get the information about the read. + * + * \return String describing read info. + */ + std::string get_info() const { + return _info; + } + + /*! + * \brief Get the items. + * + * \return Vector of package item objects. + */ + std::vector get_items() { + return _items; + } + + /*! + * \brief Get the name of the last file parsed. + * + * \return Last parsed filename. + */ + std::string lastFileParsed() + { + return _lastFileName; + } + + /*! + * \brief Set the public key from a string. + * + * \param key The key, base 64. + * + * \return True on success. + */ + bool set_public(const std::string& key) + { + return _rsa.set_public(key); + } + + std::vector targetsAvailable(); + + bool start_hasKey( + const std::string &fileName, + const std::vector& update); + + bool start( + const std::string& fileName, + const std::string& key_location, + const std::vector& update); + + bool completedAll(); + + void abort(); + + std::vector progress(); + + std::vector get_available(const std::string& directory); + + std::vector get_available(const std::vector& directories); + + // Copy / cache it (if not already in there). + bool copyUpdate(const std::string& fullNamePath, const std::string& cacheDirectory, uint32 maxCacheSize); + + std::string getScriptIfAny(); +protected: + + bool parseInternal(FILE *fp); + + void clear(); + + std::string get_signature(FILE *pFile); + + void updateCatalog(FILE* pFile); + + void recurseDirectory( + const std::string& directory, + int depth, + std::vector& values); + + int findPackageVector( + const std::vector& packages, + const std::string& fullNamePath); + + void get_availableInternal(const std::string& directory, std::vector &info); + + std::vector _items; ///< The items. + SignRsa _rsa; ///< Security. + std::string _lastFileName; ///< Last package file name. + std::string _packageName; ///< The name.txt file contents. + std::string _packageVers; ///< The version.txt file contents. + std::string _signature; ///< Digital signature of this file. + std::string _xml; ///< XML catalog of this file. + std::string _info; ///< Progress info for UI and debug. + std::size_t _startXml; ///< Where XML starts in the file. + std::size_t _startData; ///< Where data starts in the file. + FILE * _pFileUpdating; ///< File being updated. + + std::vector _streams; ///< Target streams. + std::vector _packages; ///< Packages. + std::vector _avoid; ///< Avoid these. +}; + + +#endif // PACKAGE_H_ Index: sources/update/PackageItem.cpp =================================================================== diff -u --- sources/update/PackageItem.cpp (revision 0) +++ sources/update/PackageItem.cpp (revision 20b370a54d2737831b307a0de82aec9e06e2b772) @@ -0,0 +1,220 @@ +/*! + * + * Copyright (c) 2023 Diality Inc. - All Rights Reserved. + * + * \copyright + * THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN + * WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. + * + * \file PackageItem.cpp + * \author (last) Phil Braica + * \date (last) 23-Jan-2023 + * \author (original) Phil Braica + * \date (original) 23-Jan-2023 + */ + +#include "PackageItem.h" + +#include +#include +#include + + +/*! + * \brief Constructor. + */ +PackageItem::PackageItem(): + _fileType(""), + _destPath(""), + _desc(""), + _security(0), + _version(0), + _size(0), + _rawFilename(""), + _isScript(false), + _byteOffset(0) { + +} + +/*! + * \brief Destructor. + */ +PackageItem::~PackageItem() { + ; // NOP. +} + +/*! + * \brief Unwrap xml for the next tag occurance. + * + * Doesn't work for recursion but in this case that's not an issue. + * + * Static function. + * + * \param xml_str The XML to look within. + * \param tag The tag to look for. + * \param offset The offset to look from. + * + * \return The unwrapped text. + */ +std::string PackageItem::unwrap(const std::string & xml_str, const std::string & tag, std::size_t & offset) { + std::string stag = "<" + tag + ">"; + std::string etag = ""; + + std::size_t stag_ii = xml_str.find(stag, offset); + if (stag_ii == std::string::npos) { + return ""; + } + stag_ii += stag.length(); + std::size_t etag_ii = xml_str.find(etag, stag_ii); + if (etag_ii == std::string::npos) { + return ""; + } + offset = etag_ii + etag.length(); + return trim_text(xml_str.substr(stag_ii, etag_ii - stag_ii)); +} + +/*! + * \brief Unwrap xml for the next tag occurance. + * + * Doesn't work for recursion but in this case that's not an issue. + * + * Static function, no offset is needed for this is looking for the one and only child. + * + * \param xml_str The XML to look within. + * \param tag The tag to look for. + * + * \return The unwrapped text. + */ +std::string PackageItem::unwrap(const std::string& xml_str, const std::string& tag) { + std::size_t dontcare = 0; + return unwrap(xml_str, tag, dontcare); +} + +/*! + * \brief Wrap content in XML tag. + * + * \param tag Tag to use. + * \param content Content. + * \return XML fragment. + */ +std::string PackageItem::wrap(const std::string& tag, const std::string& content) { + return " <" + tag + ">" + content + "\n"; +} + +/*! + * \brief An array of items. + * + * \param xml_str The XML to look within. + * + * \return The array of items that are intact if any. + */ +std::vector PackageItem::fromXml(const std::string& xml_str) { + std::vector rv; + + std::size_t offset = 0; + for (std::size_t ii = 0; ii < xml_str.size(); ii++) { + + PackageItem item; + bool ok = false; + try { + std::string item_xml = unwrap(xml_str, "item", ii); + if (item_xml.size() == 0) { + break; + } + // Try to get the essentials to at least know enough about this thing to drop if it needed. + item._fileType = unwrap(item_xml, "filetype"); + item._size = static_cast(std::stoul(unwrap(item_xml, "size"))); + item._byteOffset = offset; + offset += item._size; + ok = true; + + // These if they error only disable this piece of the file (for now). + item._destPath = unwrap(item_xml, "path"); + item._desc = unwrap(item_xml, "desc"); + item._rawFilename = unwrap(item_xml, "raw"); + item._security = static_cast(std::stoul(unwrap(item_xml, "security"))); + item._version = static_cast(std::stoul(unwrap(item_xml, "version"))); + std::string scriptFlag = unwrap(item_xml, "script"); + item._isScript = + scriptFlag.size() == 0 ? false : + (scriptFlag[0] == 't' || scriptFlag[0] == 'T'); + } + catch (...) { + continue; + } + if (ok && (item._size > 0) && (item._fileType != "")) { + rv.push_back(item); + } else { + // Not ok? Then file too corrupt to proceed with any of this. + rv.clear(); + break; + } + } + + // Return the list. + return rv; +} + +/*! + * \brief Turn this item into an XML fragment. + * + * \return XML fragment. + */ +std::string PackageItem::to_xml() const { + + return + "\n" + + wrap("filetype", _fileType) + + wrap("path", _destPath) + + wrap("desc", _desc) + + wrap("security", std::to_string(_security)) + + wrap("version", std::to_string(_version)) + + wrap("size", std::to_string(_size)) + + wrap("script", (_isScript ? "True" : "False")) + + wrap("raw", _rawFilename) + "\n"; +} + +/*! + * \brief Turn a set into XML + * + * \param catalog Vector to run through. + * + * \return String (xml). + */ +std::string PackageItem::to_xml(const std::vector& catalog) { + std::string rv; + for (const PackageItem& item : catalog) { + rv += item.to_xml(); + } + return rv; +} + +/*! + * \brief Trim whitespace before and after. + * + * \param txt Input text. + * + * \return Trimmed text. + */ +std::string PackageItem::trim_text(const std::string& txt) { + + // This is relatively quick, easy to test. + std::string rv = txt; + + // Front. + rv.erase( + rv.begin(), + std::find_if(rv.begin(), rv.end(), + [](unsigned char c) { + return !std::isspace(c); + })); + + // Tail. + rv.erase( + std::find_if(rv.rbegin(), rv.rend(), + [](unsigned char c) { + return !std::isspace(c); + }).base(), rv.end()); + + return rv; +} Index: sources/update/PackageItem.h =================================================================== diff -u --- sources/update/PackageItem.h (revision 0) +++ sources/update/PackageItem.h (revision 20b370a54d2737831b307a0de82aec9e06e2b772) @@ -0,0 +1,56 @@ +/*! + * + * Copyright (c) 2023 Diality Inc. - All Rights Reserved. + * + * \copyright + * THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN + * WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. + * + * \file PackageItem.h + * \author (last) Phil Braica + * \date (last) 23-Jan-2023 + * \author (original) Phil Braica + * \date (original) 23-Jan-2023 + */ + +#ifndef PACKAGE_ITEM_H_ +#define PACKAGE_ITEM_H_ + +#include +#include + +/*! + * \brief One item in a package. + */ +class PackageItem { +public: + std::string _fileType; ///< Either "LINUX", "UI", "DG", "HD", "DGFPGA", "HDFPGA" + std::string _destPath; ///< Where to copy to and name (if any) + std::string _desc; ///< Descriptive tracking info. + uint32_t _security; ///< Security tag. + uint32_t _version; ///< 2nd security tag. + uint32_t _size; ///< Size bytes + std::string _rawFilename; ///< The original raw filename, informational. + bool _isScript; ///< Is this the update script? 1 per all package items. + std::size_t _byteOffset; ///< Offset into the file (cumulative _size). + + PackageItem(); + + virtual ~PackageItem(); + + static std::vector fromXml(const std::string & xml_str); + + std::string to_xml() const; + + static std::string to_xml(const std::vector & catalog); + + static std::string unwrap(const std::string& xml_str, const std::string& tag); + static std::string unwrap(const std::string& xml_str, const std::string& tag, std::size_t& offset); + + static std::string wrap(const std::string& tag, const std::string& content); + +protected: + static std::string trim_text(const std::string& txt); +}; + +#endif // PACKAGE_ITEM_H_ Index: sources/update/SignRsa.cpp =================================================================== diff -u --- sources/update/SignRsa.cpp (revision 0) +++ sources/update/SignRsa.cpp (revision 20b370a54d2737831b307a0de82aec9e06e2b772) @@ -0,0 +1,753 @@ +/*! + * \brief This provides device independent RSA 2048 bit data signing. + * + * See the header file for details about this module. + * + * \copyright 2023, Sunrise Labs, Inc. + * + * \file sign_rsa.cpp + * + * \author PBraica + * \date March 2023 + */ + +#include "SignRsa.h" +#include +#include +#include +#include + +// Windows requires this, Linux doesn't have it. +#if defined(_WIN32) || defined(WIN32) +#include +#endif + +/** + * This class pulls some of the RSA logic into one place that + * could be error prone, and it wraps the kind and size of the hash + * currently as SHA256 so that in the future other hashes or + * custom hashes could be used. + */ +class _InternalDigest { +public: + /*! + * \brief Constructor. + */ + _InternalDigest(): + _ctx(), + _pDigest(), + _finalized(false) + { + SHA256_Init(&_ctx); + } + + /*! + * \brief Copy from other. + * + * \param pOther Other object. + */ + void copy_from(const _InternalDigest* pOther) { + // CTX is a structure in RSA. + memcpy(&_ctx, &pOther->_ctx, sizeof(SHA256_CTX)); + + // Copy the digest. + memcpy(_pDigest, pOther->_pDigest, SHA256_DIGEST_LENGTH); + } + + /*! + * \brief Update the context. + * + * \param pData Buffer pointer. + * \param size_bytes Size in bytes. + */ + void update(const unsigned char * pData, std::size_t size_bytes) { + if (size_bytes > 0) { + SHA256_Update(&_ctx, pData, size_bytes); + } + } + + /*! + * \brief Finalize the context and create the digest bytes. + * + * \return Digest bytes. + */ + unsigned char* get_digest() { + SHA256_Final(_pDigest, &_ctx); + return _pDigest; + } + + /*! + * \brief Clear. + */ + void clear() { + SHA256_Init(&_ctx); + } + +protected: + /*! + * \brief The context computation object. + */ + SHA256_CTX _ctx; + + /*! + * \brief The Digest bytes. + */ + unsigned char _pDigest[SHA256_DIGEST_LENGTH]; + + /*! \brief Was this finalized? */ + bool _finalized; +}; + +// Buffer for file read operations. The buffer must be able to accomodate +// the RSA signature in whole (e.g. 4096-bit RSA key produces 512 byte signature) +#define BUFFER_SIZE (16384) + +/*! + * \brief Do the initialization calls for openSSL. + */ +class DoOnce { +public: + /*! + * \brief Constructor. + */ + DoOnce() { + OpenSSL_add_all_algorithms(); + ERR_load_crypto_strings(); + } +}; + +/*! + * \brief Constructor. + */ +SignRsa::SignRsa(): + _pDigest(new _InternalDigest()), + _pPublic(nullptr), + _pPrivate(nullptr) { + + // On first entry do this. + static DoOnce doOnce; +} + +/*! + * \brief Copy constructor. + */ +SignRsa::SignRsa(const SignRsa& other) : + _pDigest(new _InternalDigest()), + _pPublic(nullptr), + _pPrivate(nullptr), + _lastPub("") +{ + _pDigest->copy_from(other._pDigest); + + _pPublic = (other._pPublic == nullptr) ? + nullptr : RSAPublicKey_dup(other._pPublic); + _pPrivate = (other._pPrivate == nullptr) ? + nullptr : RSAPrivateKey_dup(other._pPrivate); +} + +/*! + * \brief Destructor. + */ +SignRsa::~SignRsa() { + if (_pDigest != nullptr) { + delete _pDigest; + } + if (_pPublic != nullptr) { + RSA_free(_pPublic); + } + if (_pPrivate != nullptr) { + RSA_free(_pPrivate); + } +} + +/*! + * \brief Create new RSA keys. + */ +void SignRsa::create_keys() { + + // Free old if needed. + if (_pPublic != nullptr) { + RSA_free(_pPublic); + } + if (_pPrivate != nullptr) { + RSA_free(_pPrivate); + } + + // Create new. + unsigned long e = RSA_F4; + BIGNUM *bne = BN_new(); + int ret = BN_set_word(bne, e); + if (ret == 1) { + _pPrivate = RSA_new(); + ret = RSA_generate_key_ex(_pPrivate, 2048, bne, NULL); + if (ret == 1) { + _pPublic = RSAPublicKey_dup(_pPrivate); + } else { + RSA_free(_pPrivate); + } + } + BN_free(bne); + return; +} + +/*! + * \brief Save this objects keys to the folder if they exist. + * + * Saves them as public.pem and private.pem, if they exist. + * + * \param folder_name Folder to use. + */ +void SignRsa::save_keys(const std::string& folder_name) { + + const std::string pubName = folder_name + "/public.pem"; + const std::string priName = folder_name + "/private.pem"; + + if (_pPublic != nullptr) { + FILE* pemFile = fopen(pubName.c_str(), "wb"); + if (pemFile != NULL) { + PEM_write_RSA_PUBKEY(pemFile, _pPublic); + fclose(pemFile); + } + } + if (_pPrivate != nullptr) { + FILE* pemFile = fopen(priName.c_str(), "wb"); + if (pemFile != NULL) { + PEM_write_RSAPrivateKey(pemFile, _pPrivate, NULL, NULL, 0, NULL, NULL); + fclose(pemFile); + } + } +} + +/*! + * \brief Load the public.pem and private.pem, what ever exists. + * + * \param folder_name Folder to use, or the path+name of the public or private key. + */ +void SignRsa::load_keys(const std::string& folder_name) { + + std::string key_loc = folder_name; + std::size_t clip = key_loc.find("public.pem"); + if (clip != std::string::npos) { + key_loc.resize(clip - 1); + } + clip = key_loc.find("private.pem"); + if (clip != std::string::npos) { + key_loc.resize(clip - 1); + } + + const std::string pubName = key_loc + "/public.pem"; + const std::string priName = key_loc + "/private.pem"; + + if (_pPublic != nullptr) { + RSA_free(_pPublic); + _pPublic = nullptr; + } + if (_pPrivate != nullptr) { + RSA_free(_pPrivate); + _pPrivate = nullptr; + } + + FILE* pPubFile = NULL; + FILE* pPriFile = NULL; + std::string tmpString=""; + + // Try the _pPublic. + try { + pPubFile = fopen(pubName.c_str(), "r"); + if (pPubFile != NULL) { + // Get size. + fseek(pPubFile, 0, SEEK_END); + int size = ftell(pPubFile); + + // Got back and read into tmpString. + fseek(pPubFile, 0, SEEK_SET); + tmpString.resize(size, '\0'); + + // Read to cache the string value. + std::size_t readBytes = fread(&tmpString[0], sizeof(char), (size_t)size, pPubFile); + if (readBytes != (std::size_t)size) { + tmpString = ""; + } + + // Go back and read public key from file + fseek(pPubFile, 0, SEEK_SET); + _pPublic = PEM_read_RSA_PUBKEY(pPubFile, NULL, NULL, NULL); + } + } catch (...) { + _pPublic = nullptr; + } + + // Cache the public key since it was read ok, + // this helps when validating many files from the same API. + if (_pPublic != nullptr) { + _lastPub = tmpString; + } + + // Try the _pPrivate. + try { + pPriFile = fopen(priName.c_str(), "r"); + if (pPriFile != NULL) { + // Read public key from file + _pPrivate = PEM_read_RSAPrivateKey(pPriFile, NULL, NULL, NULL); + } + } + catch (...) { + _pPrivate = nullptr; + } + + // Because these are typically security procedures, + // if they fail we don't want to crash, we need to be able + // to keep going so the close is wrapped in try/catch. + + try + { + if (pPubFile) + { + fclose(pPubFile); + } + } + catch (...) { + ; // NOP. + } + + try + { + if (pPriFile) + { + fclose(pPriFile); + } + } + catch (...) { + ; // NOP. + } +} + +/*! + * \brief Set the public key from a string. + * + * \note We cache public keys for speed / ease of use. + * + * \param key The key, base 64. + * + * \return True on success. + */ +bool SignRsa::set_public(const std::string& key) { + + if (key == _lastPub) + { + return true; // No need to do anything and thus success. + } + + if (_pPublic != nullptr) { + RSA_free(_pPublic); + } + + BIO* keybio; + const char* cstr = key.c_str(); + keybio = BIO_new_mem_buf((void*)cstr, -1); + if (keybio == NULL) + { + return false; + } + _pPublic = PEM_read_bio_RSA_PUBKEY(keybio, &_pPublic, NULL, NULL); + BIO_free(keybio); + + _lastPub = key; + return true; +} + +/*! + * \brief Set the private key from a string. + * + * \note Private keys are NOT cached as text for security! + * + * \param key The key, base 64 + * + * \return True on success. + */ +bool SignRsa::set_private(const std::string& key) { + if (_pPrivate != nullptr) { + RSA_free(_pPrivate); + } + BIO* keybio; + const char* cstr = key.c_str(); + keybio = BIO_new_mem_buf((void*)cstr, -1); + if (keybio == NULL) { + return false; + } + _pPrivate = PEM_read_bio_RSAPrivateKey(keybio, &_pPrivate, NULL, NULL); + return true; +} + +/*! + * \brief Clear our digest to do a different new stream. + */ +void SignRsa::clear_digest() { + _pDigest->clear(); +} + +/*! + * \brief Update our Digest object from a buffer. + * + * \param pData Data pointer. + * \param size_bytes Size in bytes. + */ +void SignRsa::update_digest(const unsigned char* pData, std::size_t size_bytes) { + _pDigest->update(pData, size_bytes); +} + +/*! + * \brief Create and return a signature for a buffer. + * + * \param pData Data pointer. + * \param size_bytes Size in bytes. + * + * \return A base64 signature. + */ +std::string SignRsa::sign_data(const unsigned char* pData, std::size_t size_bytes) { + _InternalDigest digest; + // Guard not being setup or given bad things. + if ((pData != nullptr) && + (size_bytes != 0)) { + digest.update(pData, size_bytes); + } + return sign_digest(digest.get_digest()); +} + +/*! + * \brief Create and return a signature for a file. + * + * \param pFile File pointer. + * + * \return A base64 signature. + */ +std::string SignRsa::sign_file(FILE* pFile) { + // Guard not being setup or given bad things. + if (pFile == nullptr) { + return ""; + } + + // Stream file into buffer, update the digest. + static unsigned char pBuffer[BUFFER_SIZE]; + _InternalDigest digest; + std::size_t size_bytes = fread(pBuffer, 1, BUFFER_SIZE, pFile); + std::size_t total = size_bytes; + // Read data in chunks and feed it to OpenSSL SHA256 + while (size_bytes > 0) + { + digest.update(pBuffer, size_bytes); + size_bytes = fread(pBuffer, 1, BUFFER_SIZE, pFile); + total += size_bytes; + } + return sign_digest(digest.get_digest()); +} + +/*! + * \brief Create and return a signature for a file. + * + * \param pDigest Digest pointer. + * + * \return A base64 signature. + */ +std::string SignRsa::sign_digest(const unsigned char* pDigest) { + + int result = 0; // Set to 0=invalid. + + std::string signature; + + // Guard not being setup or given bad things. + // + // Invalid signature size, etc. are handled by RSA_verify, + // here we make sure our pointers are ok + if ((_pDigest != nullptr) && + (_pPrivate != nullptr)) { + + char *pBuffer = (char *)malloc(RSA_size(_pPrivate)); + if (pBuffer != NULL) { + + unsigned int size_bytes = 0; + + // Ask the library to sign. + result = RSA_sign( + NID_sha256, + pDigest, + SHA256_DIGEST_LENGTH, + (unsigned char *)pBuffer, + &size_bytes, + _pPrivate); + + if (result == 1) { + signature = encodeBase64(pBuffer, size_bytes); + } else { + signature.resize(0); + } + } + } + return signature; +} + +/*! + * \brief Given buffer and signature, verify they match w/ public key. + * + * \param pData Data pointer. + * \param size_bytes Size in bytes. + * \param signature The signature. + * + * \return True if they match, false if can't compare or failed. + */ +bool SignRsa::verify_data(const unsigned char* pData, std::size_t size_bytes, const std::string& signature) { + + _InternalDigest digest; + + // Guard not being setup or given bad things. + if ((pData != nullptr) && + (size_bytes != 0)) { + digest.update(pData, size_bytes); + } + return verify_digest(digest.get_digest(), signature); +} + +/*! + * \brief Given a file and signature verify they match w/ public key. + * + * \param pFile File pointer. + * \param signature The signature. + * + * \return True if they match, false if can't compare or failed. + */ +bool SignRsa::verify_file(FILE* pFile, const std::string& signature) { + + // Guard not being setup or given bad things. + if (pFile == nullptr) { + return false; + } + + // Stream file into buffer, update the digest. + static unsigned char pBuffer[BUFFER_SIZE]; + _InternalDigest digest; + std::size_t size_bytes = fread(pBuffer, 1, BUFFER_SIZE, pFile); + + // Read data in chunks and feed it to OpenSSL SHA256 + while (size_bytes > 0) + { + digest.update(pBuffer, size_bytes); + size_bytes = fread(pBuffer, 1, BUFFER_SIZE, pFile); + } + + // Verify the digest. + return verify_digest(digest.get_digest(), signature); +} + +/*! + * \brief Given a Digest and signature verify they match w/ public key. + * + * \param pDigest File pointer. + * \param signature The signature. + * + * \return True if they match, false if can't compare or failed. + */ +bool SignRsa::verify_digest( + const unsigned char* pDigest, + const std::string& signature) { + + int result = 0; // Set to 0=invalid. + + // Guard not being setup or given bad things. + // + // Invalid signature size, etc. are handled by RSA_verify, + // here we make sure our pointers are ok + if ((_pDigest != nullptr) && + (_pPublic != nullptr)) { + + // Our signature is base64 without end of lines. + + // For readability, get the pointer and size here. + const std::string decoded = decodeBase64(signature); + const unsigned char * pSigBuffer = (const unsigned char* )decoded.data(); + const std::size_t sig_size_bytes = decoded.size(); + + // Ask the library to verify. + result = RSA_verify( + NID_sha256, + pDigest, + SHA256_DIGEST_LENGTH, + pSigBuffer, + (unsigned int)sig_size_bytes, + _pPublic); + } + return (result == 1); +} + +/*! + * \brief Finalize the context and create the digest bytes. + * + * \return Digest bytes. + */ +const unsigned char* SignRsa::get_digest() const { + return _pDigest->get_digest(); +} + +/*! + * \brief Decode base64 to data. + * + * This is very fast and more permissive than OpenSSL's implimentation + * and thereby makes it compatible with how Python does things with OpenSSL. + * + * IF the input data is invalid, you get invalid output but it won't crash. + * + * \param base64_str String. + * + * \return String. + */ +std::string SignRsa::decodeBase64(const std::string& base64_str) const { + + static constexpr char reverse_table[256] = { + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 62, 64, 64, 64, 63, + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 64, 64, 64, 64, 64, 64, + + 64, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 64, 64, 64, 64, 64, + 64, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 64, 64, 64, 64, 64, + + // Unused unless a byte is invalid, we padd with + // stuff we don't care so we can skip array indexing for speed. + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, + + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64 + }; + + // This is meant to be simple and fast, but not validate the format. + // In the RSA sense the rest of this code consumes the decoded + // data and validates it's content so ... format checking is not + // useful it gets caught by the consumer. + // + // The fastest way to do this is process 3 input bytes at once + // and construct the output with unrolling but when it's coded + // that way it is 3-4x more complex with more corner cases to verify. + // + // This is almost that fast (fast enough for the application) and + // no corner cases. + + std::string rv; // Return value. + unsigned int acc = 0; // Accumulate the value. + unsigned int bits = 0; // Bits in the accumulated. + for (unsigned char c : base64_str) { + + // This handles the problem that openSSL has, + // it either requires line breaks or forbids line breaks. + if (::std::isspace(c) || c == '=') { + continue; + } + + // Decode the byte, accumulate 6 bits. + char v = reverse_table[c]; + acc = (acc << 6) | v; + bits += 6; + + // Export a byte if needed. + if (bits >= 8) { + bits -= 8; + rv += (char)((acc >> bits) & 0xFF); + } + } + + // Return. + return rv; +} + +/*! + * \brief Encode base64 to data. + * + * IF the input data is invalid, you get invalid output but it won't crash. + * + * \param raw_str Raw data string. + * \param sizeBytes Size in bytes. + * + * \return String. + */ +std::string SignRsa::encodeBase64(const char* raw_str, std::size_t sizeBytes) const { + static constexpr char encode_table[] = { + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', + 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', + 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', + 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', + 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', + 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', + 'w', 'x', 'y', 'z', '0', '1', '2', '3', + '4', '5', '6', '7', '8', '9', '+', '/' + }; + + std::string rv; + + std::size_t phase = 0; + unsigned char lc = 0; + unsigned char byte; + for (std::size_t ii = 0; ii < sizeBytes; ii++) { + // Every 3 characters = 4 bytes. + unsigned char c = (unsigned char) raw_str[ii]; + + // NOTE: literals are used here instead of constexpr + // because it is dramatically easier to understand. + switch (phase) { + case 0: + // top 6 bits. + byte = (c >> 2) & 0x3F; + rv += encode_table[byte]; + break; + case 1: + // last 2 bits + 4 bits. + byte = ((lc & 0x3) << 4) | (c >> 4); + rv += encode_table[byte]; + break; + default: + // last 4 bits + 2 bits. + byte = ((lc & 0xF) << 2) | (c >> 6); + rv += encode_table[byte]; + + // Last 6 bits. + byte = (c & 0x3F); + rv += encode_table[byte]; + } + phase = (phase + 1) % 3; + lc = c; + } + + // Handle tail end. + // 0 means we lined up luckily ideally, no padding. + // 1 means we have 2 bits in lc to emit, two == + // 2 means we have 4 bits in lc to emit, one = + if (phase == 1) { + byte = ((lc & 0x3) << 4); + rv += encode_table[byte]; + rv += "=="; + } + if (phase == 2) { + byte = (lc & 0xF) << 2; + rv += encode_table[byte]; + rv += "="; + } + + return rv; +} + +/*! + * \brief Encode base64 to data. + * + * IF the input data is invalid, you get invalid output but it won't crash. + * + * \param raw_str Raw data string. + * + * \return String. + */ +std::string SignRsa::encodeBase64(const std::string& raw_str) const { + return encodeBase64(raw_str.c_str(), raw_str.length()); +} Index: sources/update/SignRsa.h =================================================================== diff -u --- sources/update/SignRsa.h (revision 0) +++ sources/update/SignRsa.h (revision 20b370a54d2737831b307a0de82aec9e06e2b772) @@ -0,0 +1,151 @@ +/*! + * \brief This provides device independent RSA 2048 bit data signing. + * + * It is based on OpenSSL, which is the most maintained and most + * popular package, but it is not completely documented, and examples + * across the web use different levels of the API and different versions + * making this hard to code / recode. + * + * OpenSSL typically is part of most Linux distros and as it's the heart + * of most HTTPS operations on Linux almost always available. It is + * available as source code and there are binary installers for Windows, + * Linux, Android, iOS, iPhone and many others. + * + * Note to use it as a developer on Windows with visual studio: + * [C/C++ -> General -> Additional Include Directories] value: OpenSSL?s include directory in your machine (e.g C:\openssl\include) + * [Linker -> General -> Additional Library Directories] value: OpenSSL?s lib directory in your machine (e.g C:\openssl\lib) + * [Linker -> Input -> Additional Dependencies] value: libcrypto.lib libssl.lib + * + * Be aware some countries put import/export restrictions on anything cryptographic + * and the license for OpenSSL makes that very clear. + * + * For ITAR compliance this includes nothing novel or new. All algorithms are published, + * not restricted by US law and are used as is. + * + * Sunrise has an equivalent python3 module that is interoperable with this C++ code. + * + * \copyright 2023, Sunrise Labs, Inc. + * + * \file SignRsa.h + * + * \author PBraica + * \date March 2023 + */ + + +#ifndef SIGN_RSA_H_ +#define SIGN_RSA_H_ + +#include + + +// Prototype internal class not part of the API. +class _InternalDigest; + +// Forward declare RSA so that other classes building this object +// don't pull in the giant includes of RSA over and over. +typedef struct rsa_st RSA; + +/** + * This wraps OpenSSL, we have an equivalent sign_rsa.py file + * for scripting/tool based things that is basically call for call + * equivalent and compatible. + * + * In some cases, the software is only used to handle verify in which case + * the public key will exist and be loaded and the private is not. + * + *
+ * At the API level:
+ *   Digests are 256-bit SHA hash
+ *   Signatures are base64 strings.
+ * 
+ */ +class SignRsa { + +public: + SignRsa(); + + SignRsa(const SignRsa &other); + + virtual ~SignRsa(); + + void create_keys(); + + void save_keys(const std::string& folder_name); + + void load_keys(const std::string& folder_name); + + /*! + * \brief Set the keys from strings. + * + * \param public_key Public key string. + * \param private_key Private key string. + * + * \return True on success. + */ + bool set_keys(const std::string& public_key, const std::string& private_key) { + return set_public(public_key) && set_private(private_key); + } + + bool set_public(const std::string& key); + + bool set_private(const std::string& key); + + void clear_digest(); + + void update_digest(const unsigned char* pData, std::size_t size_bytes); + + std::string sign_data(const unsigned char* pData, std::size_t size_bytes); + + std::string sign_file(FILE * pFile); + + std::string sign_digest(const unsigned char* pDigest); + + bool verify_data(const unsigned char* pData, std::size_t size_bytes, const std::string& signature); + + bool verify_file(FILE* pFile, const std::string& signature); + + bool verify_digest(const unsigned char* pDigest, const std::string& signature); + + /*! + * \brief Does the public key exist? + * + * \return True if it exists. + */ + bool public_exists() const { + return _pPublic != nullptr; + } + + /*! + * \brief Does the private key exist? + * + * \return True if it exists. + */ + bool private_exists() const { + return _pPrivate != nullptr; + } + + /*! + * \brief Finalize the context and create the digest bytes. + * + * \return Digest bytes. + */ + const unsigned char* get_digest() const; + + // These are more tolerant versions than openSSL, which has a known + // problem with it's decode making it incompatible with many + // base64 encoders. + + std::string decodeBase64(const std::string& base64_str) const; + std::string encodeBase64(const std::string& raw_str) const; + std::string encodeBase64(const char * raw_str, std::size_t size) const; + +protected: + _InternalDigest* _pDigest; ///< Internal digest. + RSA* _pPublic; ///< Public key. + RSA* _pPrivate; ///< Private key. + std::string _lastPub; ///< Last public key. +}; + + +#endif // SIGN_RSA_H_ Index: sources/update/UiProtocol.cpp =================================================================== diff -u --- sources/update/UiProtocol.cpp (revision 0) +++ sources/update/UiProtocol.cpp (revision 20b370a54d2737831b307a0de82aec9e06e2b772) @@ -0,0 +1,408 @@ +/*! + * + * Copyright (c) 2023 Diality Inc. - All Rights Reserved. + * + * \copyright + * THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN + * WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. + * + * \file UiProtocol.cpp + * \author (last) Phil Braica + * \date (last) 23-Jan-2023 + * \author (original) Phil Braica + * \date (original) 23-Jan-2023 + * + */ + +#include "UiProtocol.h" +#include "UiSwUpdate.h" +#include "IDataProvider.h" + +#include "Logger.h" + +#include +#include + +namespace SwUpdate { + +uint16 UiProtocol::_gRandSeed = 0; ///< Seed value. +uint8 UiProtocol::_gIdNum = 0; ///< Id number. + + +/*! + * \brief Constructor. + * + * \param target Target to go after for update. + */ +UiProtocol::UiProtocol(SwUpdateTargetEnum target) : + _desired(Idle), + _state(None), + _lastState(None), + _target(target), + _pProvider(nullptr), + _transfered(0), + _maxTransfer(0), + _retries(0), + _lastRx{(uint8)0}, + _link(KBPS_WIRE), + _pThread(nullptr), + _completedOk(false) +{ + ; // NOP. +} + +/*! + * \brief Destructor (virtual). + */ +UiProtocol::~UiProtocol() { + // Set shutdown, then fire start, + // finally join thread. + _desired = Shutdown; + + // It finished and ready to join, join and exit. + if (_pThread != nullptr) { + _pThread->join(); + _pThread = nullptr; + } +} + +/*! + * \brief Start updating. + * + * \return True on started ok. + */ +bool UiProtocol::start() { + std::lock_guard lockApi(_apiMutex); + + + if (_pThread != nullptr) { + _pThread->join(); + delete _pThread; + _pThread = nullptr; + } + _pThread = new std::thread(&UiProtocol::_run, this); + + // Copy the key info in. + _desired = Complete; + _lastState = None; + + return true; +} + +/*! + * \brief Abort update. + */ +bool UiProtocol::abort() { + std::lock_guard lockapi(_apiMutex); + _desired = Idle; + _link.abort(); + + return true; +} + +/*! + * \brief Current estimated progress. + * + * When not "updating" it is the last known step + * which can describe a failure condition for debug. + * + * \return Status object. + */ +UiUpdateStatus UiProtocol::progress() { + // Our state to descriptive text. + + static const std::string state_text[] = { + "Not started", "Starting", "Streaming", "Streamed", + "Verified", "Version checked", "Completed", "Start failed", + "Stream failed", "Verification failed", "Version check failed", "Reboot failed", + "Cyber attack", "File failure"}; + + // Grab mutex so status is always coherent. + std::lock_guard lockapi(_apiMutex); + + UiUpdateStatus rv; + + // One of 3 things is the current desired end state. + rv.goal = + _desired == Idle ? "Idle" : + _desired == Complete ? "Complete" : + "Shutdown"; + + // Show as text which target. + rv.targetName = + _target == HD ? "HD" : + _target == HDFPGA ? "HDFPGA" : + _target == DG ? "DG" : + _target == DGFPGA ? "DGFPGA" : "Files"; + + bool stillInProgress = (_state != UpdateState::None) && (_state < UpdateState::Completed); + rv.inProgress = stillInProgress; + + rv.totalSteps = (uint16)UpdateState::Completed; + + // Compute the descriptive text. + uint32 stepIndex = (uint32)_state; + rv.stepName = stepIndex <= FileFailure ? state_text[stepIndex] : "Unknown SW Err"; + rv.stepIndex = _state < UpdateState::Completed ? (UpdateState)stepIndex : UpdateState::Completed; + + const float one_hundred_percent = 100.0f; + rv.percentTotal = _maxTransfer == 0 ? 0 : (_transfered * one_hundred_percent / _maxTransfer); + + return rv; +} + +/*! + * \brief Receive and check if matched. + * + * \param msgId Can message ID. + * \param pData Data pointer. + * + * \return True if it matched. + */ +bool UiProtocol::receive(uint16 msgId, uint8 * pData) { + + // Return value. + bool matched = false; + + // IF we're aren't trying to make progress in this object + // towards completion, we don't care. + if (_desired == Complete) { + + // If it's a message that matches what we'd want, + // then copy it. + matched = _link.checkReceived(msgId, pData); + if (matched) { + memcpy(_lastRx, pData, CAN_PAYLOAD_SIZE); + } + } + return matched; +} + +/////////////////////////////////////////////////////////// +// Protected. +/////////////////////////////////////////////////////////// + +/** + * \brief Thread runs update process. + */ +void UiProtocol::_run() { + + // Wait till we start. + if (_state != None) { + _lastState = _state; + _state = _desired == Complete ? Starting : None; + } + + _completedOk = false; // In progress. + + // Do most of the work, virtual call so + // it can be overriden for file like things and test. + _doUpdate(); + + // Throw away the pointer to the old data provider. + _pProvider = nullptr; + + // Cyber check. + if (_link.numberRetries() > _maxCyberRetries) { + _state = UpdateState::CyberThreat; + } + if (_state == Completed) { + _completedOk = true; + } + + // If anything is lingering, stop it. + _link.abort(); + + { + std::unique_lock lock2(_apiMutex); + _desired = _desired == Complete ? Idle : _desired; + } + + // Finally tell UiSwUpdate that coordinates multiple things + // that this set of actions are complete, and if they completed ok. + UiSwUpdate::instance().taskCompleted(_completedOk == true, this); +} + +/*! + * \brief Do the update for FW / FPGA + */ +void UiProtocol::_doUpdate() { + + // Null check / race condition. + if (_pProvider == nullptr) { + return; + } + + // Reset the transfered counter. + _transfered = 0; + _link.resetRetries(); + + // Send start. + _state = UpdateState::Starting; + bool ok = _sendCommand(SwUpdateCmdEnum::Start); + _state = ok ? UpdateState::Started : UpdateState::StartTimedOut; + if ((_desired != Complete) || (!ok)) { + return; + } + + // Stream the data. + ok = _streamData(); + _state = ok ? UpdateState::Streamed : UpdateState::StreamTimedOut; + if ((_desired != Complete) || (!ok)) { + return; + } + + // Send signature. + ok = _sendSignature(); + _state = ok ? UpdateState::Streamed : UpdateState::StreamTimedOut; + if ((_desired != Complete) || (!ok)) { + return; + } + + // Get verification data. + ok = _sendCommand(SwUpdateCmdEnum::Verify); + SwUpdateVerifyResponse *pVerify = (SwUpdateVerifyResponse*)_lastRx; + ok &= pVerify->data == _pProvider->verifyData; + _state = ok ? UpdateState::Verified : UpdateState::FailedVerify; + if ((_desired != Complete) || (!ok)) { + return; + } + + // Get the version data. + ok = _sendCommand(SwUpdateCmdEnum::Version); + pVerify = (SwUpdateVerifyResponse*)_lastRx; + ok &= pVerify->data == _pProvider->versionData; + _state = ok ? UpdateState::Versioned : UpdateState::FailedVersion; + if ((_desired != Complete) || (!ok)) { + return; + } + + // Say if we agree. + ok = _sendCommand(SwUpdateCmdEnum::Verified); + if ((_desired != Complete) || (!ok)) { + return; + } + + // Reboot but only if it's a FW target, not if its a FPGA target. + if ((_target == HD) || (_target == DG)) { + ok = _sendCommand(SwUpdateCmdEnum::RunApp); + } + _state = ok ? UpdateState::Completed : UpdateState::RebootTimeout; + + // If it failed, tell the boot loader to abort, + // at least try to. + if (_state != UpdateState::Completed) { + // Best effort abort. + _sendCommand(SwUpdateCmdEnum::Abort); + } +} + +/*! + * \brief Send command. + * + * \param cmd Command enum to use. + * + * \return True on success, else false. + */ +bool UiProtocol::_sendCommand(SwUpdateCmdEnum cmd) { + SwUpdateCommand msg; + msg.id = _gIdNum++; + msg.cmd = SwUpdate_FormCommand(_target, cmd); + msg.rand = _gRandSeed++; + SwUpdate_createSecurity((uint8 *)&msg, sizeof(SwUpdateCommand)); + return _link.sendOk(SwUpdateCanMsgIds::CommandId, (uint8 *)&msg, _retryEffort); +} + +/*! + * \brief Stream the data. + * + * \return True on completed ok. + */ +bool UiProtocol::_streamData() { + + // Stream the data. + SwUpdateDataBuffer msg; + msg.id = _gIdNum++; + msg.kind = _target; + _transfered = 0; + msg.index = 0; + + // While data to stream... + const std::size_t image_size = _pProvider->totalSize; + while (_transfered < image_size) { + // Compute the size in bytes of the transfer, upto MAX_TRANSFER_SIZE. + const uint32 sz = (uint32)(_transfered + MAX_TRANSFER_SIZE) < (uint32)image_size ? + (uint32)MAX_TRANSFER_SIZE : (uint32)(image_size - _transfered); + + // Copy the data in. + std::size_t rd_sz = _pProvider->read(msg.index * BYTES_PER_INDEX, msg.buffer, sz); + + if (rd_sz < MAX_TRANSFER_SIZE) { + memset(&msg.buffer[rd_sz], 0, MAX_TRANSFER_SIZE - rd_sz); + } + + // Obscure it. + SwUpdate_encodeData(&msg); + + // The read typically takes <1ms time, but it might not, and it might block or even fail. + bool ok = rd_sz > 0; + if (_desired != Complete) { + return false; + } + if (!ok) { + // Retry read. + continue; + } + + // Create the security token and send it. + SwUpdate_createSecurity((uint8 *) &msg, sizeof(SwUpdateDataBuffer)); + ok = _link.sendOk( + (_target == HD) || (_target == DG) ? + SwUpdateCanMsgIds::DataBufferId_FW : SwUpdateCanMsgIds::DataBufferId_FPGA, + (uint8*)&msg, _retryEffort); + if ((_desired != Complete) || (!ok)) { + return false; + } + + // Cyber check. + if (_link.numberRetries() > _maxCyberRetries) { + return false; + } + + // Good, so increment. + msg.index += (MAX_TRANSFER_SIZE / BYTES_PER_INDEX); + _transfered += MAX_TRANSFER_SIZE; + } + return true; +} + +/*! + * \brief Stream the data. + * + * \return True on completed ok. + */ +bool UiProtocol::_sendSignature() { + uint32 crc1 = _pProvider->verifyData; + uint32 crc2 = _pProvider->versionData; + SwUpdateDataBuffer msg; + msg.id = _gIdNum++; + msg.kind = _target; + _transfered = 0; + msg.index = 0xFFFF; + SwUpdate_makeSignature(crc1, crc2, msg.buffer); + + // Obscure it. + SwUpdate_encodeData(&msg); + + // Create the security token and send it. + SwUpdate_createSecurity((uint8 *) &msg, sizeof(SwUpdateDataBuffer)); + bool ok = _link.sendOk( + (_target == HD) || (_target == DG) ? + SwUpdateCanMsgIds::DataBufferId_FW : SwUpdateCanMsgIds::DataBufferId_FPGA, + (uint8*)&msg, _retryEffort); + + return (ok && (_link.numberRetries() <= _maxCyberRetries)); +} + + +} // namespace::SwUpdate Index: sources/update/UiProtocol.h =================================================================== diff -u --- sources/update/UiProtocol.h (revision 0) +++ sources/update/UiProtocol.h (revision 20b370a54d2737831b307a0de82aec9e06e2b772) @@ -0,0 +1,303 @@ +/*! + * + * Copyright (c) 2023 Diality Inc. - All Rights Reserved. + * + * \copyright + * THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN + * WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. + * + * \file UiProtocol.h + * \author (last) Phil Braica + * \date (last) 23-Jan-2023 + * \author (original) Phil Braica + * \date (original) 23-Jan-2023 + * + */ +#ifndef UI_PROTOCOL_H_ +#define UI_PROTOCOL_H_ + +#include "HalStdTypes.h" +#include "MsgLink.h" +#include "UpdateProtocol.h" +#include "UiUpdateStatus.h" +#include "IDataProvider.h" + +#include +#include +#include +#include + +namespace SwUpdate { + +/** Forward declare. */ +class IDataProvider; + +/*! + * This class uses a thread plus a waitable condition variable + * to impliment a linear procedure to update one firmware + * "target". It uses "ID" slots for commands so that in theory + * multiple updaters running in paralell won't conflict. + * + * Note that this is deliberately NOT a statemachine even + * though it could be coded as one. Statemachines have overhead + * waiting for events and the goal is to have virtually zero + * time between events and in theory never really yielding unless + * the Firmware image gets too busy. + */ +class UiProtocol { +public: + /*! + * \brief Constructor. + * + * \param target Target to go after for update. + */ + UiProtocol(SwUpdateTargetEnum target); + + /*! + * \brief Destructor (virtual). + */ + virtual ~UiProtocol(); + + /*! + * \brief Start updating. + * + * \return True on started ok. + */ + virtual bool start(); + + /*! + * \brief Abort update. + * + * \return True on success, false on timeout trying to stop the process. + */ + bool abort(); + + /*! + * \brief Current estimated progress. + * + * When not "updating" it is the last known step + * which can describe a failure condition for debug. + * + * \return Status object. + */ + UiUpdateStatus progress(); + + /*! + * \brief Receive and check if matched. + * + * \param msgId Can message ID. + * \param pData Data pointer. + * + * \return True if it matched. + */ + bool receive(uint16 msgId, uint8 * pData); + + /*! + * \brief Get the target enum value. + * + * \return Target enum. + */ + SwUpdateTargetEnum target() { + return _target; + } + + /*! + * \brief Did this complete ok? + * + * \return True if it has ran and completed successfully since a "start" call. + */ + bool completedOk() { + return _completedOk; + } + + /*! + * \brief Start updating. + * + * \param pProvider Data provider. + * \param pNext Next if the updates occur in a chain. + * + * \return True on started ok. + */ + void setProvider(IDataProvider* pProvider) { + std::lock_guard lockApi(_apiMutex); + _pProvider = pProvider; + _maxTransfer = pProvider->totalSize; + } + +protected: + + /*! + * \brief Thread runs update process. + */ + void _run(); + + /** + * \brief Do the update for FW / FPGA + */ + virtual void _doUpdate(); + + /*! + * \brief Send command. + * + * \param cmd Command enum to use. + * + * \return True on success, else false. + */ + virtual bool _sendCommand(SwUpdateCmdEnum cmd); + + /*! + * \brief Stream the data. + * + * \return True on completed ok. + */ + virtual bool _streamData(); + + /*! + * \brief Stream the data. + * + * \return True on completed ok. + */ + virtual bool _sendSignature(); + + /*! + * \brief Wait until we are paused or started. + * + * \param tillStopped Wait till stopped else wait till started. + * + * \return True on success, false on timeout. + */ + bool _wait_stop(bool tillStopped); + + /*! + * Progress states. + */ + enum UpdateState { + None = 0, + Starting, + Started, + Streamed, + Verified, + Versioned, + Completed, + + StartTimedOut, + StreamTimedOut, + FailedVerify, + FailedVersion, + RebootTimeout, + + CyberThreat, + FileFailure + }; + + /*! + * The desired state. + */ + enum DesiredState { + Idle = 0, + Complete, + Shutdown + }; + + static uint16 _gRandSeed; ///< Used to "whiten" encoding. + static uint8 _gIdNum; ///< Keeps multiple Updaters seperate. + + // Note: These would be tuned based on traffic to be as small as possible but reliable. + const uint32 _retryEffort = 40; ///< Retry effort. + const uint32 _maxCyberRetries = 200; ///< Max retries total all msgs. + + // The goal is to apply reasonable effort, and be able to report status. + // Thus, there is a desired end-state goal we apply effort towards, and + // we track the progress. We also track when we abort / stop what was the + // last state to make any desired logging / status / debug easy. + + DesiredState _desired; ///< Desired state. + UpdateState _state; ///< Current state. + UpdateState _lastState; ///< Last state before stopping. + SwUpdateTargetEnum _target; ///< Target for update. + IDataProvider* _pProvider; ///< Data provider. + uint32 _transfered; ///< Transfered bytes. + uint32 _maxTransfer; ///< Max to transfer. + uint32 _retries; ///< Number of packet retries. + uint8 _lastRx[CAN_PAYLOAD_SIZE]; ///< Last RX. + MsgLink _link; ///< Link. + ::std::thread* _pThread; ///< Thread. + ::std::mutex _apiMutex; ///< Mutex API. + bool _completedOk; ///< Completed ok, false is either not started or in progress. +}; + +/*! + * This class inherits from UiProtocol to do all of the file transfers. + */ +class UiProtocolFile : public UiProtocol { +public: + /*! + * \brief Constructor. + * + * \param target Target to go after for update. + */ + UiProtocolFile() : + UiProtocol(LINUX) { + ; // NOP. + } + + /*! + * \brief Destructor (virtual). + */ + virtual ~UiProtocolFile() { + ; // NOP. + } + + /*! + * \brief Start updating. + * + * \param targets Data provider, destination pairs. + * + * \return True on started ok. + */ + void setup( + std::vector& targets) { + + std::lock_guard lockApi(_apiMutex); + _targets = targets; + + // So UI threads can get transfer status asynchronously + // once we start, save off the transfer max in number of files. + _maxTransfer = (uint32)targets.size(); + + // Copy the key info in. + _desired = Complete; + _lastState = None; + } + + /*! + * \brief Do the update for file. + */ + virtual void _doUpdate() { + constexpr std::chrono::duration waitRetry(250); + + // In theory this is fast, so "status" is about number of files copied. + bool ok = true; + for (IDataProvider* p : _targets) { + if (_desired == Complete) { + ok = p->copyFile(); + } + else { + break; + } + if (!ok) { + break; + } + _transfered++; + } + + _state = ok ? UpdateState::Completed : UpdateState::FileFailure; + } + +protected: + std::vector _targets; ///< File targets to copy. +}; + +} // namespace::SwUpdate + +#endif // UI_PROTOCOL_H_ Index: sources/update/UiSwUpdate.cpp =================================================================== diff -u --- sources/update/UiSwUpdate.cpp (revision 0) +++ sources/update/UiSwUpdate.cpp (revision 20b370a54d2737831b307a0de82aec9e06e2b772) @@ -0,0 +1,229 @@ +/*! + * + * Copyright (c) 2023 Diality Inc. - All Rights Reserved. + * + * \copyright + * THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN + * WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. + * + * \file UiSwUpdate.cpp + * \author (last) Phil Braica + * \date (last) 23-Jan-2023 + * \author (original) Phil Braica + * \date (original) 23-Jan-2023 + * + */ + +#include "UiSwUpdate.h" +#include "IDataProvider.h" +#include + +namespace SwUpdate { + +/*! + * \brief Create the instance. + * + * \return Instance reference. + */ +UiSwUpdate & UiSwUpdate::instance() { + static UiSwUpdate inst; + return inst; +} + +/*! + * \brief Constructor. + * + * \param target Target to go after for update. + */ +UiSwUpdate::UiSwUpdate(): + _hdUpdate(HD), + _hdFpgaUpdate(HDFPGA), + _dgUpdate(DG), + _dgFpgaUpdate(DGFPGA), + _fileUpdate() { + + // Assign by target. + _all[HD] = &_hdUpdate; + _all[HDFPGA] = &_hdFpgaUpdate; + _all[DG] = &_dgUpdate; + _all[DGFPGA] = &_dgFpgaUpdate; + _all[LINUX] = &_fileUpdate; +} + + +/*! + * \brief Destructor (virtual). + */ +UiSwUpdate::~UiSwUpdate() { + std::lock_guard lock(_mutexApi); + + // Abort all. + for (auto it = _all.begin(); it != _all.end(); ++it) { + it->second->abort(); + } +} + + +/*! + * \brief Start updating. + * + * \param dataProviders + * + * \return True on successfully started. + */ +bool UiSwUpdate::start( + std::vector & dataProviders) { + + // Lock. + std::lock_guard lock(_mutexApi); + + // We first need to do 2 things: + // Make an order list of the things that are process based by target. + // Get a list of the files, UI then linux. + + std::map aboutToStart; + std::vector uiFiles; + std::vector linuxFiles; + std::vector priorTargets; + + // Make a list we can access via map to get ordering, and get the files. + for (IDataProvider* pDp : dataProviders) { + + SwUpdateTargetEnum target = pDp->targetName; + + // Handle files. + if (target == UI) { + uiFiles.push_back(pDp); + continue; + } + if (target == LINUX) { + linuxFiles.push_back(pDp); + continue; + } + + // Set the provider. + UiProtocol* pTarget = _all[target]; + aboutToStart[target] = pTarget; + _all[target]->setProvider(pDp); + + // Ensure all non-file actions happen after the tasks. + priorTargets.push_back(pTarget); + } + + // Ensure we get the UI files then linux. + uiFiles.insert(uiFiles.end(), linuxFiles.begin(), linuxFiles.end()); + + // Use the map to create the ordered set of tasks to do. + _tasks.clear(); + SwUpdateTargetEnum targetOrder[] = { HDFPGA, HD, DGFPGA, DG }; + for (SwUpdateTargetEnum t : targetOrder) { + auto it = aboutToStart.find(t); + if (it != aboutToStart.end()) { + _tasks.push_back(it->second); + } + } + // Last is files. + _tasks.push_back(&_fileUpdate); + _fileUpdate.setup(uiFiles); + + // Kick the first task off. + bool ok = true; + if (_tasks.size() > 0) { + ok = _tasks.front()->start(); + } + + return ok; +} + +/*! + * \brief Is it completed? + * + * \return True if completed. + */ +bool UiSwUpdate::completedAll() { + bool rv = true; + + std::lock_guard lock(_mutexApi); + + for (auto it = _all.begin(); it != _all.end(); ++it) { + if (it->second->progress().inProgress) { + rv = false; + break; + } + } + return rv; +} + +/*! + * \brief Abort update. + */ +void UiSwUpdate::abort() { + + std::lock_guard lock(_mutexApi); + + // Doing all causes things that "finished" to also be checked + // in case of any possible thread race condition. + for (auto it = _all.begin(); it != _all.end(); ++it) { + it->second->abort(); + } +} + +/*! + * \brief Current estimated progress. + * + * \return Vector of statuses. + */ +std::vector UiSwUpdate::progress() { + + std::lock_guard lock(_mutexApi); + std::vector rv; + + for (auto it = _all.begin(); it != _all.end(); ++it) { + rv.push_back(it->second->progress()); + } + + return rv; +} + +/*! + * \brief Receive and check if matched. + * + * \param msgId Can message ID. + * \param pData Data pointer. + * + * \return True if message was consumed. + */ +bool UiSwUpdate::receive(uint16 msgId, uint8 * pData) { + + bool rv = false; + for (auto it = _all.begin(); it != _all.end(); ++it) { + rv |= it->second->receive(msgId, pData); + } + return rv; +} + +/*! + * \brief This chains updates together, as a protocol completes, it calls this. + * + * \param ok The task completed ok. + * \param pCompleted Which protocol completed. + */ +void UiSwUpdate::taskCompleted(bool ok, UiProtocol* pCompleted) { + + // Tasks are run in order: HDFPGA, HD, DGFPGA, DG, Linux, UI. + if (!ok) { + this->abort(); + _tasks.clear(); + } else { + std::vector::iterator position = std::find(_tasks.begin(), _tasks.end(), pCompleted); + if (position != _tasks.end()) { + _tasks.erase(position); + } + if (_tasks.size() > 0) { + _tasks.front()->start(); + } + } +} + +} //namespace SwUpdate + Index: sources/update/UiSwUpdate.h =================================================================== diff -u --- sources/update/UiSwUpdate.h (revision 0) +++ sources/update/UiSwUpdate.h (revision 20b370a54d2737831b307a0de82aec9e06e2b772) @@ -0,0 +1,127 @@ +/*! + * + * Copyright (c) 2023 Diality Inc. - All Rights Reserved. + * + * \copyright + * THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN + * WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. + * + * \file UiSwUpdate.h + * \author (last) Phil Braica + * \date (last) 23-Jan-2023 + * \author (original) Phil Braica + * \date (original) 23-Jan-2023 + * + */ + +#ifndef UI_SW_UPDATE_H_ +#define UI_SW_UPDATE_H_ + +#include "HalStdTypes.h" +#include "MsgLink.h" +#include "UpdateProtocol.h" +#include "UiProtocol.h" +#include "UiUpdateStatus.h" + +#include +#include + +namespace SwUpdate { + +/** Forward declare. */ +class IDataProvider; + +/*! + * This class uses a thread plus a waitable condition variable + * to impliment a linear procedure to update one firmware + * "target". It uses "ID" slots for commands so that in theory + * multiple updaters running in paralell won't conflict. + * + * Note that this is deliberately NOT a statemachine even + * though it could be coded as one. Statemachines have overhead + * waiting for events and the goal is to have virtually zero + * time between events and in theory never really yielding unless + * the Firmware image gets too busy. + */ +class UiSwUpdate { +public: + /*! + * \brief Create the instance. + * + * \return Instance reference. + */ + static UiSwUpdate & instance(); + + /*! + * \brief Start updating. + * + * \param dataProviders + * + * \return True on successfully started. + */ + bool start( + std::vector& dataProviders); + + /*! + * \brief Is it completed? + * + * \return True if completed. + */ + bool completedAll(); + + /*! + * \brief Abort update. + */ + void abort(); + + /*! + * \brief Current estimated progress. + * + * \return Vector of statuses. + */ + std::vector progress(); + + /*! + * \brief Receive and check if matched. + * + * \param msgId Can message ID. + * \param pData Data pointer. + * + * \return True if message was consumed. + */ + bool receive(uint16 msgId, uint8 * pData); + + /*! + * \brief This chains updates together, as a protocol completes, it calls this. + * + * \param ok The task completed ok. + * \param pCompleted Which protocol completed. + */ + void taskCompleted(bool ok, UiProtocol* pCompleted); + +protected: + /*! + * \brief Constructor. + * + * \param target Target to go after for update. + */ + UiSwUpdate(); + + /*! + * \brief Destructor (virtual). + */ + virtual ~UiSwUpdate(); + + std::map _all; ///< All of them, key by kind. + std::vector _tasks; ///< Tasks to do. + UiProtocol _hdUpdate; ///< HD update. + UiProtocol _hdFpgaUpdate; ///< HD FPGA update. + UiProtocol _dgUpdate; ///< DG update. + UiProtocol _dgFpgaUpdate; ///< DG FPGA update. + UiProtocolFile _fileUpdate; ///< Files UI & Linux are done with this. + std::mutex _mutexApi; ///< Mutex so API calls are thread safe. +}; + +} //namespace SwUpdate + +#endif // UI_SW_UPDATE_H_ Index: sources/update/UiUpdateStatus.h =================================================================== diff -u --- sources/update/UiUpdateStatus.h (revision 0) +++ sources/update/UiUpdateStatus.h (revision 20b370a54d2737831b307a0de82aec9e06e2b772) @@ -0,0 +1,53 @@ +/*! + * + * Copyright (c) 2023 Diality Inc. - All Rights Reserved. + * + * \copyright + * THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN + * WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. + * + * \file UiUpdateStatus.h + * \author (last) Phil Braica + * \date (last) 23-Jan-2023 + * \author (original) Phil Braica + * \date (original) 23-Jan-2023 + * + */ + +#ifndef UI_UPDATE_STATUS_H_ +#define UI_UPDATE_STATUS_H_ + +#include "HalStdTypes.h" +#include + +namespace SwUpdate { + +struct UiUpdateStatus { + + /*! + * \brief Constructor. + */ + UiUpdateStatus() : + goal("Idle"), + targetName(""), + stepIndex(0), + totalSteps(0), + stepName(""), + percentTotal(0), + inProgress(false) { + + ; // NOP. + } + + ::std::string goal; ///< Either "Idle", "Complete" or "Shutdown". + ::std::string targetName; ///< Linux, UI, DG, DGFPGA, HG, HGFPGA + uint32 stepIndex; ///< Which step. + uint32 totalSteps; ///< Out of how many steps. + ::std::string stepName; ///< Step name. + float percentTotal; ///< Percent of all steps (estimated). + bool inProgress; ///< In progress or was this the abort/fail/complete point. +}; + +} // namespace SwUpdate + +#endif // UI_UPDATE_STATUS_H_ Index: sources/update/UpdateProtocol.c =================================================================== diff -u --- sources/update/UpdateProtocol.c (revision 0) +++ sources/update/UpdateProtocol.c (revision 20b370a54d2737831b307a0de82aec9e06e2b772) @@ -0,0 +1,668 @@ +/*! + * + * Copyright (c) 2023 Diality Inc. - All Rights Reserved. + * + * \copyright + * THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN + * WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. + * + * \file UpdateProtocol.c + * \author (last) Phil Braica + * \date (last) 23-Jan-2023 + * \author (original) Phil Braica + * \date (original) 23-Jan-2023 + * + */ + + +#include "UpdateProtocol.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define SECURE_ROT (27) + +/*! Streaming CRC support. */ +uint32 g_crcs_curr[NUM_CAN_TARGETS]; +uint32 g_crcs_prev[NUM_CAN_TARGETS]; +uint16 g_crcs_index[NUM_CAN_TARGETS]; + +/** This is a shared secret to whiten the data, they are 4 randomly chosen prime numbers. */ +const uint8 g_securitySeeds[4] = { 37, 71, 113, 181 }; + +/** Unroll CRC by 8 when we can. */ +#define CRC_BLOCK_UNROLL (8) + +// CRC is based on the open source code here: +// https://web.mit.edu/freebsd/head/sys/libkern/crc32.c +// and +// https://wiki.osdev.org/CRC32 +/*! + * CRC 32 standard table. + * + * This is computed because it avoids having the obvious CRC32 table + * pattern in the file. + */ +uint32 crc32_tab[256]; + + +/*! + * \brief From a command byte extract the target. + * + * \param cmd The command byte. + * + * \return The target of the command. + */ +SwUpdateTargetEnum SwUpdate_getTarget(uint8 cmd) { + return (SwUpdateTargetEnum)(cmd >> 4); +} + +/*! + * \brief From a command byte extract the command. + * + * \param cmd The command byte. + * + * \return The kind of command. + */ +SwUpdateCmdEnum SwUpdate_getCmd(uint8 cmd) { + return (SwUpdateCmdEnum)(cmd & 0xf); +} + +/*! + * \brief From target and command kind compose the command byte. + * + * \param target The processor target. + * \param cmd The command kind. + * + * \return The command byte. + */ +uint8 SwUpdate_FormCommand(SwUpdateTargetEnum target, SwUpdateCmdEnum cmd) { + return ((uint8)target << 4) | ((uint8)cmd & 0xf); +} + +#if 0 +const uint32 crc32_table_test = { + 0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f, + 0xe963a535, 0x9e6495a3, 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988, + 0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91, 0x1db71064, 0x6ab020f2, + 0xf3b97148, 0x84be41de, 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7, + 0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, 0x14015c4f, 0x63066cd9, + 0xfa0f3d63, 0x8d080df5, 0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172, + 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, 0x35b5a8fa, 0x42b2986c, + 0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59, + 0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423, + 0xcfba9599, 0xb8bda50f, 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924, + 0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d, 0x76dc4190, 0x01db7106, + 0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433, + 0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb, 0x086d3d2d, + 0x91646c97, 0xe6635c01, 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e, + 0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, 0x65b0d9c6, 0x12b7e950, + 0x8bbeb8ea, 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65, + 0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7, + 0xa4d1c46d, 0xd3d6f4fb, 0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0, + 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9, 0x5005713c, 0x270241aa, + 0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f, + 0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81, + 0xb7bd5c3b, 0xc0ba6cad, 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a, + 0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683, 0xe3630b12, 0x94643b84, + 0x0d6d6a3e, 0x7a6a5aa8, 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1, + 0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe, 0xf762575d, 0x806567cb, + 0x196c3671, 0x6e6b06e7, 0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc, + 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, 0xd6d6a3e8, 0xa1d1937e, + 0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b, + 0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55, + 0x316e8eef, 0x4669be79, 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236, + 0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f, 0xc5ba3bbe, 0xb2bd0b28, + 0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d, + 0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, 0x9c0906a9, 0xeb0e363f, + 0x72076785, 0x05005713, 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38, + 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, 0x86d3d2d4, 0xf1d4e242, + 0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777, + 0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69, + 0x616bffd3, 0x166ccf45, 0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2, + 0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db, 0xaed16a4a, 0xd9d65adc, + 0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9, + 0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605, 0xcdd70693, + 0x54de5729, 0x23d967bf, 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94, + 0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d +}; +#endif + +/*! + * \brief Generate a CRC32 LUT. + * + * Ideally we don't want an obvious CRC table in our + * source code for cyber reasons. + */ +void crc32_fill() { + static bool once = true; + if (once) { + once = false; + for (uint16 index = 0; index < 256; index++) { + uint32 val = index; + for (uint8 z = 8; z> 0; z--) { + val = (val&1) ? (val>>1) ^ 0xEDB88320:val>>1; + } + crc32_tab[index] = val; + } + } +} + +/*! + * \brief CRC 32 for a buffer. + * + * \param pBuf Buffer. + * \param size Buffer size. + * \param crc Old CRC, use 0xFFFFFFFF to init. + * + * \return Updated CRC value. + */ +uint32 crc32Update(const uint8* pBuf, uint32 size, uint32 crc) { + crc32_fill(); + + // Typically 8 bytes or in sets of 8 bytes. + while (size > CRC_BLOCK_UNROLL) { + crc = crc32_tab[(crc ^ *pBuf++) & 0xFF] ^ (crc >> 8); + crc = crc32_tab[(crc ^ *pBuf++) & 0xFF] ^ (crc >> 8); + crc = crc32_tab[(crc ^ *pBuf++) & 0xFF] ^ (crc >> 8); + crc = crc32_tab[(crc ^ *pBuf++) & 0xFF] ^ (crc >> 8); + + crc = crc32_tab[(crc ^ *pBuf++) & 0xFF] ^ (crc >> 8); + crc = crc32_tab[(crc ^ *pBuf++) & 0xFF] ^ (crc >> 8); + crc = crc32_tab[(crc ^ *pBuf++) & 0xFF] ^ (crc >> 8); + crc = crc32_tab[(crc ^ *pBuf++) & 0xFF] ^ (crc >> 8); + + size -= CRC_BLOCK_UNROLL; + } + + while (size--) { + crc = crc32_tab[(crc ^ *pBuf++) & 0xFF] ^ (crc >> 8); + } + return crc; +} + +/*! + * \brief CRC 32 for a byte. + * + * \param val Byte to use. + * \param crc Old CRC, use 0xFFFFFFFF to init. + * + * \return Updated CRC value. + */ +uint32 crc32UpdateByte(const uint8 val, uint32 crc) { + crc32_fill(); + return crc32_tab[(crc ^ val) & 0xFF] ^ (crc >> 8); +} + +/*! + * \brief CRC 32 for a buffer. + * + * \note Not named crc32 because on Linux Qt will link + * PNG image load to this function with a size of 0xFFFFFFFF. + * + * \param pBuf Buffer. + * \param size Buffer size. + * + * \return CRC of that buffer. + */ +uint32 crc32Fast(const uint8* pBuf, uint32 size) { + return crc32Update(pBuf, size, 0xFFFFFFFF) ^ 0xFFFFFFFF; +} + +/*! + * \brief CRC a packet. + * + * \param pBuf Buffer. + * \param size Buffer size + * + * \return CRC of that buffer. + */ +uint32 crc32Packet(const uint8* pBuf, uint32 size, uint32 crc) { + crc32_fill(); + + #if 0 // OLD + for (uint32 ii = 0; ii < size; ii++) { + // No CRC of the second word of a packet. + if ((ii < 4) || (ii >= 8)) { + crc = crc32_tab[(crc ^ pBuf[ii]) & 0xFF] ^ (crc >> 8); + } + } + #else + + // This is unrolled and MUCH faster. + + // Always the first 4 bytes. + crc = crc32_tab[(crc ^ *pBuf++) & 0xFF] ^ (crc >> 8); + crc = crc32_tab[(crc ^ *pBuf++) & 0xFF] ^ (crc >> 8); + crc = crc32_tab[(crc ^ *pBuf++) & 0xFF] ^ (crc >> 8); + crc = crc32_tab[(crc ^ *pBuf++) & 0xFF] ^ (crc >> 8); + + // Skip 32 bits + pBuf += sizeof (uint32); + + // We consumed 32 bits and skipped 32 bits. + size -= sizeof (uint32) + sizeof(uint32); + while (size--) { + crc = crc32_tab[(crc ^ *pBuf++) & 0xFF] ^ (crc >> 8); + + } + #endif + return crc; +} + +/*! + * \brief CRC 32 for a buffer. + * + * \param crc CRC to finalize. + * + * \return Updated CRC value. + */ +uint32 crc32Finalize(uint32 crc) { + return crc ^ 0xFFFFFFFF; +} + +/*! + * \brief Create and write into the buffer the security token. + * + * \param pData Packet to verify. + * \param size Size in bytes. + */ +void SwUpdate_createSecurity(uint8* pData, uint32 size) { + // This adds 2 bits of whitening which can make analysis of + // the byte stream harder to cyber attack. + static uint8 seed = 0; + seed = seed + 1; + + // Blank out the 2nd word, it is always the location of the security token. + uint32* pWord = (uint32*)pData; + + // Update the crc. + uint32 crc = crc32Packet(pData, size, 0xFFFFFFFF); + //printf(" pck: %4x seeded: ", crc); + + // Use the 2 bit whitening. + crc = crc32UpdateByte(g_securitySeeds[seed & 0x3], crc); + //printf(" %4x \n", crc); + + // Write it. + pWord[1] = crc32Finalize(crc); +} + +/*! + * \brief All packets have the second 32 bit word as security. + * + * \details If this fails, this function automatically invokes + * security features to take note of a potential threat. + * + * \param pData Packet to verify. + * \param size Size in bytes. + * + * \return True if verified. + */ +bool SwUpdate_verifySecurity(const uint8* pData, uint32 size) { + + // Assuming things are often in order, this cuts the work on the verify side statistically. + static uint8 nxtSeed = 0; + + // Update the crc skipping the 2nd word. + uint32 crc = crc32Packet(pData, size, 0xFFFFFFFF); + + // Grab the security token. + uint32 token = ((uint32*)pData)[1]; + + // Use the 2 bit whitening. + for (uint8 seed = nxtSeed; seed < 4 + nxtSeed; seed++) { + + // Check this seeding. + uint32 crc_tmp = crc32UpdateByte(g_securitySeeds[seed & 0x3], crc); + + crc_tmp = crc32Finalize(crc_tmp); + + // If match use it and cache for next expected seed. + if (crc_tmp == token) { + nxtSeed = seed + 1; + nxtSeed &= 0xF; + return true; + } + } + + return false; +} + +/*! + * \brief Create and write into the buffer the security token for a data transfer + * + * This is forming a response to the UI, always target of UI. + * + * \param pResponse Packet responding pointer. + * \param pData Data we received pointer. + */ +void SwUpdate_createSecurityForData(struct SwUpdateResponse* pResponse, uint8 * pData) { + static uint32 rand = 0; + + // Assign the whitening bytes, this should vary, we don't care too much. + // This is why no other whitening is required. + pResponse->rand = 0xFFFF & crc32Finalize(crc32Update((uint8*)&rand, 4, 0)); + + uint32 crc = crc32Packet((uint8*)pResponse, sizeof(struct SwUpdateResponse), 0xFFFFFFFF); + crc = crc32Update(pData, sizeof(struct SwUpdateDataBuffer), crc); + pResponse->token = crc; +} + +/*! + * \brief Encode / obfuscate the data. + * + * \param pData Data to encode. + */ +void SwUpdate_encodeData(struct SwUpdateDataBuffer* pData) +{ + // We always transfer the whole thing. + for (uint32 ii = 0; ii < MAX_TRANSFER_SIZE; ii++) + { + pData->buffer[ii] = 0xFF & (pData->buffer[ii] + SECURE_ROT); + } +} + +/*! + * \brief Decode / de-obfuscate the data. + * + * \param pData Data to decode. + */ +void SwUpdate_decodeData(struct SwUpdateDataBuffer* pData) +{ + for (uint32 ii = 0; ii < MAX_TRANSFER_SIZE; ii++) + { + pData->buffer[ii] = 0xFF & (pData->buffer[ii] - SECURE_ROT); + } +} + +/*! + * \brief After getting a secure response for data UI sent, check the returned security token. + * + * This checks the response to a data message. + * + * \param pResponse Packet responding pointer. + * \param pData Data we had sent pointer. + */ +BOOL SwUpdate_checkSecurityForData(const uint8* pResponse, const uint8 * pData) { + + uint32 crc = crc32Packet(pResponse, sizeof(struct SwUpdateResponse), 0xFFFFFFFF); + crc = crc32Update(pData, sizeof(struct SwUpdateDataBuffer), crc); + + struct SwUpdateResponse* pResponseStruct = (struct SwUpdateResponse*)pResponse; + if (pResponseStruct->token == crc) { + return TRUE; + } + return FALSE; +} + +/*! + * \brief Go through the data and stream it into the verification code + * + * \param target Which target is this for? + */ +void SwUpdate_resetVerificationData(SwUpdateTargetEnum target) { + g_crcs_curr[target % NUM_CAN_TARGETS] = 0xFFFFFFFF; + g_crcs_prev[target % NUM_CAN_TARGETS] = 0xFFFFFFFF; + g_crcs_index[target % NUM_CAN_TARGETS] = 0; +} + +/*! + * \brief Go through the data and stream it into the verification code + * + * This currently streams the data in, allowing for retries of the last item, + * by using the index value to roll back and re-apply if needed. + * + * \param index Index of the data. + * \param pData Data pointer. + * \param size Size of the data. + * \param target Which target is this for? + */ +void SwUpdate_updateVerificationData( + uint16 index, + uint8* pData, + uint32 size, + SwUpdateTargetEnum target) { + + // State | Action + // index = old +1 | previous = current, update current. + // index = old | current = previous, update current. + uint32 ii = target % NUM_CAN_TARGETS; + if (index == g_crcs_index[ii]) + { + g_crcs_curr[ii] = g_crcs_prev[ii]; + } + else + { + g_crcs_prev[ii] = g_crcs_curr[ii]; + } + + g_crcs_index[ii] = index; + g_crcs_curr[ii] = crc32Update(pData, size, g_crcs_curr[ii]); +} + +/*! + * \brief Compute / get the data for the image. + * + * \param asVerify If true, verify token else version token. + * \param target Which target. + * + * \return Version/verification token. + */ +uint32 SwUpdate_computeVerificationData(bool asVerify, SwUpdateTargetEnum target) { + uint32 crc = g_crcs_curr[target % NUM_CAN_TARGETS]; + + crc = crc32Update( + asVerify ? (uint8*)"security" : (uint8*)"version", + asVerify ? 8 : 7, + crc); + crc = crc32Finalize(crc); + return crc; +} + +typedef unsigned long long rsa64_BigInt; +#define SALT_1 (29) +#define SALT_2 (73) +#define SALT_3 (127) + +/** + * @brief Do the multiply/mod fast. + * + * Computes (m^key)%n. + * + * @param m Value. + * @param key The key + * @param n Modulus. + * + * @return Value. + */ +rsa64_BigInt rsa64_multiMod2( + rsa64_BigInt m, + rsa64_BigInt key, + rsa64_BigInt n) +{ + // m is <= 64 bits in size, and <= 64 bits. + // n is <= 64 bits. + // key <= 32 bits. + rsa64_BigInt symbol = 1; + + while (key > 0) + { + if (key % 2 == 1) + { + symbol = (symbol * m) % n; + } + key = key / 2; + + m = (m * m) % n; + } + return symbol; +} + +/*! + * \brief Is the cryptography signature valid? + * + * \param pSignature Pointer to MAX_TRANSFER_SIZE signature. + * \param target SwUpdateTargetEnum, 0=HD, 1=HDFPGA, 2=DG, 3=DGFPGA. + * + * \return 1 if passed else 0. + */ +uint32 SwUpdate_isCryptoSignedOk(uint8* pSignature, uint8 target) { + const rsa64_BigInt n = 0xfd2a058f; + const rsa64_BigInt publicKey = 0xe904d9313; + uint32 crc1 = SwUpdate_computeVerificationData(true, (SwUpdateTargetEnum)target); + uint32 crc2 = SwUpdate_computeVerificationData(false, (SwUpdateTargetEnum)target); + uint32 digest[16]; // 64 bytes of digest. + for (uint32 ii = 0; ii < 16; ii += 4) { + digest[ii + 0] = crc1 + ii; + digest[ii + 1] = crc2 + ii; + digest[ii + 2] = crc1 + ii + SALT_1; + digest[ii + 3] = crc2 + ii + SALT_2; + } + + // Now decode and compare. + uint16* pDigestShort = (uint16*)digest; + uint32 jj = 0; + for (uint32 ii = 0; ii < 32; ii++) + { + rsa64_BigInt a = pSignature[jj++]; + rsa64_BigInt b = pSignature[jj++]; + rsa64_BigInt c = pSignature[jj++]; + rsa64_BigInt d = pSignature[jj++]; + rsa64_BigInt m = a + (b << 8) + (c << 16) + (d << 24); + rsa64_BigInt k = rsa64_multiMod2(m, publicKey, n); + uint16 kshort = (uint16)k; + if (kshort != pDigestShort[ii]) { + return 0; + } + } + return 1; +} + + +/*! + * \brief Make a signature. + * + * \param crc1 CRC1 to do. + * \param crc2 CRC2 to do. + * \param pSignature Pointer to MAX_TRANSFER_SIZE signature. + * + * \return 1 if passed else 0. + */ +void SwUpdate_makeSignature(uint32 crc1, uint32 crc2, uint8 *pSignature) { + const rsa64_BigInt n = 0xfd2a058f; + const rsa64_BigInt privateKey = 0xb; + uint32 digest[16]; // 4 * 16 = 64 bytes of digest. + for (uint32 ii = 0; ii < 16; ii += 4) { + digest[ii + 0] = crc1 + ii; + digest[ii + 1] = crc2 + ii; + digest[ii + 2] = crc1 + ii + SALT_1; + digest[ii + 3] = crc2 + ii + SALT_2; + } + + // This outputs 32 items each 64 bits but the top 32 bits are unused. + uint16* pShort = (uint16*)digest; + uint32 jj = 0; + for (uint32 ii = 0; ii < 32; ii++) { + rsa64_BigInt m = (rsa64_BigInt)pShort[ii]; + rsa64_BigInt k = rsa64_multiMod2(m, privateKey, n); + + // k is now 64 bits, but the top 32 are zeros. + // Copy the lower 32 into pSignature. + pSignature[jj++] = k & 0xFF; + pSignature[jj++] = (k >> 8) & 0xFF; + pSignature[jj++] = (k >> 16) & 0xFF; + pSignature[jj++] = (k >> 24) & 0xFF; + } +} + + +// This is reference code to create your own keys: +#if 0 + +// to find gcd +int _rsa64_gcd(rsa64_BigInt a, rsa64_BigInt b) +{ + rsa64_BigInt tmp = a % b; + while (tmp > 0) + { + a = b; + b = tmp; + tmp = a%b; + } + return b == 1 ? 1 : 0; +} + +// Compute keys E, D and N from primes p, q. +// Make sure +int rsa64_computeKeys( + rsa64_BigInt p, + rsa64_BigInt q, + rsa64_BigInt* pE, + rsa64_BigInt* pD, + rsa64_BigInt* pN) +{ + + rsa64_BigInt n = p * q; + rsa64_BigInt t = (p-1) * (q-1); + + // This will fail to be useful or just plain overflow/fail if: + // sum_bits > 63 (likely overflow). + // t > n (definite overflow) + // n < 256 (not usable). + if ((t > n) || (n < 256) || (n > 0xFFFFFFFF) + { + return 0; + } + + int foundPair = 0; + + const rsa64_BigInt limit = 0xFFFFFFFFFFFFFFFF - t - t; + + // So that we don't always use the first found pair ... + int targetCount = 1 + (t % 7); + + for (rsa64_BigInt ii = 2; (ii < t) && (!foundPair); ii++) + { + // ii is not a candidate. + if (t % ii == 0) + { + continue; + } + // _rsa64_isPrime(ii); is an alternative. + int flag = _rsa64_gcd(t, ii); + + // ii might be a candidate... + if (flag && (ii != p) && (ii != q)) + { + // ii is a possible encryption private key. + // does d (public key) exist? + for (rsa64_BigInt kk = 1 + t; kk < limit; kk += t) + { + if (((kk % ii) == 0) && (kk > ii)) + { + // d exists. + *pE = ii; + *pD = kk / ii; + *pN = n; + + // Increment number of solutions, we have at least 1 now. + foundPair++; + + // If number of found solutions == target, good. + if (targetCount <= foundPair) { + break; + } + } + } + } + } + return foundPair > 0 ? 1 : foundPair; +} +#endif + +#ifdef __cplusplus +} +#endif + Index: sources/update/UpdateProtocol.h =================================================================== diff -u --- sources/update/UpdateProtocol.h (revision 0) +++ sources/update/UpdateProtocol.h (revision 20b370a54d2737831b307a0de82aec9e06e2b772) @@ -0,0 +1,329 @@ +/*! + * + * Copyright (c) 2023 Diality Inc. - All Rights Reserved. + * + * \copyright + * THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN + * WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. + * + * \file UpdateProtocol.h + * \author (last) Phil Braica + * \date (last) 23-Jan-2023 + * \author (original) Phil Braica + * \date (original) 23-Jan-2023 + * + */ + + /*! + * + * This is shared between the 'C' and 'C++' code modules + * and defines the principal messaging between the UI and FW + * code bases for software update. + */ + +#ifndef UPDATE_PROTOCOL_H_ +#define UPDATE_PROTOCOL_H_ + +#include "HalStdTypes.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/*! Optimize for speed. */ +#define MAX_TRANSFER_SIZE (128) ///< Payload size, must be big enough +#define MAX_DATA_TRANSFERS (4) ///< Number of active data cmd/response slots at once, per target. +#define NUM_CAN_TARGETS (4) ///< There are 4 CAN targets off of the UI. +#define CAN_PAYLOAD_SIZE (8) ///< 8 bytes per native CAN frame. +#define KBPS_WIRE (250) ///< The line speed in bps, 250 = 250 kbps, max 1250 kbps. + +/*! + *
 
+ * Bytes per index increment. At 64, 4,194,304 bytes addressable.
+ * 1. We need about half that as a minimum.
+ * 2. MAX_TRANSFER_SIZE must be greater than BYTES_PER_INDEX, and
+ * 3. MAX_TRANSFER_SIZE / BYTES_PER_INDEX must be an integer.
+ * 
+ */ +#define BYTES_PER_INDEX (64) + +/*! + * The CAN bus IDs used for update. + * + * So that theoretically an image could do both a FW and FPGA + * stream at the same time without the header data transmitted + * on each 8 byte transfer... The only way to 100% be sure the + * streams don't get confused by the "mailbox" system used by + * TI style CAN bus HW is to make them distinct command IDs. + */ +typedef enum SwUpdateCanMsgIds { + CommandId = 1, ///< SwUpdateCommand ID. + DataBufferId_FW = 2, ///< SwUpdateDataBuffer ID for FW. + DataBufferId_FPGA = 3, ///< SwUpdateDataBuffer ID for FPGA. + ResponseId = 4, ///< SwUpdateResponse ID. + VerifyId = 5, ///< SwUpdateResponse ID. + NumMsgIds ///< Always last. +} SwUpdateCanMsgIds; + +/*! + * The target for the update. + */ +typedef enum SwUpdateTargetEnum { + // Start transfer. + HD = 0, + HDFPGA, + DG, + DGFPGA, + + // Only used within UI, never in messages. + UI, + LINUX +} SwUpdateTargetEnum; + +/*! +Normal Protocol: + UI->FW: SwUpdateCommand[cmd=start] + FW->UI: Secure Ack/Nack + loop + UI->FW: Data + FW->UI: Secure Ack/Nack <- Also flow control. + end + + UI->FW: Verify + FW->UI: SwUpdateVerifyResponse + + UI->FW: Version + FW->UI: SwUpdateVerifyResponse + + UI->FW: Verified UI thinks things are good. + FW->UI: ACK/NACK FW has agreed or not and set the EPROM memory that the image is good or not. + + UI->FW: RunApp Launch the main app. + FW->UI: Secure Ack/Nack + + If streaming encounters problems we use a command with Resync + so that the FW will dump it's indexes and be ready for new data. +*/ +typedef enum SwUpdateCmdEnum { + Start = 0, + Abort, + RunApp, + Verify, + Version, + Verified, + Resync +} SwUpdateCmdEnum; + +/*! + * \brief From a command byte extract the target. + * + * \param cmd The command byte. + * + * \return The target of the command. + */ +extern SwUpdateTargetEnum SwUpdate_getTarget(uint8 cmd); + +/*! + * \brief From a command byte extract the command. + * + * \param cmd The command byte. + * + * \return The kind of command. + */ +extern SwUpdateCmdEnum SwUpdate_getCmd(uint8 cmd); + +/*! + * \brief From target and command kind compose the command byte. + * + * \param target The processor target. + * \param cmd The command kind. + * + * \return The command byte. + */ +extern uint8 SwUpdate_FormCommand(SwUpdateTargetEnum target, SwUpdateCmdEnum cmd); + +/*! + * \brief This is a secure response. 1 atomic CAN bus transaction. + * + * \param id Tell the FW one command discussion is distinct from another. + * \param cmd A variety of commands. + * \param rand Whiten the data for the security token. + * \param token 32 bit security token. + */ +struct SwUpdateCommand { + uint8 id; ///< Command/response slot ID. + uint8 cmd; ///< See above. + uint16 rand; ///< Whitens the data. + uint32 token; ///< Security token. +}; + +/*! + * \brief Software update data buffer. + * + * This maps to the bus data. The protocol allows for a + * sender to have MAX_DATA_TRANSFERS active IDs to track + * responses. This ensures a conversation with the FW is + * stateless, and can be abandoned by the UI to restart / + * recover due to CAN bus collisions or 3rd party bus use + * issues. + * + * Things are packed in... + */ +struct SwUpdateDataBuffer { + // First word. + uint8 id; ///< Command/response slot ID. + uint8 kind; ///< 0=HD, 1=HDFPGA, 2=DG, 3=DGFPGA + uint16 index; ///< Storage index, 1 increment is BYTES_PER_INDEX bytes, max is flag for signature. + + // Second word + uint32 token; ///< Security token. + + // Additional transfers. + uint8 buffer[MAX_TRANSFER_SIZE]; ///< Buffer. +}; + +/*! + * \brief This is a secure response. 1 atomic CAN bus transaction. + */ +struct SwUpdateResponse { + uint8 id; ///< Command/response slot ID. + uint8 ackNack; ///< 1=Ack, 0=Nack. + uint16 rand; ///< Whitens the data. + uint32 token; ///< Security token. +}; + +/*! + * \brief This is a secure response. 1 atomic CAN bus transaction. + * + * These are tokens, there are two uses of the message, with two different + * tokens to verify. + */ +struct SwUpdateVerifyResponse { + uint32 data; ///< Either CRC or version ID. + uint32 token; ///< Security token. +}; + +/*! + * \brief Create and write into the buffer the security token. + * + * \param pData Packet to verify. + * \param size Size in bytes. + */ +extern void SwUpdate_createSecurity( uint8* pData, uint32 size); + +/*! + * \brief All packets have the second 32 bit word as security. + * + * \details If this fails, this function automatically invokes + * security features to take note of a potential threat. + * + * \param target Who is receiving this packet. + * \param pData Packet to verify. + * \param size Size in bytes. + * + * \return True if verified. + */ +extern bool SwUpdate_verifySecurity(const uint8* pData, uint32 size); + +/*! + * \brief Create and write into the buffer the security token for a data transfer + * + * This is forming a response to the UI, always target of UI. + * + * \param pResponse Packet responding pointer. + * \param pData Data we received pointer. + */ +extern void SwUpdate_createSecurityForData(struct SwUpdateResponse* pResponse, uint8 * pData); + +/*! + * \brief Encode / obfuscate the data. + * + * \param pData Data to encode. + */ +extern void SwUpdate_encodeData(struct SwUpdateDataBuffer* pData); + +/*! + * \brief Decode / de-obfuscate the data. + * + * \param pData Data to decode. + */ +extern void SwUpdate_decodeData(struct SwUpdateDataBuffer* pData); + +/*! + * \brief After getting a secure response for data UI sent, check the returned security token. + * + * This checks the response to a data message. + * + * \param pResponse Packet responding pointer. + * \param pData Data we had sent pointer. + */ +extern BOOL SwUpdate_checkSecurityForData(const uint8 * pResponse, const uint8 * pData); + +/*! + * \brief Go through the data and stream it into the verification code + * + * \param target Which target is this for? + */ +extern void SwUpdate_resetVerificationData(SwUpdateTargetEnum target); + +/*! + * \brief Go through the data and stream it into the verification code + * + * This currently streams the data in, allowing for retries of the last item, + * by using the index value to roll back and re-apply if needed. + * + * \param index Index of the data. + * \param pData Data pointer. + * \param size Size of the data. + * \param target Which target is this for? + */ +extern void SwUpdate_updateVerificationData( + uint16 index, + uint8* pData, + uint32 size, + SwUpdateTargetEnum target); + +/*! + * \brief Compute / get the data for the image. + * + * \param asVerify If true, verify token else version token. + * \param asFwImage IF true FW image else FPGA image. + * + * \return Version/verification token. + */ +extern uint32 SwUpdate_computeVerificationData(bool asVerify, SwUpdateTargetEnum target); + +/*! + * \brief Is the cryptography signature valid? + * + * \param pSignature Pointer to MAX_TRANSFER_SIZE signature. + * \param target SwUpdateTargetEnum, 0=HD, 1=HDFPGA, 2=DG, 3=DGFPGA. + * + * \return 1 if passed else 0. + */ +extern uint32 SwUpdate_isCryptoSignedOk(uint8* pSignature, uint8 target); + + +///////////////////// +// ONLY compile this function on the UI SOM. +///////////////////// + +#ifdef COMPILING_ON_UI + +/*! + * \brief Make a signature. + * + * \param crc1 CRC1 to do. + * \param crc2 CRC2 to do. + * \param pSignature Pointer to MAX_TRANSFER_SIZE signature. + * + * \return 1 if passed else 0. + */ +void SwUpdate_makeSignature(uint32 crc1, uint32 crc2, uint8 *pSignature); + +#endif +#ifdef __cplusplus +} +#endif + +#endif // UPDATE_PROTOCOL_H_ Index: sources/update/VSwUpdate.cpp =================================================================== diff -u --- sources/update/VSwUpdate.cpp (revision 0) +++ sources/update/VSwUpdate.cpp (revision 20b370a54d2737831b307a0de82aec9e06e2b772) @@ -0,0 +1,824 @@ +/*! + * + * Copyright (c) 2023 Diality Inc. - All Rights Reserved. + * + * \copyright + * THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN + * WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. + * + * \file UpdateProtocol.c + * \author (last) Phil Braica + * \date (last) 27-Apr-2023 + * \author (original) Phil Braica + * \date (original) 27-Apr-2023 + * + */ + +#include "VSwUpdate.h" + +#include "ApplicationController.h" +#include "CanInterface.h" +#include "Logger.h" +#include "MessageDispatcher.h" + +#include +#include +#include +#include +#include +#include +#include +#include + + +// Enables (1) or disables (!1) reboot to allow for better testing. +// Default is to reboot when this is finished. +#ifndef REBOOT_ENABLED +#define REBOOT_ENABLED (0) +#endif + +#define MSG_ID_HD_ENTER_BOOTLOADER_NOW (0x808F) ///< HD Jump to the bootloader application +#define MSG_ID_HD_REBOOT_NOW (0x8090) ///< HD Reboot RM46 immediately +#define MSG_ID_HD_SET_ENTER_BOOTLOADER (0x8091) ///< HD Set the flag to stay + +/*! + * \brief Constructor. + * + * \param parent Parent object pointer, optional. + */ +VSWUpdate::VSWUpdate(QObject* parent) + : QAbstractTableModel(parent), + _pubKey(""), + _persistent(""), + _oldUpdates(""), + _factoryImg(""), + _wifiDir(""), + _maxOld(3), + _updating(false), + _percent(0), + _selected(-1), + _stateMsg(" "), // Not "", so notify occurs. + _buttonText(" "), // Not "Start", so notify occurs. + _mpBackButton(nullptr), + _isVisible(false), + _progressVisible(false), + _postWasOk(false), + _settingsOk(false) +{ + setButtonText("Start"); + setState(""); + + controlUSB(true); + + // Setup the directories and parameters. + QStringList usbDirs; + usbDirs.append("/media/usb/"); + usbDirs.append("/home/denali/Projects/update/usbTest"); + QString factoryImage = "/home/denali/Projects/update/factory"; + QString wifiDir = "/home/denali/Projects/update/wifi"; + QString cacheDir = "/home/denali/Projects/update/cache"; + setup( + usbDirs, + factoryImage, + wifiDir, + cacheDir, + 3, + "/home/denali/Projects/update/public.pem"); + + // Determine if this was a trial boot after an install or not. + // If we fail and it was a trial boot, U-Boot will try the last version. + // + // That will likely be incompatible with HD and DG and require an install BUT + // it is likely a better more stable state to try to do an install from. + QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); + _wasTrialBoot = env.value("upgrade_available") == "1"; + + // Initial scan for updates. + scanForUpdates(); + + // Setup periodic scanning. + connect(&_timer, &QTimer::timeout, this, &VSWUpdate::handle_update); + + connect(&_ApplicationController, &ApplicationController::didSettingsDone, + this , &VSWUpdate::onSettingsDone); + connect(&_ApplicationController, &ApplicationController::didPOSTDone, + this , &VSWUpdate::onPostDone); + _timer.start(500); +} + +/*! + * \brief Destructor. + */ +VSWUpdate::~VSWUpdate() { + _package.abort(); +} + +/*! + * \brief Setup the UI for production. + * + * \note + * Call just before showing the widget for production. + * + * factoryImageDir can be either the folder path containing the factory golden + * original image or be the file fullname/path. + * + * wifiImageDir is the directory of the wifi downloaded package images. + * + * \param directories Directories to scan + * \param factoryImageDir Directory containing the factory image. + * \param wifiImageDir Wifi downloaded image directory. + * \param oldUpdateDir Which directory is where old applied ones are stored. + * \param maxOldUpdates How many updates to keep in "oldUpdateDir". + * \param publicKey The public key string. + */ +void VSWUpdate::setup( + const QStringList directories, + const QString factoryImageDir, + const QString wifiImageDir, + const QString oldUpdateDir, + uint32 maxOldUpdates, + const QString publicKeyFile) +{ + QFile f(publicKeyFile); + if (!f.open(QFile::ReadOnly | QFile::Text)) { + return; + } + QTextStream in(&f); + _pubKey = in.readAll(); + + _updateDirs = directories; + _factoryImg = factoryImageDir.toStdString(); + _wifiDir = wifiImageDir.toStdString(); + _oldUpdates = oldUpdateDir.toStdString(); + _maxOld = maxOldUpdates; + + // We remove duplicates, add oldUpdateDir if not in directories. + // It's ok if oldUpdateDir is in that list already or not, or a subdirectory of that list. + if (!_updateDirs.contains(oldUpdateDir)) { + _updateDirs.append(oldUpdateDir); + } + if (!_updateDirs.contains(factoryImageDir)) { + _updateDirs.append(factoryImageDir); + } + if (!_updateDirs.contains(wifiImageDir)) { + _updateDirs.append(wifiImageDir); + } +} + +/*! + * \brief Turn timepoint into std::time_t. + * + * \param tp Timepoint. + * + * \return std::time_t. + */ +template +std::time_t to_time_t(TP tp) +{ + using namespace std::chrono; + auto sctp = time_point_cast(tp - TP::clock::now() + + system_clock::now()); + return system_clock::to_time_t(sctp); +} + +/*! + * \brief Get table data. + * + * \param index Table model index. + * \param role Kind of data (text, color). + * + * \return Depending on the role, either text or color. + */ +QVariant VSWUpdate::data(const QModelIndex& index, int role) const +{ + int row = index.row(); + int col = index.column(); + + if (!index.isValid() + || row < 0 || row >= (int)_available.size() + || col < 0 || col >= columnCount()) { + return QVariant(); + } + + if (role == Qt::DisplayRole) { + std::string txt = + (col == 0) ? _available[row].fileName : + (col == 2) ? _available[row].reserved : + (col == 3) ? _available[row].packageName : ""; + if (col == 1) { + std::stringstream stringstream; + std::time_t tt = to_time_t(_available[row].lastWritten); + std::tm* gmt = std::gmtime(&tt); + std::stringstream buffer; + stringstream << std::put_time(gmt, "%d %B %Y %H:%M"); + txt = stringstream.str(); + } + return QString::fromStdString(txt); + } + + // Cell color. + if (role == Qt::UserRole + 1) { + bool selected = _selected == row; + QColor color(0xBFC9CA); + if (_available.size() > (unsigned int)row) { + std::string rsvd = _available[row].reserved; + color = selected ? QColor(0xBFC9CA) : QColor(0x95A5A6); // Gray. + if (rsvd == "Factory") { + color = selected ? QColor(0xF8C471) : QColor(0xF39C12); // Gold + } + if (rsvd == "WiFi") { + color = selected ? QColor(0x7DCEA0) : QColor(0x27AE60); // Greenish. + } + if (rsvd == "USB") { + color = selected ? QColor(0x7FB3D5) : QColor(0x2980B9); // Blueish. + } + } + if (col == 4) { + // scroll color. + color = QColor(0x95A5A6); // Gray + } + return color; + } + if (row == Qt::UserRole + 2) { + return (col < 4) ? 1 : 0; + } + + + return QVariant(); +} + +/*! + * \brief The start/stop button was clicked. + */ +void VSWUpdate::startStopBtnClicked() { + + // Either we are updating or not. + if (_updating) { + _CanInterface.setSWUpdateMode(false); + _package.abort(); + set_progress("Aborted."); + startStopUpdate("Aborted update", false); + _ApplicationController.enableKeepAlive(true); + + } else { + int row_index = (int)_selected; + if (row_index >= 0) { + std::vector update; + std::string package_location = _available[row_index].fullPath; + + // Copy / cache it (if not already in there). + // + // The UI has already checked the package as ok before allowing the user to pick it. + bool ok = _package.copyUpdate(package_location, _oldUpdates, _maxOld); + if (!ok) + { + writeLog("Failed to cache: " + QString::fromStdString(package_location), true); + } + + // As part of starting, it re-checks and then leaves the file open during the rest of the process + // and thus not-modifiable as a stream as a security feature. + _ApplicationController.enableKeepAlive(false); + _CanInterface.setSWUpdateMode(true); + + // Tell it to enter bootloader, this is done using the application protocol but from here. + // + // If these did fail which is extremely unlikely it feels to the user like they didn't quite + // press the start button. + sendAppCommand(MSG_ID_HD_ENTER_BOOTLOADER_NOW, false); + QThread::currentThread()->msleep(10); + sendAppCommand(MSG_ID_HD_ENTER_BOOTLOADER_NOW, true); + QThread::currentThread()->msleep(10); + + ok = _package.start_hasKey(package_location, update); + if (ok) + { + // If we got an ok, we are no longer allowed to trust the DG or HD image + // that exists so we'll never use MSG_ID_HD_REBOOT_NOW + QString info = QString::fromStdString(_package.get_info()); + set_progress(info); + startStopUpdate("Started update" + info, true); + } else { + // This is the only point in an update that can fail and just go to the existing image. + // Because we didn't write MSG_ID_HD_SET_ENTER_BOOTLOADER this should reboot to the app. + // + // TODO: Don't have a good recovery for this ... + sendAppCommand(MSG_ID_HD_REBOOT_NOW, false); + QThread::currentThread()->msleep(10); + sendAppCommand(MSG_ID_HD_REBOOT_NOW, true); + QThread::currentThread()->msleep(10); + + set_progress("Failed to start " + QString::fromStdString(_available[row_index].fileName)); + writeLog("Failed to start " + QString::fromStdString(package_location), false); + writeLog(QString::fromStdString(_package.get_info()), true); + } + } + } + + // Falling through, success or failure ensure these are set correctly. + _ApplicationController.enableKeepAlive(!_updating); + _mpBackButton->setEnabled(!_updating); +} + +void VSWUpdate::rowClicked(int row) { + // When !_updating it's used to show details when table is clicked. + if (!_updating) { + + if ((row >= 0) && (row < (int)_available.size())) { + this->beginResetModel(); + _selected = row; + QString location = "Cached prior update, last applied "; + if (_available[row].reserved == "Factory") { + location = "Original factory image, created "; + } + if (_available[row].reserved == "WiFi") { + location = "Downloaded via WiFi, on "; + } + if (_available[row].reserved == "USB") { + location = "From USB drive, written "; + } + + std::time_t tt = to_time_t(_available[row].lastWritten); + std::tm* gmt = std::gmtime(&tt); + std::stringstream buffer; + buffer << std::put_time(gmt, "%A, %d %B %Y %H:%M"); + + location += QString::fromStdString(buffer.str()); + + setState( + QString::fromStdString(_available[row].fileName) + "\n" + + location + "\n" + + QString::fromStdString(_available[row].packageName) + "\n" + + QString::fromStdString(_available[row].packageVersionInfo)); + this->endResetModel(); + } else { + setState(""); + } + } +} + +/*! + * \brief Shown or hidden. + * + * \param visible It has become visible or hidden. + */ +void VSWUpdate::visibilityChanged(bool visible, QObject * qml) { + if (_mpBackButton == nullptr) { + _mpBackButton = qml->parent()->findChild("_backButton"); + } + _isVisible = visible; +} + +/*! + * \brief Did post pass? + * + * \param vPass It passed, else failed. + */ +void VSWUpdate::onPostDone(bool vPass) +{ + _postWasOk = vPass; + + if (!vPass) { + if (_wasTrialBoot) { + writeLog("Post failed after update, U-Boot will automatically revert.", false); + + // Revert by allowing the user to choose to power cycle or not. + // On next boot the U-Boot configuration will revert to the other image if this one + // is the speculative image. + // Basically do nothing, an option would be to sleep and call reboot(). + } + } +} + +/*! + * \brief Were settings fetched ok? + */ +void VSWUpdate::onSettingsDone() { + _settingsOk = true; // Only got here if it was ok. + if (_postWasOk) { + + if (_wasTrialBoot) { + // This is a first time boot attempt of this image + // make it permanent else it will auto-revert on next reboot. + + // Both POST and fetch of settings worked, make this the boot image if it isn't already. + QProcess* pd = new QProcess(); + pd->start(QString::fromStdString("setenv upgrade_available 0; saveenv"), QStringList()); + pd->waitForFinished(); + + // If we got here, we are likely failing at rebooting for some reason and should log it. + // User can always manually reboot. + writeLog("Reboot to new image successful, new image is now the always boot to image.", false); + } + } + + // If it wasn't ok or we never get here, the next power cycle + // will auto revert if this is a speculative try once upgrade boot. +} + +/*! + * \brief Send an APP layer style message to the FW. + * + * cmd: 0x808F Enter bootloader (MSG_ID_HD_ENTER_BOOTLOADER_NOW) + * 0x8090 Reboot now (MSG_ID_HD_REBOOT_NOW) + * 0x8091 Set boot bit to stay in bootloader after reboot (MSG_ID_HD_SET_ENTER_BOOTLOADER) + * + * \param cmd 16 bit command. + * \param isHD It is for HD not DG. + */ +void VSWUpdate::sendAppCommand(uint16 cmd, bool isHD) { + uint8 msg[8]; + + // Zero it. + memset(msg, 0, 8); + + // Sync byte. + msg[0] = 0xA5; + + // 2 bytes sequence #. + uint16_t seqNo = _MessageDispatcher.getSequenceNumber(); + + msg[1] = (uint8)(seqNo >> 8); + msg[2] = (uint8)(seqNo & 0xFF); + + // 2 byte msgID. + msg[2] = (uint8)((cmd >> 8) & 0xFF); + msg[3] = (uint8)(cmd & 0xFF); + + // 1 byte payload length, msg[4] = 0, already done. + + // Send it. + // eChlid_UI_HD = 0x100, ///< UI => HD [Out] + // eChlid_UI_DG = 0x110, ///< UI => DG [Out] + _CanInterface.sendSWUpdateMsg(isHD ? eChlid_UI_HD : eChlid_UI_DG, msg, 8); +} + +/*! + * \brief Log a message. + * + * \param msg What to log, human readable text. + * \param isDebug Is a debug message. + * + * \note + * Events are used so that the Update code does not tie + * to a current logger or prohibit a future logger inplimentation. + * + * \details + * For update, there are really only two levels of logging needed: + *
    + *
  • The big "event" like things, of starting, + * completing, aborting, etc.
  • + *
  • The details that are useful for debug, that really + * are only worth keeping if a problem was detected.
  • + *
      + * + * The raw data isn't logged as it's generated at times + * because the context (effect) is seen a bit after the event. + * + * To make the logs useful the messages are created with the + * extra context information needed to make sense of them. + */ +void VSWUpdate::writeLog(const QString& msg, bool isDebug) { + + if (isDebug) { + LOG_DEBUG(msg); + } else { + LOG_APPED_UI(msg); + } +} + +// Virtual to make testing easier. +void VSWUpdate::reboot() { + // TODO: cause reboot, note this function is virtual. + + // For now just log. + writeLog("We are rebooting!", false); +#if (REBOOT_ENABLED == 1) + QProcess* pd = new QProcess(); + pd->start(QString::fromStdString("systemctl --message=\"Software upgrade\" reboot; sleep 1s"), QStringList()); + + // We want the process we spawned to finish causing the reboot even if the CPU was busy. + // Sleep 1 second (just in case) then spin wait for finished. There is a sleep of 1s in the command line. + QThread::currentThread()->msleep(1000); + pd->waitForFinished(); + + // If we got here, we are likely failing at rebooting for some reason and should log it. + // User can always manually reboot. + writeLog("We failed to reboot???", false); + setState(_stateMsg + "\nFailed to update, please power cycle!"); +#endif +} + +// State. +void VSWUpdate::startStopUpdate(const QString& msg, bool asStart) { + writeLog(msg, false); // Not debug message emit. + _updating = asStart; + _progressVisible |= asStart; // Once shown keep. + setButtonText(asStart ? "Abort" : "Start"); + _CanInterface.setSWUpdateMode(asStart); +} + +// Utility. +void VSWUpdate::set_progress(const QString& txt) { + _persistent = txt; + setState(txt + "\n"); + setPercent(0); +} + +void VSWUpdate::update_progress(const QString& txt, float percent) { + setState(_persistent + txt + "\n"); + setPercent(percent); +} + +void VSWUpdate::clear_progress() { + _persistent = ""; + setState(""); + setPercent(0); +} + +// Periodic update poll. +void VSWUpdate::handle_update() { + + if (!_isVisible) { + // Widget is dormant, not even being shown so + // do no background scanning. + } + + // While not performing an update, scan and return. + if (!_updating) { + // Not doing an update, allow CAN setting to happen. + scanForUpdates(); + return; + } + + std::vector stats = _package.progress(); + + float percent = 0; + float sum = 0; + QString update = ""; + bool nextUpdate = false; + QStringList logMsgs; + QStringList dbgMsgs; + bool cyberStop = false; + QString cyberConcerns = ""; + for (std::size_t ii = 0; ii < stats.size(); ii++) { + SwUpdate::UiUpdateStatus & s = stats[ii]; + if (s.stepName == "Cyber attack") + { + if (cyberConcerns.size() != 0) + { + cyberConcerns += ", "; + } + cyberConcerns += QString::fromStdString(s.targetName); + + logMsgs.append(QString::fromStdString("Possible cyber attack on " + s.targetName + ", haulting update.")); + cyberStop = true; + } + if (s.inProgress) { + update += + QString::fromStdString(s.targetName + ": " + + s.stepName + " : " + std::to_string(s.stepIndex) + " of " + std::to_string(s.totalSteps) + "\n"); + + percent += stats[ii].percentTotal; + sum += 1; + nextUpdate = true; + + } else { + update += QString::fromStdString(s.targetName + ": " + s.stepName + "\n"); + if (s.stepName == "Completed") { + percent += 100.0; + sum += 1; + logMsgs.append("Completed update of " + QString::fromStdString(s.targetName)); + } + } + } + + + // Update state, turns update off if done. + _updating = nextUpdate; + + // Handle state things. + if (!_updating) { + QString info = QString::fromStdString(_package.get_info()); + + // If there is a script to run do it. + std::string script = _package.getScriptIfAny(); + if (script.size() > 0) { + // Run it. + QProcess* pd = new QProcess(); + pd->start(QString::fromStdString("." + script), QStringList()); + pd->waitForFinished(); + } + // Signal done. + startStopUpdate("Update completed " + info, false); + } + + // Log all messages now. + for (const QString& m : logMsgs) { + writeLog(m, false); + } + + update_progress(update, sum <= 0 ? 0 : percent / sum); + + if (cyberStop) + { + _package.abort(); + startStopUpdate("Aborted update, cyber security problem", false); + set_progress("Aborted, cyber security problem."); + + emit securityCompromised("Security concerns while updating: " + cyberConcerns); + } + + if ((!cyberStop) && (!_updating)) { + bool allCompleted = true; + bool hadUi = false; + for (SwUpdate::UiUpdateStatus s : stats) { + hadUi |= s.targetName == "Files"; + allCompleted &= (s.stepName == "Completed"); + } + if (allCompleted && hadUi) { + emit aboutToReboot(); + this->reboot(); + } else { + std::string msg = std::string("No reboot."); + if (!allCompleted) { + msg = msg + std::string(" Not all transfers completed ok."); + } + if (!hadUi) { + msg = msg + std::string(" No UI files transfered, no need."); + } + + writeLog(QString::fromStdString(msg), false); + } + } + + // Ensure these match up with _updating state. + _ApplicationController.enableKeepAlive(!_updating); + _mpBackButton->setEnabled(!_updating); +} + +// Utility functions. +void VSWUpdate::scanForUpdates() { + + // Get drive access if not already. + checkMountUsb(); + + // Get the latest key, if it hasn't changed this is fast. + _package.set_public(_pubKey.toStdString()); + + // Look in the folder: updateDir + std::vector dirs; + for (int ii = 0; ii < _updateDirs.size(); ii++) { + QString d = _updateDirs[ii]; + dirs.push_back(d.toStdString()); + } + + this->beginResetModel(); + int row_selected = _selected; // Local copy. + + // Scan all the directories. + std::vector oldAvailable = _available; + _available = _package.get_available(dirs); + ui_sort(_available); + + if (oldAvailable != _available) { + + // Replace. + int newRowSelected = -1; + for (std::size_t ii = 0; ii < _available.size(); ii++) + { + // This is the new row that was previously selected, it + // could be a different index but it's the right data. + if ((row_selected >= 0) && (_available[ii] == oldAvailable[row_selected])) + { + newRowSelected = (int)ii; + } + } + + // Redo selection so things just magically add. + if (newRowSelected >= 0) + { + _selected = newRowSelected; + } + } + this->endResetModel(); +} + +// Sort packages found for UI table. +void VSWUpdate::ui_sort(std::vector& pkgs) { + + std::size_t index = 0; + // Find all factory images. + if (_factoryImg.size() > 0) { + for (std::size_t ii = 0; ii < pkgs.size(); ii++) { + if ((pkgs[ii].fullPath.find(_factoryImg) != std::string::npos) || + (pkgs[ii].fullPath == _factoryImg)) { + + // Swap. + if (index != ii) { + PackageInfo tmp = pkgs[index]; + pkgs[index] = pkgs[ii]; + pkgs[ii] = tmp; + } + pkgs[index].reserved = "Factory"; + index++; + } + } + } + + // Find the wifi. + if (_wifiDir.size() > 0) { + for (std::size_t ii = index; ii < pkgs.size(); ii++) { + if (pkgs[ii].fullPath.find(_wifiDir) != std::string::npos) { + // Swap. + if (index != ii) { + PackageInfo tmp = pkgs[index]; + pkgs[index] = pkgs[ii]; + pkgs[ii] = tmp; + } + pkgs[index].reserved = "WiFi"; + index++; + } + } + } + + // USB / old. + if (_oldUpdates.size() > 0) { + for (std::size_t ii = index; ii < pkgs.size(); ii++) { + if (pkgs[ii].reserved.size() == 0) { + + if (pkgs[ii].fullPath.find(_oldUpdates) == std::string::npos) { + // Swap. + if (index != ii) { + PackageInfo tmp = pkgs[index]; + pkgs[index] = pkgs[ii]; + pkgs[ii] = tmp; + } + pkgs[index].reserved = "USB"; + index++; + } + } + } + } + + // Rest are Cache. + for (std::size_t ii = index; ii < pkgs.size(); ii++) { + if (pkgs[ii].reserved.size() == 0) { + pkgs[ii].reserved = "Cache"; + } + } +} + +// Control the USB, both enable and mounting. +bool VSWUpdate::controlUSB(bool enable) { + /* if (QSysInfo::productType().contains("windows")) { + // Development environment, do nothing, just return. + return true; + } + + // Disable case: + if (!enable) { + QProcess* pd = new QProcess(); + pd->start("echo 0 > /sys/bus/usb/devices/usb1/authorized", QStringList()); + pd->waitForFinished(); + return true; + } + + // Enable case: + QProcess* pe = new QProcess(); + pe->start("echo 1 > /sys/bus/usb/devices/usb1/authorized", QStringList()); + pe->waitForFinished(); + + // Let it kick in for sure. + QThread::msleep(250); + return true;*/ + (void)enable; + return true; +} + +void VSWUpdate::checkMountUsb() { + static bool mounted = false; + static QString oldDevice; + QString dev = "/dev/sd"; + QString device = ""; + for (char a = 'a'; a <= 'z'; a++) { + device = dev + a + '1'; + if (QFileInfo::exists(device) && (device != oldDevice) && (!mounted)) { +#if 0 + const char* _usbDrive = ""; + bool ok; + _usbDrive = vDevice.toLatin1().constData(); + ok = ::mount(_usbDrive, USB_Mount_Point, USB_File_System, MS_SYNCHRONOUS | MS_NOEXEC, "") == 0; + if (ok) { +#endif + QProcess* pm = new QProcess(); + pm->start("mount " + device + " /media/usb", QStringList()); + pm->waitForFinished(); + + oldDevice = device; + mounted = true; + return; + } + } + mounted = false; + oldDevice = ""; +} Index: sources/update/VSwUpdate.h =================================================================== diff -u --- sources/update/VSwUpdate.h (revision 0) +++ sources/update/VSwUpdate.h (revision 20b370a54d2737831b307a0de82aec9e06e2b772) @@ -0,0 +1,307 @@ +/*! + * + * Copyright (c) 2023 Diality Inc. - All Rights Reserved. + * + * \copyright + * THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN + * WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. + * + * \file VSwUpdate.h + * \author (last) Phil Braica + * \date (last) 23-Jan-2023 + * \author (original) Phil Braica + * \date (original) 23-Jan-2023 + * + */ + +#ifndef V_SW_UPDATE_H_ +#define V_SW_UPDATE_H_ + +#include +#include +#include +#include +#include +#include +#include +#include +#include "Package.h" + +/*! + * \brief The view model for software update. + */ +class VSWUpdate : public QAbstractTableModel +{ + Q_OBJECT + Q_ENUMS(Roles) + Q_PROPERTY(QString buttonText READ getButtonText WRITE setButtonText NOTIFY buttonTextChanged) + Q_PROPERTY(QString stateData READ getState WRITE setState NOTIFY stateChanged) + Q_PROPERTY(float percent READ getPercent WRITE setPercent NOTIFY percentChanged) + Q_PROPERTY(bool progressVisible READ getProgressVisible WRITE setProgressVisible NOTIFY progressVisibleChanged) + +public: + // Constructor. + explicit VSWUpdate(QObject* parent = nullptr); + + // Virtual destructor. + virtual ~VSWUpdate(); + + // Setup function. + void setup( + const QStringList directories, + const QString factoryImageDir, + const QString wifiImageDir, + const QString oldUpdateDir, + uint32 maxOldUpdates, + const QString publicKeyFile); + + /*! + * \brief Provide a list of available data roles. + * + * Used by QML to provide cell like data. + * + * \return Hash of int, names. + */ + QHash roleNames() const override { + return {{Qt::DisplayRole, "displayRole"}, + {Qt::UserRole + 1, "colorRole"}}; + } + + /*! + * \brief Number of rows (number of available updates). + * + * \param parent Model index. + * + * \return Number of rows. + */ + int rowCount(const QModelIndex& parent = QModelIndex()) const override { + if (parent.isValid()) { + return 0; + } + return (int)_available.size(); + } + + /*! + * \brief Return the number of columns. + * + * \param parent Model index. + * + * \return 4, there are 4 columns. + */ + int columnCount(const QModelIndex& parent = QModelIndex()) const override { + if (parent.isValid()) { + return 0; + } + return 5; + } + + /*! + * \brief Get table data. + * + * \param index Table model index. + * \param role Kind of data (text, color). + * + * \return Depending on the role, either text or color. + */ + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; + + /*! + * \brief Get the message text. + * + * \return Text string. + */ + QString getState() const { + return _stateMsg; + } + + /*! + * \brief Set the message text. + * + * Sets the value and fires the property event. + * + * \param value New value. + */ + void setState(const QString& value) { + + // Trim compare, then set as needed and fire event. + QString trim = value.trimmed(); + + trim = (_postWasOk ? QString("Post passed. ") : QString("Post failed! ")) + + (_settingsOk ? QString("Settings/version info retrieved. ") : QString("Settings/version failed! ")) + + (_wasTrialBoot ? QString("First reboot after install\n") : QString("Previous functional boot\n")) + trim; + + if (_stateMsg != trim) { + _stateMsg = trim; + emit stateChanged(); + } + } + + /*! + * \brief Get the button text property for the XML button. + * + * \return Text string. + */ + QString getButtonText() const { + return _buttonText; + } + + /*! + * \brief Set the button text. + * + * Sets the value and fires the property event. + * + * \param value New value. + */ + void setButtonText(const QString& value) { + if (_buttonText != value) { + _buttonText = value; + emit buttonTextChanged(); + } + } + + /*! + * \brief Get the percent complete value. + * + * \return Value 0 to 1.. + */ + float getPercent() const { + return _percent; + } + + /*! + * \brief Set the percent complete value. + * + * Sets the value and fires the property event. + * + * \param value New value. + */ + void setPercent(const float& value) { + if (_percent != value) { + _percent = value; + emit percentChanged(); + } + } + + /*! + * \brief Get the bool indicating the progress bar should be visible. + * + * \return True if the progress bar should be visible. + */ + bool getProgressVisible() { + return _progressVisible; + } + + /*! + * \brief Set whether the progress bar should be visible or not. + * + * \param value New value. + */ + void setProgressVisible(bool value) { + if (_progressVisible != value) { + _progressVisible = value; + emit progressVisibleChanged(); + } + } + +signals: + + /*! Signal that data changed. */ + void stateChanged(); + + /*! Signal the button text changed. */ + void buttonTextChanged(); + + /*! Signal the percentage value changed. */ + void percentChanged(); + + /*! Signal that progress changed. */ + void progressVisibleChanged(); + + /*! + * \brief Fired when we are about to try to reboot the Linux box. + * + * Allows the UI to attempt any custom logging. After this callback + * signal completes, the reboot will get called. + */ + void aboutToReboot(); + + /*! + * \brief Indicates a likely cyber attack occured. + * + * The msg contains info already logged, in case the main + * UI also needs that message for some reason. + * + * \param msg What has already been logged. + */ + void securityCompromised(const QString& msg); + +public slots: + // Start / stop button clicked. + void startStopBtnClicked(); + + // Row clicked. + void rowClicked(int row); + + // Shown / hidden. + void visibilityChanged(bool visible, QObject * qml); + + // Things to determine if we booted ok. + void onPostDone(bool vPass); + void onSettingsDone(); + +protected: + + // Write an app styled can msg to control bootloader behavior. + void sendAppCommand(uint16 cmd, bool isHD); + + // Log a message. + void writeLog(const QString& msg, bool isDebug); + + // Virtual to make testing easier. + virtual void reboot(); + + // State. + void startStopUpdate(const QString& msg, bool asStart); + + // Utility. + void set_progress(const QString& txt); + void update_progress(const QString& txt, float percent = 0); + void clear_progress(); + + // Periodic update poll. + void handle_update(); + + // Utility functions. + void scanForUpdates(); + + // Sort packages found for UI table. + void ui_sort(std::vector& pkgs); + + // Control the USB, both enable and mounting. + bool controlUSB(bool enable); + void checkMountUsb(); + + std::vector _available; ///< List of available things. + QString _pubKey; ///< Public key. + QStringList _updateDirs; ///< Update available directories. + QString _persistent; ///< Persistent update text. + std::string _oldUpdates; ///< Old updates directory. + std::string _factoryImg; ///< Factory image or factory directory. + std::string _wifiDir; ///< Wifi download dir. + uint32 _maxOld; ///< Max old updates. + bool _updating; ///< Updating flag. + float _percent; ///< Percent complete, [0-1]. + QTimer _timer; ///< Timer for update. + sint32 _selected; ///< Selected row, -1 is none. + Package _package; ///< Our update object (UI indepenedent C++). + QString _stateMsg; ///< State text. + QString _buttonText; ///< Button text. + QQuickItem* _mpBackButton; ///< Back button. + bool _isVisible; ///< Widget is visible or not. + bool _progressVisible; ///< Is the progress bar visible. + bool _postWasOk; ///< Post was ok flag. + bool _settingsOk; ///< Version and settings are ok. + bool _wasTrialBoot; ///< This was a trial boot or normal. +}; + +#endif // V_SW_UPDATE_H_ Index: sources/view/VView.h =================================================================== diff -u -r6d76149dca70e879f33bf6ed44203d0d06bc523a -r20b370a54d2737831b307a0de82aec9e06e2b772 --- sources/view/VView.h (.../VView.h) (revision 6d76149dca70e879f33bf6ed44203d0d06bc523a) +++ sources/view/VView.h (.../VView.h) (revision 20b370a54d2737831b307a0de82aec9e06e2b772) @@ -195,5 +195,7 @@ REGISTER_TYPE( VPostTreatmentAdjustmentTreatmentLog ) \ /* Disinfection */ \ REGISTER_TYPE( VDisinfectAdjustDisinfect ) \ + /* Update */ \ + REGISTER_TYPE( VSWUpdate) //--------------------------------------------------------------------------------//