From fa2d78c6088be96e755d97b1a72343753abec8d1 Mon Sep 17 00:00:00 2001 From: Rainer Keller Date: Thu, 4 Sep 2014 10:38:02 +0200 Subject: Add tools related for update b2qt-update-application is for doing the actual update. b2qt-update-util is for initiating and finishing the update. Change-Id: Ide1c1ba307d9346bacf83c507816feecefe87a16 Reviewed-by: Eirik Aavitsland --- src/b2qt-update-application/.gitignore | 4 + .../b2qt-update-application.pro | 14 + src/b2qt-update-application/filewrapper.cpp | 53 ++++ src/b2qt-update-application/filewrapper.h | 40 +++ src/b2qt-update-application/main.cpp | 203 ++++++++++++++ src/b2qt-update-application/tar.cpp | 309 +++++++++++++++++++++ src/b2qt-update-application/tar.h | 99 +++++++ src/b2qt-update-application/update.cpp | 202 ++++++++++++++ src/b2qt-update-application/update.h | 50 ++++ src/b2qt-update-util/.gitignore | 4 + src/b2qt-update-util/b2qt-update-util.pro | 2 + src/b2qt-update-util/main.cpp | 138 +++++++++ src/src.pro | 1 + 13 files changed, 1119 insertions(+) create mode 100644 src/b2qt-update-application/.gitignore create mode 100644 src/b2qt-update-application/b2qt-update-application.pro create mode 100644 src/b2qt-update-application/filewrapper.cpp create mode 100644 src/b2qt-update-application/filewrapper.h create mode 100644 src/b2qt-update-application/main.cpp create mode 100644 src/b2qt-update-application/tar.cpp create mode 100644 src/b2qt-update-application/tar.h create mode 100644 src/b2qt-update-application/update.cpp create mode 100644 src/b2qt-update-application/update.h create mode 100644 src/b2qt-update-util/.gitignore create mode 100644 src/b2qt-update-util/b2qt-update-util.pro create mode 100644 src/b2qt-update-util/main.cpp (limited to 'src') diff --git a/src/b2qt-update-application/.gitignore b/src/b2qt-update-application/.gitignore new file mode 100644 index 0000000..d2a78c0 --- /dev/null +++ b/src/b2qt-update-application/.gitignore @@ -0,0 +1,4 @@ +Makefile +b2qt-update-application +moc_*.cpp +*.o diff --git a/src/b2qt-update-application/b2qt-update-application.pro b/src/b2qt-update-application/b2qt-update-application.pro new file mode 100644 index 0000000..5fc557b --- /dev/null +++ b/src/b2qt-update-application/b2qt-update-application.pro @@ -0,0 +1,14 @@ +CONFIG += c++11 +QT = core network +SOURCES += \ + main.cpp \ + tar.cpp \ + filewrapper.cpp \ + update.cpp + +HEADERS += \ + tar.h \ + filewrapper.h \ + update.h + +LIBS += -lcrypto diff --git a/src/b2qt-update-application/filewrapper.cpp b/src/b2qt-update-application/filewrapper.cpp new file mode 100644 index 0000000..6b4a759 --- /dev/null +++ b/src/b2qt-update-application/filewrapper.cpp @@ -0,0 +1,53 @@ +/**************************************************************************** +** +** Copyright (C) 2014 Digia Plc +** All rights reserved. +** For any questions to Digia, please use the contact form at +** http://qt.digia.com/ +** +** This file is part of Qt Enterprise Embedded. +** +** Licensees holding valid Qt Enterprise licenses may use this file in +** accordance with the Qt Enterprise License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** the contact form at http://qt.digia.com/ +** +****************************************************************************/ + +#include "filewrapper.h" +#include +#include + +FileWrapper::FileWrapper(const QString &fileName, QObject *parent) + : QFile(fileName, parent) + , mTimer(new QTimer(this)) +{ + mTimer->setInterval(50); + connect(mTimer, &QTimer::timeout, this, &FileWrapper::emitReadyRead); +} + +FileWrapper::~FileWrapper() +{ +} + +bool FileWrapper::open(OpenMode mode) +{ + bool rc = QFile::open(mode); + if (rc) { + mTimer->start(); + } + return rc; +} + +void FileWrapper::emitReadyRead() +{ + if (!atEnd()) { + emit readyRead(); + } else { + mTimer->stop(); + close(); + } +} diff --git a/src/b2qt-update-application/filewrapper.h b/src/b2qt-update-application/filewrapper.h new file mode 100644 index 0000000..fc8a977 --- /dev/null +++ b/src/b2qt-update-application/filewrapper.h @@ -0,0 +1,40 @@ +/**************************************************************************** +** +** Copyright (C) 2014 Digia Plc +** All rights reserved. +** For any questions to Digia, please use the contact form at +** http://qt.digia.com/ +** +** This file is part of Qt Enterprise Embedded. +** +** Licensees holding valid Qt Enterprise licenses may use this file in +** accordance with the Qt Enterprise License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** the contact form at http://qt.digia.com/ +** +****************************************************************************/ + +#ifndef FILEWRAPPER_H +#define FILEWRAPPER_H + +#include +class QTimer; + +class FileWrapper : public QFile +{ + Q_OBJECT + +public: + FileWrapper(const QString &fileName, QObject *parent = 0); + virtual ~FileWrapper(); + bool open(OpenMode mode); + +private: + void emitReadyRead(); + QTimer *mTimer; +}; + +#endif // FILEWRAPPER_H diff --git a/src/b2qt-update-application/main.cpp b/src/b2qt-update-application/main.cpp new file mode 100644 index 0000000..157af4f --- /dev/null +++ b/src/b2qt-update-application/main.cpp @@ -0,0 +1,203 @@ +/**************************************************************************** +** +** Copyright (C) 2014 Digia Plc +** All rights reserved. +** For any questions to Digia, please use the contact form at +** http://qt.digia.com/ +** +** This file is part of Qt Enterprise Embedded. +** +** Licensees holding valid Qt Enterprise licenses may use this file in +** accordance with the Qt Enterprise License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** the contact form at http://qt.digia.com/ +** +****************************************************************************/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "update.h" +#include "filewrapper.h" + +#define UPDATE_MOUNTPOINT "/mnt/update" + +QStringList mounts; + +void cleanup() +{ + foreach (const QString &m, mounts) { + QProcess::execute("umount", QStringList() << m); + } + mounts.clear(); +} + +void error(const QString &message) +{ + fprintf(stderr, "%s\n", message.toLocal8Bit().constData()); + cleanup(); + exit(1); +} + +bool execute(const QString &binary, const QStringList &arguments) +{ + int rc = QProcess::execute(binary, arguments); + if (rc != 0) + error("Failed to execute command '" + binary + " " + arguments.join(" ")); + return rc == 0; +} + +bool mount(const QString &device, const QString &mountpoint, const QString &arguments = QString()) +{ + QStringList tmp; + if (!arguments.isEmpty()) + tmp << "-o" << arguments; + + if (!execute("mount", QStringList() << tmp << device << mountpoint)) + return false; + + if (!arguments.contains("remount")) + mounts << mountpoint; + return true; +} + +QByteArray readAll(const QString &fileName) +{ + QFile f(fileName); + if (!f.open(QFile::ReadOnly)) { + qWarning() << "Could not read" << fileName; + return QByteArray(); + } + + return f.readAll(); +} + +bool writeAll(const QString &fileName, const QByteArray &content) +{ + QFile f(fileName); + if (!f.open(QFile::WriteOnly)) { + qWarning() << "Could not write" << fileName; + return false; + } + if (f.write(content) != content.size()) { + qWarning() << "write size mismatch"; + return false; + } + f.close(); + return true; +} + +QStringList find_usb_storage() +{ + QStringList rc; + QDir d("/sys/dev/block"); + + foreach (QString s, d.entryList()) { + QFileInfo fi(d.absoluteFilePath(s)); + QString path = fi.canonicalFilePath(); + if (path.contains("/usb")) + rc += path.mid(path.lastIndexOf('/')); + } + return rc; +} + +int main(int argc, char **argv) +{ + QCoreApplication app(argc, argv); + + mount(QString(), "/", "remount,rw"); + execute("mkdir", QStringList() << "-p" << "/mnt/boot"); + execute("mkdir", QStringList() << "-p" << "/mnt/root"); + execute("mkdir", QStringList() << "-p" << UPDATE_MOUNTPOINT); + mount("/dev/mmcblk0p1", "/mnt/boot", "ro"); + + QByteArray update_state = readAll("/mnt/boot/update/state").trimmed(); + if (update_state.isEmpty()) + qFatal("Update state is empty"); + + if (update_state == "v") + qFatal("Update state is 'valid', this should not happen"); + else if (update_state == "t") + qDebug() << "Update state is 'testing', this should not happen"; + else if (update_state == "u") + qDebug() << "Update state is 'update'"; + else + qDebug() << "Unknown update state:" << update_state; + + QStringList usbsticks = find_usb_storage(); + + qDebug() << "Found USB storage devices:" << usbsticks; + + bool update_found = false; + foreach (QString s, usbsticks) { + if (QProcess::execute("mount", QStringList() << "-o" << "ro" << "/dev/" + s << UPDATE_MOUNTPOINT) == 0) { + mounts << UPDATE_MOUNTPOINT; + qDebug() << "mount successful"; + + if (QFile::exists(UPDATE_MOUNTPOINT "/b2qt-update")) { + qDebug() << "update found"; + update_found = true; + break; + } + execute("umount", QStringList() << UPDATE_MOUNTPOINT); + mounts.removeAt(mounts.lastIndexOf(UPDATE_MOUNTPOINT)); + qDebug() << "no update found"; + } else { + qDebug() << "mount of" << s << "failed"; + } + } + + Update update; + + if (update_found) { + FileWrapper *fw = new FileWrapper(UPDATE_MOUNTPOINT "/b2qt-update", &update); + if (!fw->open(QFile::ReadOnly)) + qFatal("Failed to open update"); + qDebug() << "Starting update from USB"; + update.setDevice(fw); + } else { + QByteArray update_source = readAll("/mnt/boot/update/source").trimmed(); + if (update_source.isEmpty()) { + execute("umount", QStringList() << "/mnt/boot"); // FIXME + mounts.removeAt(mounts.lastIndexOf("/mnt/boot")); + qFatal("Update source is empty"); + } + + execute("udhcpc", QStringList() << "-i" << "eth0"); + + QNetworkAccessManager *manager = new QNetworkAccessManager(0); + QNetworkReply *reply = manager->get(QNetworkRequest(QUrl(update_source))); + reply->setReadBufferSize(2000000); // 2 MB + + QObject::connect(reply, (void (QNetworkReply::*)(QNetworkReply::NetworkError))&QNetworkReply::error, + [](QNetworkReply::NetworkError e){qDebug() << "network error" << e;}); + QObject::connect(reply, &QNetworkReply::sslErrors, + [](QList){ qDebug() << "ssl errors";}); + + qDebug() << "Starting update from Internet" << update_source; + update.setDevice(reply); + } + + app.exec(); + + qDebug() << "unmount"; + cleanup(); + + qDebug() << "sync"; + sync(); + qDebug() << "reboot; waiting 2 seconds"; + sleep(2); + reboot(RB_AUTOBOOT); + return 0; +} diff --git a/src/b2qt-update-application/tar.cpp b/src/b2qt-update-application/tar.cpp new file mode 100644 index 0000000..c0369ff --- /dev/null +++ b/src/b2qt-update-application/tar.cpp @@ -0,0 +1,309 @@ +/**************************************************************************** +** +** Copyright (C) 2014 Digia Plc +** All rights reserved. +** For any questions to Digia, please use the contact form at +** http://qt.digia.com/ +** +** This file is part of Qt Enterprise Embedded. +** +** Licensees holding valid Qt Enterprise licenses may use this file in +** accordance with the Qt Enterprise License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** the contact form at http://qt.digia.com/ +** +****************************************************************************/ + +#include "tar.h" +#include +#include +#include +#include + +static bool isZeroed(const char *ptr, unsigned int size) +{ + while (size) { + if (*ptr) { + return false; + } + ++ptr; + --size; + } + return true; +} + +Tar::Tar(QIODevice *source) + : QObject(source) + , mSource(source) + , mSize(0) + , mRemainingSize(0) + , mRemainingFileBytes(0) + , mProcess(0) + , mState(WaitForHeader) + , x509(0) + , mdctx(0) +{ + // Initialize table with all digests in order to look them up by string later + OpenSSL_add_all_digests(); + + connect(source, &QIODevice::readyRead, this, &Tar::dataIncoming); + connect(source, &QIODevice::aboutToClose, this, &Tar::aboutToClose); +} + +Tar::~Tar() +{ + if (x509) + X509_free(x509); + x509 = 0; + if (mdctx) + EVP_MD_CTX_destroy(mdctx); + mdctx = 0; +} + +const QByteArray &Tar::currentContent() const +{ + return mContent; +} + +unsigned long Tar::currentSize() const +{ + return mSize; +} + +QString Tar::currentFileName() const +{ + if (mHeader.name[sizeof(mHeader.name)-1]) + return QString::fromLatin1(mHeader.name, sizeof(mHeader.name)); + else + return QString::fromLatin1(mHeader.name); +} + +void Tar::dataIncoming() +{ + while (mSource->bytesAvailable() > 512) { + + if (mState == WaitForHeader) { + if (mSource->bytesAvailable() < 512) + return; + if (mSource->read((char*)&mHeader, sizeof(mHeader)) != sizeof(mHeader)) + qFatal("Tar read error"); + if (isZeroed((char*)&mHeader, sizeof(mHeader))) { + qDebug() << "Zero Tar block"; + qDebug() << "Bytes left" << mSource->bytesAvailable(); + mSource->close(); + return; + } + if (mHeader.name[0] == 0) + qFatal("Header starts with 0"); + + mSize = QString::fromLatin1(mHeader.size, sizeof(mHeader.size)-1).toULongLong(0, 8); + mRemainingFileBytes = mSize; + mRemainingSize = mSize; + if (mSize % 512) // padding to 512 byte block + mRemainingSize += 512 - (mSize % 512); + + if (mSize == 0) { + qFatal("Size of file is 0"); + } + + mState = WaitForDecision; + qDebug() << "Started file" << currentFileName() << mSize; + if (x509) + setupMDContext(); + emit startingFile(currentFileName()); + } else if (mState == WaitForDecision) { + return; + } else if (mState == Extract) { + QByteArray ba; + + quint64 bytesToHandle = mRemainingSize; + + if (mSource->bytesAvailable() < mRemainingSize) + bytesToHandle = mSource->bytesAvailable(); + if (bytesToHandle > 60000) + bytesToHandle = 60000; + ba = mSource->read(bytesToHandle); + + updateMDContext(ba); + mProcess->write(ba); + mProcess->waitForBytesWritten(); + mRemainingSize -= ba.size(); + // do not return + } else if (mState == Receive) { + QByteArray ba; + + if (mSource->bytesAvailable() > mRemainingSize) + ba = mSource->read(mRemainingSize); + else + ba = mSource->readAll(); + updateMDContext(ba); + mContent += ba; + mRemainingSize -= ba.size(); + if (mContent.size() > mSize) + mContent.resize(mSize); + // do not return + } else if (mState == Skip) { + QByteArray ba; + + if (mSource->bytesAvailable() > mRemainingSize) { + ba = mSource->read(mRemainingSize); + } else { + ba = mSource->readAll(); + } + updateMDContext(ba); + mRemainingSize -= ba.size(); + // drop data + // do not return + } else if (mState == WaitForContinue) { + return; + } else { + qFatal("Unknown tar state"); + } + + if (mState != WaitForContinue && mState != WaitForHeader && mRemainingSize == 0) { + if (mState == Extract) { + qDebug() << "End tar process"; + // please tar + // it needs 2 zeroed blocks + char buffer[sizeof(mHeader)]; + memset(buffer, 0, sizeof(buffer)); + mProcess->write(buffer, sizeof(buffer)); + mProcess->write(buffer, sizeof(buffer)); + mProcess->waitForBytesWritten(); + mProcess->closeWriteChannel(); + mProcess->waitForFinished(-1); + mProcess->deleteLater(); + mProcess = 0; + } + mState = WaitForContinue; + + emit(endingFile(currentFileName())); + } + + } +} + +void Tar::extractContent(const QString &targetDir) +{ + if (mState == WaitForDecision) { + qDebug() << Q_FUNC_INFO; + mState = Extract; + delete mProcess; + mProcess = new QProcess(this); + mProcess->setProcessChannelMode(QProcess::ForwardedChannels); + mProcess->start("tar", QStringList() << "xvf" << "-" << "-C" << targetDir); + + // if we want to extract a file directly, the header needs to be passed to tar + if (!currentFileName().endsWith(".tar")) + mProcess->write((const char*)&mHeader, sizeof(mHeader)); + } +} + +void Tar::receiveContent() +{ + if (mState == WaitForDecision) + mState = Receive; +} + +void Tar::skipContent() +{ + if (mState == WaitForDecision) + mState = Skip; +} + +void Tar::continueContent() +{ + if (mState == WaitForContinue) { + mState = WaitForHeader; + mSize = 0; + mContent.clear(); + delete mProcess; + mProcess = 0; + } else + qDebug() << "Called" << Q_FUNC_INFO << "in wrong state"; +} + +void Tar::aboutToClose() +{ + if (mState != WaitForHeader) { + qWarning() << "unexpected close"; + } + emit finished(); +} + +bool Tar::setVerificationData(const QString &certificateFileName, const QString &mdType) +{ + if (x509) + qFatal("Setting key twice"); + + // Load key from file + BIO *i = BIO_new(BIO_s_file()); + BIO *o = BIO_new_fp(stdout,BIO_NOCLOSE); + + if ((BIO_read_filename(i, certificateFileName.toLatin1().constData()) <= 0) || + ((x509 = PEM_read_bio_X509_AUX(i, NULL, NULL, NULL)) == NULL)) { + qWarning() << "Invalid certificate, failed to read file" << certificateFileName; + return false; + } + // Showing used certificate + X509_print_ex(o, x509, XN_FLAG_COMPAT, X509_FLAG_COMPAT); + BIO_free(i); + BIO_free(o); + qDebug() << "Using verification key:" << certificateFileName; + + md = EVP_get_digestbyname(mdType.toLatin1().constData()); + if (md == NULL) + qFatal("Digest not found"); + return true; +} + +void Tar::setupMDContext() +{ + if (mdctx) + EVP_MD_CTX_destroy(mdctx); + if (!(mdctx = EVP_MD_CTX_create())) + qFatal("EVP_MD_CTX_create failed"); + + EVP_PKEY *key = X509_get_pubkey(x509); + if (key == NULL) { + qFatal("X509_get_pubkey failed"); + } + + if (md == NULL) + qFatal("No digest set"); + + if (1 != EVP_DigestVerifyInit(mdctx, NULL, md, NULL, key)) + qFatal("EVP_DigestVerifyInit failed"); +} + +void Tar::updateMDContext(QByteArray data) +{ + if (mRemainingFileBytes == 0) + qFatal("Trying to update MD context when no more bytes are expected"); + + if (mRemainingFileBytes < data.size()) + data = data.left(mRemainingFileBytes); + mRemainingFileBytes -= data.size(); + + if (!mdctx) + return; + + if (1 != EVP_DigestVerifyUpdate(mdctx, data.constData(), data.size())) + qFatal("EVP_DigestVerifyUpdate failed"); +} + +bool Tar::checkSignature(const QByteArray &signature) +{ + return 1 == EVP_DigestVerifyFinal(mdctx, (unsigned char*)signature.constData(), signature.size()); +} + +bool Tar::verifyCurrentContent(const QByteArray &signature) +{ + setupMDContext(); + if (1 != EVP_DigestVerifyUpdate(mdctx, mContent.constData(), mContent.size())) + qFatal("EVP_DigestVerifyUpdate failed"); + return checkSignature(signature); +} diff --git a/src/b2qt-update-application/tar.h b/src/b2qt-update-application/tar.h new file mode 100644 index 0000000..e3ca1d2 --- /dev/null +++ b/src/b2qt-update-application/tar.h @@ -0,0 +1,99 @@ +/**************************************************************************** +** +** Copyright (C) 2014 Digia Plc +** All rights reserved. +** For any questions to Digia, please use the contact form at +** http://qt.digia.com/ +** +** This file is part of Qt Enterprise Embedded. +** +** Licensees holding valid Qt Enterprise licenses may use this file in +** accordance with the Qt Enterprise License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** the contact form at http://qt.digia.com/ +** +****************************************************************************/ + +#ifndef TAR_H +#define TAR_H + +#include +#include +class QIODevice; +class QProcess; +#define USTAR_HEADER_SIZE 512 + +struct ustar_header +{ +char name[100]; /* File name. Null-terminated if room. */ +char mode[8]; /* Permissions as octal string. */ +char uid[8]; /* User ID as octal string. */ +char gid[8]; /* Group ID as octal string. */ +char size[12]; /* File size in bytes as octal string. */ +char mtime[12]; /* Modification time in seconds from Jan 1, 1970, as octal string. */ +char chksum[8]; /* Sum of octets in header as octal string. */ +char typeflag; /* An enum ustar_type value. */ +char linkname[100]; /* Name of link target. Null-terminated if room. */ +char magic[6]; /* "ustar\0" */ +char version[2]; /* "00" */ +char uname[32]; /* User name, always null-terminated. */ +char gname[32]; /* Group name, always null-terminated. */ +char devmajor[8]; /* Device major number as octal string. */ +char devminor[8]; /* Device minor number as octal string. */ +char prefix[155]; /* Prefix to file name. Null-terminated if room. */ +char padding[12]; /* Pad to 512 bytes. */ +} __attribute__((packed)); + +class Tar : public QObject +{ + Q_OBJECT + +public: + Tar(QIODevice *source); + virtual ~Tar(); + + const QByteArray ¤tContent() const; + unsigned long currentSize() const; + QString currentFileName() const; + + enum State { WaitForHeader, WaitForDecision, Extract, Receive, Skip, WaitForContinue}; + bool setVerificationData(const QString &certificateFileName, const QString &md); + bool checkSignature(const QByteArray &signature); + bool verifyCurrentContent(const QByteArray &signature); + +signals: + void startingFile(const QString &name); + void endingFile(const QString &name); + void finished(); + +public slots: + void extractContent(const QString &targetDir); + void receiveContent(); + void skipContent(); + void dataIncoming(); + void continueContent(); + +private slots: + void aboutToClose(); + +private: + void setupMDContext(); + void updateMDContext(QByteArray); + + QIODevice *mSource; + qint64 mSize; + qint64 mRemainingSize; // File Size + Padding + qint64 mRemainingFileBytes; // File Size without Padding + ustar_header mHeader; + QByteArray mContent; + QProcess *mProcess; + State mState; + X509 *x509; + EVP_MD_CTX *mdctx; + const EVP_MD *md; +}; + +#endif // TAR_H diff --git a/src/b2qt-update-application/update.cpp b/src/b2qt-update-application/update.cpp new file mode 100644 index 0000000..79fea64 --- /dev/null +++ b/src/b2qt-update-application/update.cpp @@ -0,0 +1,202 @@ +/**************************************************************************** +** +** Copyright (C) 2014 Digia Plc +** All rights reserved. +** For any questions to Digia, please use the contact form at +** http://qt.digia.com/ +** +** This file is part of Qt Enterprise Embedded. +** +** Licensees holding valid Qt Enterprise licenses may use this file in +** accordance with the Qt Enterprise License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** the contact form at http://qt.digia.com/ +** +****************************************************************************/ + +#include "update.h" +#include "tar.h" +#include +#include +#include +#include + +extern QStringList mounts; +bool writeAll(const QString &fileName, const QByteArray &content); +static QStringList allowedFiles; +QByteArray readAll(const QString &fileName); +bool execute(const QString &binary, const QStringList &arguments); +bool mount(const QString &device, const QString &mountpoint, const QString &arguments = QString()); + +QMap parseMetaInfo(const QByteArray &data) +{ + QMap rc; + QList list = data.split('\n'); + foreach (const QByteArray &ba, list) { + if (ba.isEmpty()) + continue; + int i = ba.indexOf(':'); + if (i < 0 || i == ba.size()) + qFatal("Invalid meta.info content: No colon found or no data after colon"); + + QString key = ba.left(i); + QString value = ba.mid(i+1).trimmed(); + + if (rc.contains(key)) + qFatal("Invalid meta.info content: Same key given twice"); + rc.insert(key, value); + } + if (rc.isEmpty()) + qFatal("Invalid meta.info content: No keys given"); + return rc; +} + +Update::Update(QObject *parent) + : QObject(parent) + , mTar(0) + , mSource(0) +{ + allowedFiles << "key.info" << "meta.info" << "keys.tar" << "uImage" << "rootfs.tar"; +} + +Update::~Update() +{ +} + +//# openssl dgst -sha256 -sign priv.pem file.txt > file.txt.sig +//# openssl dgst -sha256 -verify public.pem -signature file.txt.sig file.txt + +void Update::setDevice(QIODevice *source) +{ + if (!source) { + qFatal("Source device is NULL"); + return; + } + + if (mSource) { + qFatal("Source already set"); + return; + } + + mSource = source; + mTar = new Tar(mSource); + + connect(mTar, &Tar::startingFile, this, &Update::tarStartingFile); + connect(mTar, &Tar::endingFile, this, &Update::tarEndingFile); + connect(mTar, &Tar::finished, this, &Update::tarFinished); +} + +void Update::tarStartingFile(const QString &name) +{ + if (name == "key.info") { + mTar->receiveContent(); + } else if (name.endsWith(".sig")) { + mTar->receiveContent(); + mCurrentFile = name; + mCurrentFile.chop(4); + + bool found = false; + while (!allowedFiles.isEmpty()) { + QString n = allowedFiles.takeFirst(); + if (n == mCurrentFile) { + found = true; + break; + } + } + + if (!found) + qFatal("File not allowed"); + + } else if (mCurrentFile != name) { + qFatal("Invalid file order"); + } else { + if (mCurrentFile == "meta.info") { + mTar->receiveContent(); + } else if (mCurrentFile == "uImage") { + mount(QString(), "/mnt/boot", "remount,rw"); + mTar->extractContent("/mnt/boot"); + } else if (mCurrentFile == "rootfs.tar") { + qDebug() << "Formatting rootfs"; + execute("mkfs." + mMetaInfo["filesystemType"], QStringList() << mMetaInfo["rootDevice"]); + mount(mMetaInfo["rootDevice"], "/mnt/root"); + mTar->extractContent("/mnt/root"); + } else if (mCurrentFile == "keys.tar") { + mount(QString(), "/mnt/boot", "remount,rw"); + execute("rm", QStringList() << "-r" << "/mnt/boot/update/keys/"); + execute("mkdir", QStringList() << "/mnt/boot/update/keys/"); + mTar->extractContent("/mnt/boot/update/keys"); + } else { + qFatal("Unexpected file in update data"); + } + } +} + +void Update::tarEndingFile(const QString &name) +{ + if (name == "meta.info") { + QMap map = parseMetaInfo(mTar->currentContent()); + + // Some rough sanity checks + if (!map.contains("version")) + qFatal("No version information found" ); + if (map["version"] != "1") + qFatal("Invalid update version"); + if (!map.contains("platform") || map["platform"].isEmpty()) + qFatal("Platform information missing"); + if (map["platform"] != readAll("/mnt/boot/update/platform").trimmed()) + qFatal("Invalid platform information"); + else + qDebug() << "Platform matches"; + if (!map.contains("rootDevice") || map["rootDevice"].isEmpty()) + qFatal("Root device information missing"); + if (!QFile::exists(map["rootDevice"])) + qFatal("Root device not found"); + if (!map.contains("key") || map["key"].isEmpty() || map["key"].contains('/')) + qFatal("Invalid or no key fingerprint"); + if (!map.contains("digest") || map["digest"].isEmpty()) + qFatal("Invalid digest type"); + if (!map.contains("filesystemType") || map["filesystemType"].isEmpty() || map["filesystemType"].contains(' ') || map["filesystemType"].contains('/')) + qFatal("Invalid filesystem type"); + + mMetaInfo = map; + + if (!mTar->setVerificationData("/mnt/boot/update/keys/" + mMetaInfo["key"], mMetaInfo["digest"])) + qFatal("Loading of certificate failed"); + + // Now verify the meta.info itself + if (mTar->verifyCurrentContent(mCurrentSignature)) { + qDebug() << "meta.info VERIFIED OK"; + } else { + qDebug() << "meta.info VERIFICATION FAILED"; + qFatal("VERIFICATION FAILED"); + return; + } + + } else if (name.endsWith(".sig")) { + mCurrentSignature = mTar->currentContent(); + qDebug() << "Received signature with" << mCurrentSignature.size() << "bytes."; + } else { + if (mTar->checkSignature(mCurrentSignature)) { + qDebug() << "VERIFIED OK"; + // FIXME: do something here + } else { + qDebug() << "VERIFICATION FAILED"; + qFatal("VERIFICATION FAILED"); + } + } + + mTar->continueContent(); +} + +void Update::tarFinished() +{ + qDebug() << "Update finished"; + mount(QString(), "/mnt/boot", "remount,rw"); + if (!writeAll("/mnt/boot/update/state", "t")) + qFatal("Could not set state to testing"); + mount(QString(), "/mnt/boot", "remount,ro"); + qApp->exit(); +} diff --git a/src/b2qt-update-application/update.h b/src/b2qt-update-application/update.h new file mode 100644 index 0000000..830be56 --- /dev/null +++ b/src/b2qt-update-application/update.h @@ -0,0 +1,50 @@ +/**************************************************************************** +** +** Copyright (C) 2014 Digia Plc +** All rights reserved. +** For any questions to Digia, please use the contact form at +** http://qt.digia.com/ +** +** This file is part of Qt Enterprise Embedded. +** +** Licensees holding valid Qt Enterprise licenses may use this file in +** accordance with the Qt Enterprise License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** the contact form at http://qt.digia.com/ +** +****************************************************************************/ + +#ifndef UPDATE_H +#define UPDATE_H + +#include +#include +class Tar; +class QIODevice; + +class Update : public QObject +{ + Q_OBJECT + +public: + Update(QObject *parent = 0); + virtual ~Update(); + void setDevice(QIODevice *source); + +private slots: + void tarStartingFile(const QString &name); + void tarEndingFile(const QString &name); + void tarFinished(); + +private: + Tar *mTar; + QIODevice *mSource; + QString mCurrentFile; + QByteArray mCurrentSignature; + QMap mMetaInfo; +}; + +#endif // UPDATE_H diff --git a/src/b2qt-update-util/.gitignore b/src/b2qt-update-util/.gitignore new file mode 100644 index 0000000..67f0098 --- /dev/null +++ b/src/b2qt-update-util/.gitignore @@ -0,0 +1,4 @@ +Makefile +b2qt-update-util +*.o +moc_*.cpp diff --git a/src/b2qt-update-util/b2qt-update-util.pro b/src/b2qt-update-util/b2qt-update-util.pro new file mode 100644 index 0000000..87397b4 --- /dev/null +++ b/src/b2qt-update-util/b2qt-update-util.pro @@ -0,0 +1,2 @@ +QT = core +SOURCES = main.cpp diff --git a/src/b2qt-update-util/main.cpp b/src/b2qt-update-util/main.cpp new file mode 100644 index 0000000..ae26dc5 --- /dev/null +++ b/src/b2qt-update-util/main.cpp @@ -0,0 +1,138 @@ +/**************************************************************************** +** +** Copyright (C) 2014 Digia Plc +** All rights reserved. +** For any questions to Digia, please use the contact form at +** http://qt.digia.com/ +** +** This file is part of Qt Enterprise Embedded. +** +** Licensees holding valid Qt Enterprise licenses may use this file in +** accordance with the Qt Enterprise License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** the contact form at http://qt.digia.com/ +** +****************************************************************************/ + +#include +#include +#include +#include +#include +#include +#include + +bool mount_boot() +{ + return QProcess::execute("mount", QStringList() << "/dev/mmcblk0p1" << "/boot") == 0; +} + +bool umount_boot() +{ + return QProcess::execute("umount", QStringList() << "/boot") == 0; +} + +bool finish_update() +{ + if (!mount_boot()) { + qWarning() << "Could not mount /boot"; + return false; + } + QFile f("/boot/update/state"); + if (!f.open(QFile::WriteOnly)) { + qWarning() << "Could not open file for writing"; + return false; + } + if (f.write("v") != 1) { + qWarning() << "Write error"; + return false; + } + fsync(f.handle()); + f.close(); + if (!umount_boot()) { + qWarning() << "Could not unmount /boot"; + return false; + } + return true; +} + +int start_update(const QString &source) +{ + if (!mount_boot()) { + qWarning() << "Could not mount /boot"; + return false; + } + { + QFile f("/boot/update/source"); + if (!f.open(QFile::WriteOnly)) { + qWarning() << "Could not open file for writing"; + return false; + } + QByteArray ba = source.toUtf8(); + + if (f.write(ba) != ba.size()) { + qWarning() << "Write error"; + return false; + } + fsync(f.handle()); + f.close(); + } + + { + QFile f("/boot/update/state"); + if (!f.open(QFile::WriteOnly)) { + qWarning() << "Could not open file for writing"; + return false; + } + if (f.write("u") != 1) { + qWarning() << "Write error"; + return false; + } + fsync(f.handle()); + f.close(); + } + if (!umount_boot()) { + qWarning() << "Could not unmount /boot"; + return false; + } + reboot(RB_AUTOBOOT); + return true; +} + +void usage() +{ + fprintf(stderr, + "b2qt-update [start|finish] [...]\n" + " start: For internet update provide a http URL as parameter\n" + " finish: An update\n" + ); +} + +int main(int argc, char **argv) +{ + QCoreApplication app(argc, argv); + QStringList args = app.arguments(); + + args.removeFirst(); + if (args.size() == 0) { + usage(); + return 1; + } + + QString arg = args.takeFirst(); + + if (arg == "finish") + return finish_update(); + else if (arg == "start") { + if (args.size() == 0) + return start_update(QString()); + else + return start_update(args.takeFirst()); + } else { + usage(); + return 1; + } +} diff --git a/src/src.pro b/src/src.pro index fe2f463..e8ecbaf 100644 --- a/src/src.pro +++ b/src/src.pro @@ -5,6 +5,7 @@ SUBDIRS += \ imports \ doc \ plugins \ + b2qt-update-util android: SUBDIRS += doppelganger qt_hw_init qconnectivity -- cgit v1.2.3