From 86f893388cce684a31463e775c81d2155bfb2278 Mon Sep 17 00:00:00 2001 From: Floris Bos Date: Sun, 17 Jan 2021 17:43:17 +0100 Subject: [PATCH] Shift+Ctrl+X for advanced users that likes to customize the image - Adds "hidden" shift+ctrl+X shortcut for eXpert image customization options. Allows one to set certain options on RPI OS images, namely: * disable overscan * set hostname * enable ssh and - set Pi user password if using password authentication OR - set authorized_keys (if running Imager on Linux/Mac this will have contents of ~/.ssh/id_rsa.pub prefilled) * configure wifi settings (if computer running Imager is connected by wifi it will prefill wifi SSID and if on Windows also PSK). * set time zone and keyboard layout Related to feature requests/issues: Ref #127 Ref #86 Ref #102 Ref #73 Ref #68 Ref #25 Ref #12 - Option Window also allows setting a couple other general settings: * Adds option for audible notification (beep) when imaging completes. Closes #46 * Adds option not to eject media when done. Closes #144 - No longer suspends a number of Windows services during Imaging (We want Windows to detect the drive and mount it, or we may not be able to alter files on FAT partition). --- CMakeLists.txt | 10 +- OptionsPopup.qml | 473 ++++++++++++++++ countries.txt | 249 +++++++++ debian/changelog | 7 + dependencies/sha256crypt/sha256crypt.c | 741 +++++++++++++++++++++++++ dependencies/sha256crypt/sha256crypt.h | 11 + downloadthread.cpp | 235 +++++++- downloadthread.h | 10 +- driveformatthread.cpp | 2 +- imagewriter.cpp | 240 +++++++- imagewriter.h | 16 +- license.txt | 8 + linux/udisks2api.cpp | 37 ++ linux/udisks2api.h | 2 + main.qml | 12 + qml.qrc | 3 + timezones.txt | 541 ++++++++++++++++++ 17 files changed, 2563 insertions(+), 34 deletions(-) create mode 100644 OptionsPopup.qml create mode 100644 countries.txt create mode 100644 dependencies/sha256crypt/sha256crypt.c create mode 100644 dependencies/sha256crypt/sha256crypt.h create mode 100644 timezones.txt diff --git a/CMakeLists.txt b/CMakeLists.txt index c54dd3ee..3d7fc792 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,9 +6,9 @@ if (APPLE) set(CMAKE_OSX_DEPLOYMENT_TARGET "10.13" CACHE STRING "" FORCE) endif() -project(rpi-imager LANGUAGES CXX) +project(rpi-imager LANGUAGES CXX C) set(IMAGER_VERSION_MAJOR 1) -set(IMAGER_VERSION_MINOR 5) +set(IMAGER_VERSION_MINOR 6) set(IMAGER_VERSION_STR "${IMAGER_VERSION_MAJOR}.${IMAGER_VERSION_MINOR}") set(IMAGER_VERSION_CSV "${IMAGER_VERSION_MAJOR},${IMAGER_VERSION_MINOR},0,0") add_definitions(-DIMAGER_VERSION_STR="${IMAGER_VERSION_STR}") @@ -20,7 +20,7 @@ set(CMAKE_AUTORCC ON) # Adding headers explicity so they are displayed in Qt Creator set(HEADERS config.h imagewriter.h networkaccessmanagerfactory.h nan.h drivelistitem.h drivelistmodel.h drivelistmodelpollthread.h driveformatthread.h powersaveblocker.h - downloadthread.h downloadextractthread.h localfileextractthread.h downloadstatstelemetry.h dependencies/mountutils/src/mountutils.hpp) + downloadthread.h downloadextractthread.h localfileextractthread.h downloadstatstelemetry.h dependencies/mountutils/src/mountutils.hpp dependencies/sha256crypt/sha256crypt.h) # Add dependencies if (APPLE) @@ -43,7 +43,7 @@ elseif (WIN32) windows/winfile.cpp windows/winfile.h windows/rpi-imager.rc) find_package(Qt5WinExtras REQUIRED) - set(EXTRALIBS setupapi Qt5::WinExtras) + set(EXTRALIBS setupapi wlanapi Qt5::WinExtras) endif() include_directories(BEFORE .) @@ -69,7 +69,7 @@ endif() set(SOURCES "main.cpp" "imagewriter.cpp" "networkaccessmanagerfactory.cpp" "drivelistitem.cpp" "drivelistmodel.cpp" "drivelistmodelpollthread.cpp" "downloadthread.cpp" "downloadextractthread.cpp" - "driveformatthread.cpp" "localfileextractthread.cpp" "powersaveblocker.cpp" "downloadstatstelemetry.cpp" "qml.qrc") + "driveformatthread.cpp" "localfileextractthread.cpp" "powersaveblocker.cpp" "downloadstatstelemetry.cpp" "qml.qrc" "dependencies/sha256crypt/sha256crypt.c") find_package(Qt5 COMPONENTS Core Quick LinguistTools Svg OPTIONAL_COMPONENTS Widgets) if (Qt5Widgets_FOUND) diff --git a/OptionsPopup.qml b/OptionsPopup.qml new file mode 100644 index 00000000..6b215b7d --- /dev/null +++ b/OptionsPopup.qml @@ -0,0 +1,473 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (C) 2021 Raspberry Pi (Trading) Limited + */ + +import QtQuick 2.9 +import QtQuick.Controls 2.2 +import QtQuick.Layouts 1.0 +import QtQuick.Controls.Material 2.2 + +Popup { + id: popup + //x: 62 + x: (parent.width-width)/2 + y: 10 + //width: parent.width-125 + width: popupbody.implicitWidth+60 + height: parent.height-20 + padding: 0 + closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside + property bool initialized: false + property string config + property string cmdline + property string firstrun + + // background of title + Rectangle { + color: "#f5f5f5" + anchors.right: parent.right + anchors.top: parent.top + height: 35 + width: parent.width + } + // line under title + Rectangle { + color: "#afafaf" + width: parent.width + y: 35 + implicitHeight: 1 + } + + Text { + id: msgx + text: "X" + anchors.right: parent.right + anchors.top: parent.top + anchors.rightMargin: 25 + anchors.topMargin: 10 + font.family: roboto.name + font.bold: true + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + popup.close() + } + } + } + + ColumnLayout { + spacing: 20 + anchors.fill: parent + + Text { + id: popupheader + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + Layout.fillWidth: true + Layout.topMargin: 10 + font.family: roboto.name + font.bold: true + text: qsTr("Advanced options") + } + + ScrollView { + id: popupbody + font.family: roboto.name + //Layout.maximumWidth: popup.width-30 + Layout.fillWidth: true + Layout.fillHeight: true + Layout.leftMargin: 25 + Layout.topMargin: 10 + clip: true + ScrollBar.vertical.policy: ScrollBar.AlwaysOn + + ColumnLayout { + + GroupBox { + title: qsTr("Image customization options (for this session only)") + Layout.fillWidth: true + + ColumnLayout { + spacing: -10 + + CheckBox { + id: chkOverscan + text: qsTr("Disable overscan") + } + CheckBox { + id: chkHostname + text: qsTr("Set hostname") + onCheckedChanged: { + if (checked) { + fieldHostname.forceActiveFocus() + } + } + } + RowLayout { + enabled: chkHostname.checked + Layout.leftMargin: 40 + + TextField { + id: fieldHostname + text: "raspberrypi" + } + Text { + text : ".local" + color: parent.enabled ? "black" : "grey" + } + } + CheckBox { + id: chkSSH + text: qsTr("Enable SSH") + onCheckedChanged: { + if (checked) { + if (!radioPasswordAuthentication.checked && !radioPubKeyAuthentication.checked) { + radioPasswordAuthentication.checked = true + } + if (radioPasswordAuthentication.checked && !fieldUserPassword.length) { + fieldUserPassword.forceActiveFocus() + } + } + } + } + ColumnLayout { + enabled: chkSSH.checked + Layout.leftMargin: 40 + spacing: -5 + + RadioButton { + id: radioPasswordAuthentication + text: qsTr("Use password authentication") + onCheckedChanged: { + if (checked) { + fieldUserPassword.forceActiveFocus() + } + } + } + + GridLayout { + Layout.leftMargin: 40 + columns: 2 + columnSpacing: 10 + rowSpacing: -5 + enabled: radioPasswordAuthentication.checked + + Text { + text: qsTr("Set password for 'pi' user:") + color: parent.enabled ? "black" : "grey" + } + TextField { + id: fieldUserPassword + echoMode: TextInput.Password + Layout.minimumWidth: 200 + } + } + + RadioButton { + id: radioPubKeyAuthentication + text: qsTr("Allow public-key authentication only") + onCheckedChanged: { + if (checked) { + fieldPublicKey.forceActiveFocus() + } + } + } + GridLayout { + Layout.leftMargin: 40 + columns: 2 + columnSpacing: 10 + rowSpacing: -5 + enabled: radioPubKeyAuthentication.checked + + Text { + text: qsTr("Set authorized_keys for 'pi':") + color: parent.enabled ? "black" : "grey" + } + TextField { + id: fieldPublicKey + Layout.minimumWidth: 200 + } + } + } + + CheckBox { + id: chkWifi + text: qsTr("Configure wifi") + onCheckedChanged: { + if (checked) { + if (!fieldWifiSSID.length) { + fieldWifiSSID.forceActiveFocus() + } else if (!fieldWifiPassword.length) { + fieldWifiPassword.forceActiveFocus() + } + } + } + } + GridLayout { + enabled: chkWifi.checked + Layout.leftMargin: 40 + columns: 2 + columnSpacing: 10 + rowSpacing: -5 + + Text { + text: qsTr("SSID:") + color: parent.enabled ? "black" : "grey" + } + TextField { + id: fieldWifiSSID + Layout.minimumWidth: 200 + } + + Text { + text: qsTr("Password:") + color: parent.enabled ? "black" : "grey" + } + TextField { + id: fieldWifiPassword + Layout.minimumWidth: 200 + echoMode: chkShowPassword.checked ? TextInput.Normal : TextInput.Password + } + + CheckBox { + id: chkShowPassword + Layout.columnSpan: 2 + text: qsTr("Show password") + checked: true + } + + Text { + text: qsTr("Wifi country:") + color: parent.enabled ? "black" : "grey" + } + ComboBox { + id: fieldWifiCountry + editable: true + } + } + + CheckBox { + id: chkLocale + text: qsTr("Set locale settings") + } + GridLayout { + enabled: chkLocale.checked + Layout.leftMargin: 40 + columns: 2 + columnSpacing: 10 + rowSpacing: -5 + + Text { + text: qsTr("Time zone:") + color: parent.enabled ? "black" : "grey" + } + ComboBox { + id: fieldTimezone + editable: true + Layout.minimumWidth: 200 + } + + Text { + text: qsTr("Keyboard layout:") + color: parent.enabled ? "black" : "grey" + } + TextField { + id: fieldKeyboardLayout + Layout.minimumWidth: 200 + text: "us" + } + CheckBox { + id: chkSkipFirstUse + text: qsTr("Skip first-run wizard") + } + } + } + } + + GroupBox { + title: qsTr("Persistent settings") + Layout.fillWidth: true + + ColumnLayout { + spacing: -10 + + CheckBox { + id: chkBeep + text: qsTr("Play sound when finished") + } + CheckBox { + id: chkEject + text: qsTr("Eject media when finished") + } + CheckBox { + id: chkTelemtry + text: qsTr("Enable telemetry") + } + } + } + } + } + + RowLayout { + Layout.alignment: Qt.AlignCenter | Qt.AlignBottom + Layout.bottomMargin: 10 + spacing: 20 + + Button { + text: qsTr("SAVE") + onClicked: { + if (radioPasswordAuthentication.checked && fieldUserPassword.text.length == 0) + { + fieldUserPassword.forceActiveFocus() + return + } + + applySettings() + popup.close() + } + Material.foreground: "#ffffff" + Material.background: "#c51a4a" + font.family: roboto.name + Accessible.onPressAction: clicked() + } + + Text { text: " " } + } + } + + function openPopup() { + if (!initialized) { + chkBeep.checked = imageWriter.getBoolSetting("beep") + chkTelemtry.checked = imageWriter.getBoolSetting("telemetry") + chkEject.checked = imageWriter.getBoolSetting("eject") + fieldTimezone.model = imageWriter.getTimezoneList() + fieldPublicKey.text = imageWriter.getDefaultPubKey() + fieldWifiCountry.model = imageWriter.getCountryList() + fieldWifiCountry.currentIndex = fieldWifiCountry.find("GB") + fieldWifiSSID.text = imageWriter.getSSID() + if (fieldWifiSSID.text.length) { + fieldWifiPassword.text = imageWriter.getPSK(fieldWifiSSID.text) + if (fieldWifiPassword.text.length) { + chkShowPassword.checked = false + } + } + var tz = imageWriter.getTimezone() + var tzidx = fieldTimezone.find(tz) + if (tzidx === -1) { + fieldTimezone.editText = tz + } else { + fieldTimezone.currentIndex = tzidx + } + /* Lacking an easy cross-platform to fetch keyboard layout + from host system, just default to "gb" for people in + UK time zone for now, and "us" for everyone else */ + if (tz == "Europe/London") { + fieldKeyboardLayout.text = "gb" + } + + initialized = true + } + + open() + } + + function addCmdline(s) { + cmdline += " "+s + } + function addConfig(s) { + config += s+"\n" + } + function addFirstRun(s) { + firstrun += s+"\n" + } + function escapeshellarg(arg) { + return "'"+arg.replace(/'/g, "\\'")+"'" + } + + function applySettings() + { + cmdline = "" + config = "" + firstrun = "" + + if (chkOverscan.checked) { + addConfig("disable_overscan=1") + } + if (chkHostname.checked && fieldHostname.length) { + addFirstRun("CURRENT_HOSTNAME=`cat /etc/hostname | tr -d \" \\t\\n\\r\"`") + addFirstRun("echo "+fieldHostname.text+" >/etc/hostname") + addFirstRun("sed -i \"s/127.0.1.1.*$CURRENT_HOSTNAME/127.0.1.1\\t"+fieldHostname.text+"/g\" /etc/hosts") + } + if (chkSSH.checked) { + // First user may not be called 'pi' on all distributions, so look username up + addFirstRun("FIRSTUSER=`getent passwd 1000 | cut -d: -f1`"); + addFirstRun("FIRSTUSERHOME=`getent passwd 1000 | cut -d: -f6`") + + if (radioPasswordAuthentication.checked) { + addFirstRun("echo \"$FIRSTUSER:\""+escapeshellarg(imageWriter.crypt(fieldUserPassword.text))+" | chpasswd -e") + } + if (radioPubKeyAuthentication.checked) { + var pubkey = fieldPublicKey.text.replace(/\n/g, "") + if (pubkey.length) { + addFirstRun("install -o \"$FIRSTUSER\" -m 700 -d \"$FIRSTUSERHOME/.ssh\"") + addFirstRun("install -o \"$FIRSTUSER\" -m 600 <(echo \""+pubkey+"\") \"$FIRSTUSERHOME/.ssh/authorized_keys\"") + } + addFirstRun("echo 'PasswordAuthentication no' >>/etc/ssh/sshd_config") + } + addFirstRun("systemctl enable ssh") + } + if (chkWifi.checked) { + var wpaconfig = "country="+fieldWifiCountry.editText+"\n" + wpaconfig += "ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev\n" + wpaconfig += "ap_scan=1\n\n" + wpaconfig += "update_config=1\n" + wpaconfig += "network={\n" + wpaconfig += "\tssid=\""+fieldWifiSSID.text+"\"\n" + wpaconfig += "\tpsk=\""+fieldWifiPassword.text+"\"\n" + wpaconfig += "}\n" + + addFirstRun("cat >/etc/wpa_supplicant/wpa_supplicant.conf < $filename") + addFirstRun("done") + } + if (chkLocale.checked) { + if (chkSkipFirstUse) { + addFirstRun("rm -f /etc/xdg/autostart/piwiz.desktop") + } + + addFirstRun("rm -f /etc/localtime") + addFirstRun("echo \""+fieldTimezone.editText+"\" >/etc/timezone") + addFirstRun("dpkg-reconfigure -f noninteractive tzdata") + addFirstRun("cat >/etc/default/keyboard < Fri, 15 Jan 2021 22:00:21 +0100 + rpi-imager (1.5) unstable; urgency=medium * More verbose progress/error reporting diff --git a/dependencies/sha256crypt/sha256crypt.c b/dependencies/sha256crypt/sha256crypt.c new file mode 100644 index 00000000..256fbf0b --- /dev/null +++ b/dependencies/sha256crypt/sha256crypt.c @@ -0,0 +1,741 @@ +/* SHA256-based Unix crypt implementation. + Released into the Public Domain by Ulrich Drepper . */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef __APPLE__ +#include +#else +#ifdef _WIN32 +#define __LITTLE_ENDIAN 1234 +#define __BYTE_ORDER __LITTLE_ENDIAN +#define MAX(x, y) (((x) > (y)) ? (x) : (y)) +#define MIN(x, y) (((x) < (y)) ? (x) : (y)) + +char *stpncpy(char *dest, const char *src, size_t n) +{ + size_t size = strnlen(src, n); + memcpy(dest, src, size); + dest += size; + if (size != n) + dest[0] = '\0'; + + return dest; +} + +#else +#include +#endif +#endif + +/* Structure to save state of computation between the single steps. */ +struct sha256_ctx +{ + uint32_t H[8]; + + uint32_t total[2]; + uint32_t buflen; + char buffer[128]; /* NB: always correctly aligned for uint32_t. */ +}; + + +#if __BYTE_ORDER == __LITTLE_ENDIAN +# define SWAP(n) \ + (((n) << 24) | (((n) & 0xff00) << 8) | (((n) >> 8) & 0xff00) | ((n) >> 24)) +#else +# define SWAP(n) (n) +#endif + + +/* This array contains the bytes used to pad the buffer to the next + 64-byte boundary. (FIPS 180-2:5.1.1) */ +static const unsigned char fillbuf[64] = { 0x80, 0 /* , 0, 0, ... */ }; + + +/* Constants for SHA256 from FIPS 180-2:4.2.2. */ +static const uint32_t K[64] = + { + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, + 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, + 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, + 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, + 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, + 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, + 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, + 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, + 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, + 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, + 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, + 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, + 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, + 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, + 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, + 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2 + }; + + +/* Process LEN bytes of BUFFER, accumulating context into CTX. + It is assumed that LEN % 64 == 0. */ +static void +sha256_process_block (const void *buffer, size_t len, struct sha256_ctx *ctx) +{ + const uint32_t *words = buffer; + size_t nwords = len / sizeof (uint32_t); + uint32_t a = ctx->H[0]; + uint32_t b = ctx->H[1]; + uint32_t c = ctx->H[2]; + uint32_t d = ctx->H[3]; + uint32_t e = ctx->H[4]; + uint32_t f = ctx->H[5]; + uint32_t g = ctx->H[6]; + uint32_t h = ctx->H[7]; + + /* First increment the byte count. FIPS 180-2 specifies the possible + length of the file up to 2^64 bits. Here we only compute the + number of bytes. Do a double word increment. */ + ctx->total[0] += len; + if (ctx->total[0] < len) + ++ctx->total[1]; + + /* Process all bytes in the buffer with 64 bytes in each round of + the loop. */ + while (nwords > 0) + { + uint32_t W[64]; + uint32_t a_save = a; + uint32_t b_save = b; + uint32_t c_save = c; + uint32_t d_save = d; + uint32_t e_save = e; + uint32_t f_save = f; + uint32_t g_save = g; + uint32_t h_save = h; + + /* Operators defined in FIPS 180-2:4.1.2. */ +#define Ch(x, y, z) ((x & y) ^ (~x & z)) +#define Maj(x, y, z) ((x & y) ^ (x & z) ^ (y & z)) +#define S0(x) (CYCLIC (x, 2) ^ CYCLIC (x, 13) ^ CYCLIC (x, 22)) +#define S1(x) (CYCLIC (x, 6) ^ CYCLIC (x, 11) ^ CYCLIC (x, 25)) +#define R0(x) (CYCLIC (x, 7) ^ CYCLIC (x, 18) ^ (x >> 3)) +#define R1(x) (CYCLIC (x, 17) ^ CYCLIC (x, 19) ^ (x >> 10)) + + /* It is unfortunate that C does not provide an operator for + cyclic rotation. Hope the C compiler is smart enough. */ +#define CYCLIC(w, s) ((w >> s) | (w << (32 - s))) + + /* Compute the message schedule according to FIPS 180-2:6.2.2 step 2. */ + for (unsigned int t = 0; t < 16; ++t) + { + W[t] = SWAP (*words); + ++words; + } + for (unsigned int t = 16; t < 64; ++t) + W[t] = R1 (W[t - 2]) + W[t - 7] + R0 (W[t - 15]) + W[t - 16]; + + /* The actual computation according to FIPS 180-2:6.2.2 step 3. */ + for (unsigned int t = 0; t < 64; ++t) + { + uint32_t T1 = h + S1 (e) + Ch (e, f, g) + K[t] + W[t]; + uint32_t T2 = S0 (a) + Maj (a, b, c); + h = g; + g = f; + f = e; + e = d + T1; + d = c; + c = b; + b = a; + a = T1 + T2; + } + + /* Add the starting values of the context according to FIPS 180-2:6.2.2 + step 4. */ + a += a_save; + b += b_save; + c += c_save; + d += d_save; + e += e_save; + f += f_save; + g += g_save; + h += h_save; + + /* Prepare for the next round. */ + nwords -= 16; + } + + /* Put checksum in context given as argument. */ + ctx->H[0] = a; + ctx->H[1] = b; + ctx->H[2] = c; + ctx->H[3] = d; + ctx->H[4] = e; + ctx->H[5] = f; + ctx->H[6] = g; + ctx->H[7] = h; +} + + +/* Initialize structure containing state of computation. + (FIPS 180-2:5.3.2) */ +static void +sha256_init_ctx (struct sha256_ctx *ctx) +{ + ctx->H[0] = 0x6a09e667; + ctx->H[1] = 0xbb67ae85; + ctx->H[2] = 0x3c6ef372; + ctx->H[3] = 0xa54ff53a; + ctx->H[4] = 0x510e527f; + ctx->H[5] = 0x9b05688c; + ctx->H[6] = 0x1f83d9ab; + ctx->H[7] = 0x5be0cd19; + + ctx->total[0] = ctx->total[1] = 0; + ctx->buflen = 0; +} + + +/* Process the remaining bytes in the internal buffer and the usual + prolog according to the standard and write the result to RESBUF. + + IMPORTANT: On some systems it is required that RESBUF is correctly + aligned for a 32 bits value. */ +static void * +sha256_finish_ctx (struct sha256_ctx *ctx, void *resbuf) +{ + /* Take yet unprocessed bytes into account. */ + uint32_t bytes = ctx->buflen; + size_t pad; + + /* Now count remaining bytes. */ + ctx->total[0] += bytes; + if (ctx->total[0] < bytes) + ++ctx->total[1]; + + pad = bytes >= 56 ? 64 + 56 - bytes : 56 - bytes; + memcpy (&ctx->buffer[bytes], fillbuf, pad); + + /* Put the 64-bit file length in *bits* at the end of the buffer. */ + *(uint32_t *) &ctx->buffer[bytes + pad + 4] = SWAP (ctx->total[0] << 3); + *(uint32_t *) &ctx->buffer[bytes + pad] = SWAP ((ctx->total[1] << 3) | + (ctx->total[0] >> 29)); + + /* Process last bytes. */ + sha256_process_block (ctx->buffer, bytes + pad + 8, ctx); + + /* Put result from CTX in first 32 bytes following RESBUF. */ + for (unsigned int i = 0; i < 8; ++i) + ((uint32_t *) resbuf)[i] = SWAP (ctx->H[i]); + + return resbuf; +} + + +static void +sha256_process_bytes (const void *buffer, size_t len, struct sha256_ctx *ctx) +{ + /* When we already have some bits in our internal buffer concatenate + both inputs first. */ + if (ctx->buflen != 0) + { + size_t left_over = ctx->buflen; + size_t add = 128 - left_over > len ? len : 128 - left_over; + + memcpy (&ctx->buffer[left_over], buffer, add); + ctx->buflen += add; + + if (ctx->buflen > 64) + { + sha256_process_block (ctx->buffer, ctx->buflen & ~63, ctx); + + ctx->buflen &= 63; + /* The regions in the following copy operation cannot overlap. */ + memcpy (ctx->buffer, &ctx->buffer[(left_over + add) & ~63], + ctx->buflen); + } + + buffer = (const char *) buffer + add; + len -= add; + } + + /* Process available complete blocks. */ + if (len >= 64) + { +/* To check alignment gcc has an appropriate operator. Other + compilers don't. */ +#if __GNUC__ >= 2 +# define UNALIGNED_P(p) (((uintptr_t) p) % __alignof__ (uint32_t) != 0) +#else +# define UNALIGNED_P(p) (((uintptr_t) p) % sizeof (uint32_t) != 0) +#endif + if (UNALIGNED_P (buffer)) + while (len > 64) + { + sha256_process_block (memcpy (ctx->buffer, buffer, 64), 64, ctx); + buffer = (const char *) buffer + 64; + len -= 64; + } + else + { + sha256_process_block (buffer, len & ~63, ctx); + buffer = (const char *) buffer + (len & ~63); + len &= 63; + } + } + + /* Move remaining bytes into internal buffer. */ + if (len > 0) + { + size_t left_over = ctx->buflen; + + memcpy (&ctx->buffer[left_over], buffer, len); + left_over += len; + if (left_over >= 64) + { + sha256_process_block (ctx->buffer, 64, ctx); + left_over -= 64; + memcpy (ctx->buffer, &ctx->buffer[64], left_over); + } + ctx->buflen = left_over; + } +} + + +/* Define our magic string to mark salt for SHA256 "encryption" + replacement. */ +static const char sha256_salt_prefix[] = "$5$"; + +/* Prefix for optional rounds specification. */ +static const char sha256_rounds_prefix[] = "rounds="; + +/* Maximum salt string length. */ +#define SALT_LEN_MAX 16 +/* Default number of rounds if not explicitly specified. */ +#define ROUNDS_DEFAULT 5000 +/* Minimum number of rounds. */ +#define ROUNDS_MIN 1000 +/* Maximum number of rounds. */ +#define ROUNDS_MAX 999999999 + +/* Table with characters for base64 transformation. */ +static const char b64t[64] = +"./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + + +static char * +sha256_crypt_r (const char *key, const char *salt, char *buffer, int buflen) +{ + unsigned char alt_result[32] + __attribute__ ((__aligned__ (__alignof__ (uint32_t)))); + unsigned char temp_result[32] + __attribute__ ((__aligned__ (__alignof__ (uint32_t)))); + struct sha256_ctx ctx; + struct sha256_ctx alt_ctx; + size_t salt_len; + size_t key_len; + size_t cnt; + char *cp; + char *copied_key = NULL; + char *copied_salt = NULL; + char *p_bytes; + char *s_bytes; + /* Default number of rounds. */ + size_t rounds = ROUNDS_DEFAULT; + bool rounds_custom = false; + + /* Find beginning of salt string. The prefix should normally always + be present. Just in case it is not. */ + if (strncmp (sha256_salt_prefix, salt, sizeof (sha256_salt_prefix) - 1) == 0) + /* Skip salt prefix. */ + salt += sizeof (sha256_salt_prefix) - 1; + + if (strncmp (salt, sha256_rounds_prefix, sizeof (sha256_rounds_prefix) - 1) + == 0) + { + const char *num = salt + sizeof (sha256_rounds_prefix) - 1; + char *endp; + unsigned long int srounds = strtoul (num, &endp, 10); + if (*endp == '$') + { + salt = endp + 1; + rounds = MAX (ROUNDS_MIN, MIN (srounds, ROUNDS_MAX)); + rounds_custom = true; + } + } + + salt_len = MIN (strcspn (salt, "$"), SALT_LEN_MAX); + key_len = strlen (key); + + if ((key - (char *) 0) % __alignof__ (uint32_t) != 0) + { + char *tmp = (char *) alloca (key_len + __alignof__ (uint32_t)); + key = copied_key = + memcpy (tmp + __alignof__ (uint32_t) + - (tmp - (char *) 0) % __alignof__ (uint32_t), + key, key_len); + } + + if ((salt - (char *) 0) % __alignof__ (uint32_t) != 0) + { + char *tmp = (char *) alloca (salt_len + __alignof__ (uint32_t)); + salt = copied_salt = + memcpy (tmp + __alignof__ (uint32_t) + - (tmp - (char *) 0) % __alignof__ (uint32_t), + salt, salt_len); + } + + /* Prepare for the real work. */ + sha256_init_ctx (&ctx); + + /* Add the key string. */ + sha256_process_bytes (key, key_len, &ctx); + + /* The last part is the salt string. This must be at most 16 + characters and it ends at the first `$' character (for + compatibility with existing implementations). */ + sha256_process_bytes (salt, salt_len, &ctx); + + + /* Compute alternate SHA256 sum with input KEY, SALT, and KEY. The + final result will be added to the first context. */ + sha256_init_ctx (&alt_ctx); + + /* Add key. */ + sha256_process_bytes (key, key_len, &alt_ctx); + + /* Add salt. */ + sha256_process_bytes (salt, salt_len, &alt_ctx); + + /* Add key again. */ + sha256_process_bytes (key, key_len, &alt_ctx); + + /* Now get result of this (32 bytes) and add it to the other + context. */ + sha256_finish_ctx (&alt_ctx, alt_result); + + /* Add for any character in the key one byte of the alternate sum. */ + for (cnt = key_len; cnt > 32; cnt -= 32) + sha256_process_bytes (alt_result, 32, &ctx); + sha256_process_bytes (alt_result, cnt, &ctx); + + /* Take the binary representation of the length of the key and for every + 1 add the alternate sum, for every 0 the key. */ + for (cnt = key_len; cnt > 0; cnt >>= 1) + if ((cnt & 1) != 0) + sha256_process_bytes (alt_result, 32, &ctx); + else + sha256_process_bytes (key, key_len, &ctx); + + /* Create intermediate result. */ + sha256_finish_ctx (&ctx, alt_result); + + /* Start computation of P byte sequence. */ + sha256_init_ctx (&alt_ctx); + + /* For every character in the password add the entire password. */ + for (cnt = 0; cnt < key_len; ++cnt) + sha256_process_bytes (key, key_len, &alt_ctx); + + /* Finish the digest. */ + sha256_finish_ctx (&alt_ctx, temp_result); + + /* Create byte sequence P. */ + cp = p_bytes = alloca (key_len); + for (cnt = key_len; cnt >= 32; cnt -= 32) + cp = memcpy (cp, temp_result, 32) + 32; + memcpy (cp, temp_result, cnt); + + /* Start computation of S byte sequence. */ + sha256_init_ctx (&alt_ctx); + + /* For every character in the password add the entire password. */ + for (cnt = 0; cnt < 16 + alt_result[0]; ++cnt) + sha256_process_bytes (salt, salt_len, &alt_ctx); + + /* Finish the digest. */ + sha256_finish_ctx (&alt_ctx, temp_result); + + /* Create byte sequence S. */ + cp = s_bytes = alloca (salt_len); + for (cnt = salt_len; cnt >= 32; cnt -= 32) + cp = memcpy (cp, temp_result, 32) + 32; + memcpy (cp, temp_result, cnt); + + /* Repeatedly run the collected hash value through SHA256 to burn + CPU cycles. */ + for (cnt = 0; cnt < rounds; ++cnt) + { + /* New context. */ + sha256_init_ctx (&ctx); + + /* Add key or last result. */ + if ((cnt & 1) != 0) + sha256_process_bytes (p_bytes, key_len, &ctx); + else + sha256_process_bytes (alt_result, 32, &ctx); + + /* Add salt for numbers not divisible by 3. */ + if (cnt % 3 != 0) + sha256_process_bytes (s_bytes, salt_len, &ctx); + + /* Add key for numbers not divisible by 7. */ + if (cnt % 7 != 0) + sha256_process_bytes (p_bytes, key_len, &ctx); + + /* Add key or last result. */ + if ((cnt & 1) != 0) + sha256_process_bytes (alt_result, 32, &ctx); + else + sha256_process_bytes (p_bytes, key_len, &ctx); + + /* Create intermediate result. */ + sha256_finish_ctx (&ctx, alt_result); + } + + /* Now we can construct the result string. It consists of three + parts. */ + cp = stpncpy (buffer, sha256_salt_prefix, MAX (0, buflen)); + buflen -= sizeof (sha256_salt_prefix) - 1; + + if (rounds_custom) + { + int n = snprintf (cp, MAX (0, buflen), "%s%zu$", + sha256_rounds_prefix, rounds); + cp += n; + buflen -= n; + } + + cp = stpncpy (cp, salt, MIN ((size_t) MAX (0, buflen), salt_len)); + buflen -= MIN ((size_t) MAX (0, buflen), salt_len); + + if (buflen > 0) + { + *cp++ = '$'; + --buflen; + } + +#define b64_from_24bit(B2, B1, B0, N) \ + do { \ + unsigned int w = ((B2) << 16) | ((B1) << 8) | (B0); \ + int n = (N); \ + while (n-- > 0 && buflen > 0) \ + { \ + *cp++ = b64t[w & 0x3f]; \ + --buflen; \ + w >>= 6; \ + } \ + } while (0) + + b64_from_24bit (alt_result[0], alt_result[10], alt_result[20], 4); + b64_from_24bit (alt_result[21], alt_result[1], alt_result[11], 4); + b64_from_24bit (alt_result[12], alt_result[22], alt_result[2], 4); + b64_from_24bit (alt_result[3], alt_result[13], alt_result[23], 4); + b64_from_24bit (alt_result[24], alt_result[4], alt_result[14], 4); + b64_from_24bit (alt_result[15], alt_result[25], alt_result[5], 4); + b64_from_24bit (alt_result[6], alt_result[16], alt_result[26], 4); + b64_from_24bit (alt_result[27], alt_result[7], alt_result[17], 4); + b64_from_24bit (alt_result[18], alt_result[28], alt_result[8], 4); + b64_from_24bit (alt_result[9], alt_result[19], alt_result[29], 4); + b64_from_24bit (0, alt_result[31], alt_result[30], 3); + if (buflen <= 0) + { + errno = ERANGE; + buffer = NULL; + } + else + *cp = '\0'; /* Terminate the string. */ + + /* Clear the buffer for the intermediate result so that people + attaching to processes or reading core dumps cannot get any + information. We do it in this way to clear correct_words[] + inside the SHA256 implementation as well. */ + sha256_init_ctx (&ctx); + sha256_finish_ctx (&ctx, alt_result); + memset (temp_result, '\0', sizeof (temp_result)); + memset (p_bytes, '\0', key_len); + memset (s_bytes, '\0', salt_len); + memset (&ctx, '\0', sizeof (ctx)); + memset (&alt_ctx, '\0', sizeof (alt_ctx)); + if (copied_key != NULL) + memset (copied_key, '\0', key_len); + if (copied_salt != NULL) + memset (copied_salt, '\0', salt_len); + + return buffer; +} + + +/* This entry point is equivalent to the `crypt' function in Unix + libcs. */ +char * +sha256_crypt (const char *key, const char *salt) +{ + /* We don't want to have an arbitrary limit in the size of the + password. We can compute an upper bound for the size of the + result in advance and so we can prepare the buffer we pass to + `sha256_crypt_r'. */ + static char *buffer; + static int buflen; + int needed = (sizeof (sha256_salt_prefix) - 1 + + sizeof (sha256_rounds_prefix) + 9 + 1 + + strlen (salt) + 1 + 43 + 1); + + if (buflen < needed) + { + char *new_buffer = (char *) realloc (buffer, needed); + if (new_buffer == NULL) + return NULL; + + buffer = new_buffer; + buflen = needed; + } + + return sha256_crypt_r (key, salt, buffer, buflen); +} + + +#ifdef TEST +static const struct +{ + const char *input; + const char result[32]; +} tests[] = + { + /* Test vectors from FIPS 180-2: appendix B.1. */ + { "abc", + "\xba\x78\x16\xbf\x8f\x01\xcf\xea\x41\x41\x40\xde\x5d\xae\x22\x23" + "\xb0\x03\x61\xa3\x96\x17\x7a\x9c\xb4\x10\xff\x61\xf2\x00\x15\xad" }, + /* Test vectors from FIPS 180-2: appendix B.2. */ + { "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq", + "\x24\x8d\x6a\x61\xd2\x06\x38\xb8\xe5\xc0\x26\x93\x0c\x3e\x60\x39" + "\xa3\x3c\xe4\x59\x64\xff\x21\x67\xf6\xec\xed\xd4\x19\xdb\x06\xc1" }, + /* Test vectors from the NESSIE project. */ + { "", + "\xe3\xb0\xc4\x42\x98\xfc\x1c\x14\x9a\xfb\xf4\xc8\x99\x6f\xb9\x24" + "\x27\xae\x41\xe4\x64\x9b\x93\x4c\xa4\x95\x99\x1b\x78\x52\xb8\x55" }, + { "a", + "\xca\x97\x81\x12\xca\x1b\xbd\xca\xfa\xc2\x31\xb3\x9a\x23\xdc\x4d" + "\xa7\x86\xef\xf8\x14\x7c\x4e\x72\xb9\x80\x77\x85\xaf\xee\x48\xbb" }, + { "message digest", + "\xf7\x84\x6f\x55\xcf\x23\xe1\x4e\xeb\xea\xb5\xb4\xe1\x55\x0c\xad" + "\x5b\x50\x9e\x33\x48\xfb\xc4\xef\xa3\xa1\x41\x3d\x39\x3c\xb6\x50" }, + { "abcdefghijklmnopqrstuvwxyz", + "\x71\xc4\x80\xdf\x93\xd6\xae\x2f\x1e\xfa\xd1\x44\x7c\x66\xc9\x52" + "\x5e\x31\x62\x18\xcf\x51\xfc\x8d\x9e\xd8\x32\xf2\xda\xf1\x8b\x73" }, + { "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq", + "\x24\x8d\x6a\x61\xd2\x06\x38\xb8\xe5\xc0\x26\x93\x0c\x3e\x60\x39" + "\xa3\x3c\xe4\x59\x64\xff\x21\x67\xf6\xec\xed\xd4\x19\xdb\x06\xc1" }, + { "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789", + "\xdb\x4b\xfc\xbd\x4d\xa0\xcd\x85\xa6\x0c\x3c\x37\xd3\xfb\xd8\x80" + "\x5c\x77\xf1\x5f\xc6\xb1\xfd\xfe\x61\x4e\xe0\xa7\xc8\xfd\xb4\xc0" }, + { "123456789012345678901234567890123456789012345678901234567890" + "12345678901234567890", + "\xf3\x71\xbc\x4a\x31\x1f\x2b\x00\x9e\xef\x95\x2d\xd8\x3c\xa8\x0e" + "\x2b\x60\x02\x6c\x8e\x93\x55\x92\xd0\xf9\xc3\x08\x45\x3c\x81\x3e" } + }; +#define ntests (sizeof (tests) / sizeof (tests[0])) + + +static const struct +{ + const char *salt; + const char *input; + const char *expected; +} tests2[] = +{ + { "$5$saltstring", "Hello world!", + "$5$saltstring$5B8vYYiY.CVt1RlTTf8KbXBH3hsxY/GNooZaBBGWEc5" }, + { "$5$rounds=10000$saltstringsaltstring", "Hello world!", + "$5$rounds=10000$saltstringsaltst$3xv.VbSHBb41AL9AvLeujZkZRBAwqFMz2." + "opqey6IcA" }, + { "$5$rounds=5000$toolongsaltstring", "This is just a test", + "$5$rounds=5000$toolongsaltstrin$Un/5jzAHMgOGZ5.mWJpuVolil07guHPvOW8" + "mGRcvxa5" }, + { "$5$rounds=1400$anotherlongsaltstring", + "a very much longer text to encrypt. This one even stretches over more" + "than one line.", + "$5$rounds=1400$anotherlongsalts$Rx.j8H.h8HjEDGomFU8bDkXm3XIUnzyxf12" + "oP84Bnq1" }, + { "$5$rounds=77777$short", + "we have a short salt string but not a short password", + "$5$rounds=77777$short$JiO1O3ZpDAxGJeaDIuqCoEFysAe1mZNJRs3pw0KQRd/" }, + { "$5$rounds=123456$asaltof16chars..", "a short string", + "$5$rounds=123456$asaltof16chars..$gP3VQ/6X7UUEW3HkBn2w1/Ptq2jxPyzV/" + "cZKmF/wJvD" }, + { "$5$rounds=10$roundstoolow", "the minimum number is still observed", + "$5$rounds=1000$roundstoolow$yfvwcWrQ8l/K0DAWyuPMDNHpIVlTQebY9l/gL97" + "2bIC" }, +}; +#define ntests2 (sizeof (tests2) / sizeof (tests2[0])) + + +int +main (void) +{ + struct sha256_ctx ctx; + char sum[32]; + int result = 0; + int cnt; + + for (cnt = 0; cnt < (int) ntests; ++cnt) + { + sha256_init_ctx (&ctx); + sha256_process_bytes (tests[cnt].input, strlen (tests[cnt].input), &ctx); + sha256_finish_ctx (&ctx, sum); + if (memcmp (tests[cnt].result, sum, 32) != 0) + { + printf ("test %d run %d failed\n", cnt, 1); + result = 1; + } + + sha256_init_ctx (&ctx); + for (int i = 0; tests[cnt].input[i] != '\0'; ++i) + sha256_process_bytes (&tests[cnt].input[i], 1, &ctx); + sha256_finish_ctx (&ctx, sum); + if (memcmp (tests[cnt].result, sum, 32) != 0) + { + printf ("test %d run %d failed\n", cnt, 2); + result = 1; + } + } + + /* Test vector from FIPS 180-2: appendix B.3. */ + char buf[1000]; + memset (buf, 'a', sizeof (buf)); + sha256_init_ctx (&ctx); + for (int i = 0; i < 1000; ++i) + sha256_process_bytes (buf, sizeof (buf), &ctx); + sha256_finish_ctx (&ctx, sum); + static const char expected[32] = + "\xcd\xc7\x6e\x5c\x99\x14\xfb\x92\x81\xa1\xc7\xe2\x84\xd7\x3e\x67" + "\xf1\x80\x9a\x48\xa4\x97\x20\x0e\x04\x6d\x39\xcc\xc7\x11\x2c\xd0"; + if (memcmp (expected, sum, 32) != 0) + { + printf ("test %d failed\n", cnt); + result = 1; + } + + for (cnt = 0; cnt < ntests2; ++cnt) + { + char *cp = sha256_crypt (tests2[cnt].input, tests2[cnt].salt); + + if (strcmp (cp, tests2[cnt].expected) != 0) + { + printf ("test %d: expected \"%s\", got \"%s\"\n", + cnt, tests2[cnt].expected, cp); + result = 1; + } + } + + if (result == 0) + puts ("all tests OK"); + + return result; +} +#endif diff --git a/dependencies/sha256crypt/sha256crypt.h b/dependencies/sha256crypt/sha256crypt.h new file mode 100644 index 00000000..635f5ddc --- /dev/null +++ b/dependencies/sha256crypt/sha256crypt.h @@ -0,0 +1,11 @@ +#ifndef SHA256CRYPT_H +#define SHA256CRYPT_H + +extern "C" { + +char * +sha256_crypt (const char *key, const char *salt); + +} + +#endif // SHA256CRYPT_H diff --git a/downloadthread.cpp b/downloadthread.cpp index 24c4432c..925204b8 100644 --- a/downloadthread.cpp +++ b/downloadthread.cpp @@ -40,6 +40,9 @@ DownloadThread::DownloadThread(const QByteArray &url, const QByteArray &localfil if (!_curlCount) curl_global_init(CURL_GLOBAL_DEFAULT); _curlCount++; + + QSettings settings; + _ejectEnabled = settings.value("eject", true).toBool(); } DownloadThread::~DownloadThread() @@ -124,15 +127,15 @@ bool DownloadThread::_openAndPrepareDevice() if (std::regex_match(_filename.constData(), m, windriveregex)) { - QByteArray nr = QByteArray::fromStdString(m[1]); + QByteArray _nr = QByteArray::fromStdString(m[1]); - if (!nr.isEmpty()) { - qDebug() << "Removing partition table from Windows drive #" << nr << "(" << _filename << ")"; + if (!_nr.isEmpty()) { + qDebug() << "Removing partition table from Windows drive #" << _nr << "(" << _filename << ")"; QProcess proc; proc.start("diskpart"); proc.waitForStarted(); - proc.write("select disk "+nr+"\r\n" + proc.write("select disk "+_nr+"\r\n" "clean\r\n" "rescan\r\n"); proc.closeWriteChannel(); @@ -665,14 +668,6 @@ void DownloadThread::_writeComplete() emit finalizing(); -#ifdef Q_OS_WIN - // Temporarily stop storage services to prevent \[System Volume Information]\WPSettings.dat being created - QProcess p1; - QStringList args = {"stop", "StorSvc"}; - qDebug() << "Stopping storage services"; - p1.execute("net", args); -#endif - if (_firstBlock) { qDebug() << "Writing first block (which we skipped at first)"; @@ -696,14 +691,18 @@ void DownloadThread::_writeComplete() QThread::sleep(1); _filename.replace("/dev/rdisk", "/dev/disk"); #endif - eject_disk(_filename.constData()); -#ifdef Q_OS_WIN - QStringList args2 = {"start", "StorSvc"}; - QProcess *p2 = new QProcess(this); - qDebug() << "Restarting storage services"; - p2->startDetached("net", args2); -#endif + if (_ejectEnabled && _config.isEmpty() && _cmdline.isEmpty()) + eject_disk(_filename.constData()); + + if (!_config.isEmpty() || !_cmdline.isEmpty()) + { + if (!_customizeImage()) + return; + + if (_ejectEnabled) + eject_disk(_filename.constData()); + } emit success(); } @@ -797,3 +796,201 @@ qint64 DownloadThread::_sectorsWritten() #endif return -1; } + +void DownloadThread::setImageCustomization(const QByteArray &config, const QByteArray &cmdline, const QByteArray &firstrun) +{ + _config = config; + _cmdline = cmdline; + _firstrun = firstrun; +} + +bool DownloadThread::_customizeImage() +{ + QString folder; + QByteArray devlower = _filename.toLower(); + + emit preparationStatusUpdate(tr("Waiting for FAT partition to be mounted")); + + /* See if OS auto-mounted the device */ + for (int tries = 0; tries < 3; tries++) + { + QThread::sleep(1); + auto l = Drivelist::ListStorageDevices(); + for (auto i : l) + { + if (QByteArray::fromStdString(i.device).toLower() == devlower && i.mountpoints.size()) + { + folder = QByteArray::fromStdString(i.mountpoints.front()); + break; + } + } + } + +#ifdef Q_OS_LINUX + bool manualmount = false; + + if (folder.isEmpty()) + { + /* Manually mount folder */ + manualmount = true; + QByteArray fatpartition = _filename; + if (isdigit(fatpartition.at(fatpartition.length()-1))) + fatpartition += "p1"; + else + fatpartition += "1"; + + if (::access(devlower.constData(), W_OK) != 0) + { + /* Not running as root, try to outsource mounting to udisks2 */ + #ifndef QT_NO_DBUS + UDisks2Api udisks2; + folder = udisks2.mountDevice(fatpartition); + #endif + } + else + { + /* Running as root, attempt running mount directly */ + QTemporaryDir td; + QStringList args; + folder = td.path(); + args << fatpartition << folder; + + if (QProcess::execute("mount", args) != 0) + { + emit error(tr("Error mounting FAT32 partition")); + return false; + } + td.setAutoRemove(false); + } + } +#endif + + if (folder.isEmpty()) + { + // + qDebug() << "drive info. searching for:" << devlower; + auto l = Drivelist::ListStorageDevices(); + for (auto i : l) + { + qDebug() << "drive" << QByteArray::fromStdString(i.device).toLower(); + for (auto mp : i.mountpoints) { + qDebug() << "mountpoint:" << QByteArray::fromStdString(mp); + } + } + // + + emit error(tr("Operating system did not mount FAT32 partition")); + return false; + } + + emit preparationStatusUpdate(tr("Customizing image")); + + if (!_firstrun.isEmpty()) + { + QFile f(folder+"/firstrun.sh"); + if (f.open(f.WriteOnly)) + { + f.write(_firstrun); + f.close(); + } + else + { + emit error(tr("Error creating firstrun.sh on FAT partition")); + return false; + } + } + + if (!_config.isEmpty()) + { + auto configItems = _config.split('\n'); + configItems.removeAll(""); + QByteArray config; + + QFile f(folder+"/config.txt"); + if (f.exists() && f.open(f.ReadOnly)) + { + config = f.readAll(); + f.close(); + } + + for (QByteArray item : configItems) + { + if (config.contains("#"+item)) { + /* Uncomment existing line */ + config.replace("#"+item, item); + } else if (config.contains("\n"+item)) { + /* config.txt already contains the line */ + } else { + /* Append new line to config.txt */ + if (config.right(1) != "\n") + config += "\n"+item+"\n"; + else + config += item+"\n"; + } + } + + if (f.open(f.WriteOnly)) + { + f.write(config); + f.close(); + } + else + { + emit error(tr("Error writing to config.txt on FAT partition")); + return false; + } + } + + if (!_cmdline.isEmpty()) + { + QByteArray cmdline; + + QFile f(folder+"/cmdline.txt"); + if (f.exists() && f.open(f.ReadOnly)) + { + cmdline = f.readAll().trimmed(); + f.close(); + } + + cmdline += _cmdline; + if (f.open(f.WriteOnly)) + { + f.write(cmdline); + f.close(); + } + else + { + emit error(tr("Error writing to cmdline.txt on FAT partition")); + return false; + } + } + + emit finalizing(); + +#ifdef Q_OS_LINUX + if (manualmount) + { + if (::access(devlower.constData(), W_OK) != 0) + { + #ifndef QT_NO_DBUS + UDisks2Api udisks2; + udisks2.unmountDrive(devlower); + #endif + } + else + { + QStringList args; + args << folder; + QProcess::execute("umount", args); + QDir d; + d.rmdir(folder); + } + } +#endif + +#ifndef Q_OS_WIN + ::sync(); +#endif + + return true; +} diff --git a/downloadthread.h b/downloadthread.h index 37f52a89..103d3e47 100644 --- a/downloadthread.h +++ b/downloadthread.h @@ -109,6 +109,11 @@ class DownloadThread : public QThread */ void setInputBufferSize(int len); + /* + * Enable image customization + */ + void setImageCustomization(const QByteArray &config, const QByteArray &cmdline, const QByteArray &firstrun); + /* * Thread safe download progress query functions */ @@ -142,6 +147,7 @@ class DownloadThread : public QThread qint64 _sectorsWritten(); void _closeFiles(); QByteArray _fileGetContentsTrimmed(const QString &filename); + bool _customizeImage(); /* * libcurl callbacks @@ -158,12 +164,12 @@ class DownloadThread : public QThread curl_off_t _startOffset; std::atomic _lastDlTotal, _lastDlNow, _verifyTotal, _lastVerifyNow, _bytesWritten; qint64 _sectorsStart; - QByteArray _url, _useragent, _buf, _filename, _lastError, _expectedHash; + QByteArray _url, _useragent, _buf, _filename, _lastError, _expectedHash, _config, _cmdline, _firstrun; char *_firstBlock; size_t _firstBlockSize; static QByteArray _proxy; static int _curlCount; - bool _cancelled, _successful, _verifyEnabled, _cacheEnabled; + bool _cancelled, _successful, _verifyEnabled, _cacheEnabled, _ejectEnabled; time_t _lastModified, _serverTime, _lastFailureTime; QElapsedTimer _timer; int _inputBufferSize; diff --git a/driveformatthread.cpp b/driveformatthread.cpp index c83113a9..bf34d036 100644 --- a/driveformatthread.cpp +++ b/driveformatthread.cpp @@ -130,7 +130,7 @@ void DriveFormatThread::run() #elif defined(Q_OS_LINUX) - if (::access(_device, W_OK) != 0) + if (::access(_device.constData(), W_OK) != 0) { /* Not running as root, try to outsource formatting to udisks2 */ diff --git a/imagewriter.cpp b/imagewriter.cpp index 0c54c258..46934e0d 100644 --- a/imagewriter.cpp +++ b/imagewriter.cpp @@ -7,17 +7,21 @@ #include "drivelistitem.h" #include "downloadextractthread.h" #include "dependencies/drivelist/src/drivelist.hpp" +#include "dependencies/sha256crypt/sha256crypt.h" #include "driveformatthread.h" #include "localfileextractthread.h" #include "downloadstatstelemetry.h" #include #include +#include #include #include #include #include +#include #include #include +#include #include #include #include @@ -29,11 +33,17 @@ #include #ifndef QT_NO_WIDGETS #include +#include #endif #ifdef Q_OS_WIN #include #include +#include +#ifndef WLAN_PROFILE_GET_PLAINTEXT_KEY +#define WLAN_PROFILE_GET_PLAINTEXT_KEY 4 +#endif + #include #include #endif @@ -55,8 +65,6 @@ ImageWriter::ImageWriter(QObject *parent) #ifdef Q_OS_WIN _taskbarButton = nullptr; - QProcess *p = new QProcess(this); - p->start("net stop ShellHWDetection"); #endif if (!_settings.isWritable() && !_settings.fileName().isEmpty()) @@ -104,10 +112,7 @@ ImageWriter::ImageWriter(QObject *parent) ImageWriter::~ImageWriter() { -#ifdef Q_OS_WIN - QProcess *p = new QProcess(this); - p->startDetached("net start ShellHWDetection"); -#endif + } void ImageWriter::setEngine(QQmlApplicationEngine *engine) @@ -216,6 +221,7 @@ void ImageWriter::startWrite() connect(_thread, SIGNAL(preparationStatusUpdate(QString)), SLOT(onPreparationStatusUpdate(QString))); _thread->setVerifyEnabled(_verifyEnabled); _thread->setUserAgent(QString("Mozilla/5.0 rpi-imager/%1").arg(constantVersion()).toUtf8()); + _thread->setImageCustomization(_config, _cmdline, _firstrun); if (!_expectedHash.isEmpty() && _cachedFileHash != _expectedHash && _cachingEnabled) { @@ -453,12 +459,24 @@ void ImageWriter::onSuccess() { stopProgressPolling(); emit success(); + +#ifndef QT_NO_WIDGETS + if (_settings.value("beep").toBool()) + { + QApplication::beep(); + } +#endif } void ImageWriter::onError(QString msg) { stopProgressPolling(); emit error(msg); + +#ifndef QT_NO_WIDGETS + if (_settings.value("beep").toBool()) + QApplication::beep(); +#endif } void ImageWriter::onFinalizing() @@ -620,6 +638,8 @@ void ImageWriter::onTimeSyncReply(QNetworkReply *reply) } reply->deleteLater(); +#else + Q_UNUSED(reply) #endif } @@ -698,6 +718,214 @@ QByteArray ImageWriter::getUsbSourceOSlist() #endif } +QString ImageWriter::getDefaultPubKey() +{ + QByteArray pubkey; + QFile pubfile(QDir::homePath()+"/.ssh/id_rsa.pub"); + + if (pubfile.exists() && pubfile.open(QFile::ReadOnly)) + { + pubkey = pubfile.readAll().trimmed(); + pubfile.close(); + } + + return pubkey; +} + +QString ImageWriter::getTimezone() +{ + return QTimeZone::systemTimeZoneId(); +} + +QStringList ImageWriter::getTimezoneList() +{ + QStringList timezones; + QFile f(":/timezones.txt"); + if ( f.open(f.ReadOnly) ) + { + timezones = QString(f.readAll()).split('\n'); + f.close(); + } + + return timezones; +} + +QStringList ImageWriter::getCountryList() +{ + QStringList countries; + QFile f(":/countries.txt"); + if ( f.open(f.ReadOnly) ) + { + countries = QString(f.readAll()).split('\n'); + f.close(); + } + + return countries; +} + +QString ImageWriter::getSSID() +{ + /* Qt used to have proper bearer management that was able to provide a list of + SSIDs, but since they retired it, resort to calling platform specific tools for now. + Minimal implementation that only gets the currently connected SSID */ + + QString program, regexpstr, ssid; + QStringList args; + QProcess proc; + +#ifdef Q_OS_WIN + program = "netsh"; + args << "wlan" << "show" << "interfaces"; + regexpstr = "[ \t]+SSID[ \t]*: (.+)"; +#else +#ifdef Q_OS_DARWIN + program = "/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport"; + args << "-I"; + regexpstr = "[ \t]+SSID: (.+)"; +#else + program = "iwgetid"; + args << "-r"; +#endif +#endif + + proc.start(program, args); + if (proc.waitForStarted(2000) && proc.waitForFinished(2000)) + { + if (regexpstr.isEmpty()) + { + ssid = proc.readAll().trimmed(); + } + else + { + QRegExp rx(regexpstr); + QList outputlines = proc.readAll().replace('\r', "").split('\n'); + + for (QByteArray line : outputlines) { + if (rx.indexIn(line) != -1) + { + ssid = rx.cap(1); + break; + } + } + } + } + + return ssid; +} + +QString ImageWriter::getPSK(const QString &ssid) +{ +#ifdef Q_OS_WIN + /* Windows allows retrieving wifi PSK */ + HANDLE h; + DWORD ret = 0; + DWORD supportedVersion = 0; + DWORD clientVersion = 2; + QString psk; + + if (WlanOpenHandle(clientVersion, NULL, &supportedVersion, &h) != ERROR_SUCCESS) + return QString(); + + PWLAN_INTERFACE_INFO_LIST ifList = NULL; + + if (WlanEnumInterfaces(h, NULL, &ifList) == ERROR_SUCCESS) + { + for (int i=0; i < ifList->dwNumberOfItems; i++) + { + PWLAN_PROFILE_INFO_LIST profileList = NULL; + + if (WlanGetProfileList(h, &ifList->InterfaceInfo[i].InterfaceGuid, + NULL, &profileList) == ERROR_SUCCESS) + { + for (int j=0; j < profileList->dwNumberOfItems; j++) + { + QString s = QString::fromWCharArray(profileList->ProfileInfo[j].strProfileName); + qDebug() << "Enumerating wifi profiles, SSID found:" << s << " looking for:" << ssid; + + if (s == ssid) { + DWORD flags = WLAN_PROFILE_GET_PLAINTEXT_KEY; + DWORD access = 0; + DWORD ret = 0; + LPWSTR xmlstr = NULL; + + if ( (ret = WlanGetProfile(h, &ifList->InterfaceInfo[i].InterfaceGuid, profileList->ProfileInfo[j].strProfileName, + NULL, &xmlstr, &flags, &access)) == ERROR_SUCCESS && xmlstr) + { + QString xml = QString::fromWCharArray(xmlstr); + qDebug() << "XML wifi profile:" << xml; + QRegExp rx("(.+)"); + if (rx.indexIn(xml) != -1) { + psk = rx.cap(1); + } + + WlanFreeMemory(xmlstr); + break; + } + } + } + } + + if (profileList) { + WlanFreeMemory(profileList); + } + } + } + + if (ifList) + WlanFreeMemory(ifList); + WlanCloseHandle(h, NULL); + + return psk; + +#else + Q_UNUSED(ssid) + return QString(); +#endif +} + +bool ImageWriter::getBoolSetting(const QString &key) +{ + /* Some keys have defaults */ + if (key == "telemetry") + return _settings.value(key, TELEMETRY_ENABLED_DEFAULT).toBool(); + else if (key == "eject") + return _settings.value(key, true).toBool(); + else + return _settings.value(key).toBool(); +} + +void ImageWriter::setSetting(const QString &key, const QVariant &value) +{ + _settings.setValue(key, value); + _settings.sync(); +} + +void ImageWriter::setImageCustomization(const QByteArray &config, const QByteArray &cmdline, const QByteArray &firstrun) +{ + _config = config; + _cmdline = cmdline; + _firstrun = firstrun; + + qDebug() << "Custom config.txt entries:" << config; + qDebug() << "Custom cmdline.txt entries:" << cmdline; + qDebug() << "Custom firstuse.sh:" << firstrun; +} + +QString ImageWriter::crypt(const QByteArray &password) +{ + QByteArray salt = "$5$"; + QByteArray saltchars = + "./0123456789ABCDEFGHIJKLMNOPQRST" + "UVWXYZabcdefghijklmnopqrstuvwxyz"; + std::mt19937 gen(static_cast(QDateTime::currentMSecsSinceEpoch())); + std::uniform_int_distribution<> uid(0, saltchars.length()-1); + + for (int i=0; i<10; i++) + salt += saltchars[uid(gen)]; + + return sha256_crypt(password.constData(), salt.constData()); +} + void MountUtilsLog(std::string msg) { qDebug() << "mountutils:" << msg.c_str(); } diff --git a/imagewriter.h b/imagewriter.h index 07c7730a..5d711652 100644 --- a/imagewriter.h +++ b/imagewriter.h @@ -92,6 +92,20 @@ class ImageWriter : public QObject /* Returns a json formatted list of the OS images found on USB stick */ Q_INVOKABLE QByteArray getUsbSourceOSlist(); + /* Functions to collect information from computer running imager to make image customization easier */ + Q_INVOKABLE QString getDefaultPubKey(); + Q_INVOKABLE QString getTimezone(); + Q_INVOKABLE QStringList getTimezoneList(); + Q_INVOKABLE QStringList getCountryList(); + Q_INVOKABLE QString getSSID(); + Q_INVOKABLE QString getPSK(const QString &ssid); + + Q_INVOKABLE bool getBoolSetting(const QString &key); + Q_INVOKABLE void setSetting(const QString &key, const QVariant &value); + Q_INVOKABLE void setImageCustomization(const QByteArray &config, const QByteArray &cmdline, const QByteArray &firstrun); + + Q_INVOKABLE QString crypt(const QByteArray &password); + signals: /* We are emiting signals with QVariant as parameters because QML likes it that way */ @@ -124,7 +138,7 @@ protected slots: protected: QUrl _src, _repo; QString _dst, _cacheFileName, _parentCategory, _osName; - QByteArray _expectedHash, _cachedFileHash; + QByteArray _expectedHash, _cachedFileHash, _cmdline, _config, _firstrun; quint64 _downloadLen, _extrLen, _devLen, _dlnow, _verifynow; DriveListModel _drivelist; QQmlApplicationEngine *_engine; diff --git a/license.txt b/license.txt index b3e53bc8..065dbc90 100644 --- a/license.txt +++ b/license.txt @@ -30,6 +30,7 @@ This software depends on the following third-party components covered by open so - Balena.io mountutils (Apache license) - libarchive (new BSD license) - liblzma (public domain) +- cryptsha256 (public domain) - zlib (zlib license) - libcurl (libcurl license) - fat32format.exe (GPL license) @@ -1010,6 +1011,13 @@ liblzma license - liblzma is in the public domain. +== +cryptsha256 license +== + +SHA256-based Unix crypt implementation. +Released into the Public Domain by Ulrich Drepper + == zlib license == diff --git a/linux/udisks2api.cpp b/linux/udisks2api.cpp index 97f25a64..c2e90783 100644 --- a/linux/udisks2api.cpp +++ b/linux/udisks2api.cpp @@ -170,3 +170,40 @@ bool UDisks2Api::formatDrive(const QString &device, bool mountAfterwards) return true; } + +QString UDisks2Api::mountDevice(const QString &device) +{ + QString devpath = _resolveDevice(device); + if (devpath.isEmpty()) + return QString(); + + QDBusInterface filesystem("org.freedesktop.UDisks2", devpath, + "org.freedesktop.UDisks2.Filesystem", QDBusConnection::systemBus()); + QVariantMap mountOptions; + + for (int attempt = 0; attempt < 10; attempt++) + { + qDebug() << "Mounting partition"; + QDBusReply mp = filesystem.call("Mount", mountOptions); + + if (mp.isValid()) + { + qDebug() << "Mounted file system at:" << mp; + return mp; + } + + QThread::sleep(1); + } + + qDebug() << "Failed to mount file system."; + return QString(); +} + +void UDisks2Api::unmountDrive(const QString &device) +{ + QString devpath = _resolveDevice(device); + if (devpath.isEmpty()) + return; + + _unmountDrive(devpath); +} diff --git a/linux/udisks2api.h b/linux/udisks2api.h index 8fe1ddef..e3e784b0 100644 --- a/linux/udisks2api.h +++ b/linux/udisks2api.h @@ -16,6 +16,8 @@ class UDisks2Api : public QObject explicit UDisks2Api(QObject *parent = nullptr); int authOpen(const QString &device, const QString &mode = "rw"); bool formatDrive(const QString &device, bool mountAfterwards = true); + QString mountDevice(const QString &device); + void unmountDrive(const QString &device); protected: QString _resolveDevice(const QString &device); diff --git a/main.qml b/main.qml index 99e8f277..60fe622b 100644 --- a/main.qml +++ b/main.qml @@ -43,6 +43,14 @@ ApplicationWindow { } } + Shortcut { + sequences: ["Shift+Ctrl+X", "Shift+Meta+X"] + context: Qt.ApplicationShortcut + onActivated: { + optionspopup.openPopup() + } + } + ColumnLayout { id: bg spacing: 0 @@ -778,6 +786,10 @@ ApplicationWindow { } } + OptionsPopup { + id: optionspopup + } + /* Utility functions */ function httpRequest(url, callback) { var xhr = new XMLHttpRequest(); diff --git a/qml.qrc b/qml.qrc index 2339a5be..be982deb 100644 --- a/qml.qrc +++ b/qml.qrc @@ -22,5 +22,8 @@ icons/cat_misc_utility_images.png icons/cat_media_players.png icons/cat_emulation_and_games.png + OptionsPopup.qml + countries.txt + timezones.txt diff --git a/timezones.txt b/timezones.txt new file mode 100644 index 00000000..a4534335 --- /dev/null +++ b/timezones.txt @@ -0,0 +1,541 @@ +Africa/Abidjan +Africa/Accra +Africa/Addis_Ababa +Africa/Algiers +Africa/Asmara +Africa/Asmera +Africa/Bamako +Africa/Bangui +Africa/Banjul +Africa/Bissau +Africa/Blantyre +Africa/Brazzaville +Africa/Bujumbura +Africa/Cairo +Africa/Casablanca +Africa/Ceuta +Africa/Conakry +Africa/Dakar +Africa/Dar_es_Salaam +Africa/Djibouti +Africa/Douala +Africa/El_Aaiun +Africa/Freetown +Africa/Gaborone +Africa/Harare +Africa/Johannesburg +Africa/Juba +Africa/Kampala +Africa/Khartoum +Africa/Kigali +Africa/Kinshasa +Africa/Lagos +Africa/Libreville +Africa/Lome +Africa/Luanda +Africa/Lubumbashi +Africa/Lusaka +Africa/Malabo +Africa/Maputo +Africa/Maseru +Africa/Mbabane +Africa/Mogadishu +Africa/Monrovia +Africa/Nairobi +Africa/Ndjamena +Africa/Niamey +Africa/Nouakchott +Africa/Ouagadougou +Africa/Porto-Novo +Africa/Sao_Tome +Africa/Timbuktu +Africa/Tripoli +Africa/Tunis +Africa/Windhoek +America/Adak +America/Anchorage +America/Anguilla +America/Antigua +America/Araguaina +America/Argentina +America/Argentina/Buenos_Aires +America/Argentina/Catamarca +America/Argentina/ComodRivadavia +America/Argentina/Cordoba +America/Argentina/Jujuy +America/Argentina/La_Rioja +America/Argentina/Mendoza +America/Argentina/Rio_Gallegos +America/Argentina/Salta +America/Argentina/San_Juan +America/Argentina/San_Luis +America/Argentina/Tucuman +America/Argentina/Ushuaia +America/Aruba +America/Asuncion +America/Atikokan +America/Atka +America/Bahia +America/Bahia_Banderas +America/Barbados +America/Belem +America/Belize +America/Blanc-Sablon +America/Boa_Vista +America/Bogota +America/Boise +America/Buenos_Aires +America/Cambridge_Bay +America/Campo_Grande +America/Cancun +America/Caracas +America/Catamarca +America/Cayenne +America/Cayman +America/Chicago +America/Chihuahua +America/Coral_Harbour +America/Cordoba +America/Costa_Rica +America/Creston +America/Cuiaba +America/Curacao +America/Danmarkshavn +America/Dawson +America/Dawson_Creek +America/Denver +America/Detroit +America/Dominica +America/Edmonton +America/Eirunepe +America/El_Salvador +America/Ensenada +America/Fortaleza +America/Fort_Wayne +America/Glace_Bay +America/Godthab +America/Goose_Bay +America/Grand_Turk +America/Grenada +America/Guadeloupe +America/Guatemala +America/Guayaquil +America/Guyana +America/Halifax +America/Havana +America/Hermosillo +America/Indiana +America/Indiana/Indianapolis +America/Indiana/Knox +America/Indiana/Marengo +America/Indiana/Petersburg +America/Indianapolis +America/Indiana/Tell_City +America/Indiana/Vevay +America/Indiana/Vincennes +America/Indiana/Winamac +America/Inuvik +America/Iqaluit +America/Jamaica +America/Jujuy +America/Juneau +America/Kentucky +America/Kentucky/Louisville +America/Kentucky/Monticello +America/Knox_IN +America/Kralendijk +America/La_Paz +America/Lima +America/Los_Angeles +America/Louisville +America/Lower_Princes +America/Maceio +America/Managua +America/Manaus +America/Marigot +America/Martinique +America/Matamoros +America/Mazatlan +America/Mendoza +America/Menominee +America/Merida +America/Metlakatla +America/Mexico_City +America/Miquelon +America/Moncton +America/Monterrey +America/Montevideo +America/Montreal +America/Montserrat +America/Nassau +America/New_York +America/Nipigon +America/Nome +America/Noronha +America/North_Dakota +America/North_Dakota/Beulah +America/North_Dakota/Center +America/North_Dakota/New_Salem +America/Ojinaga +America/Panama +America/Pangnirtung +America/Paramaribo +America/Phoenix +America/Port-au-Prince +America/Porto_Acre +America/Port_of_Spain +America/Porto_Velho +America/Puerto_Rico +America/Rainy_River +America/Rankin_Inlet +America/Recife +America/Regina +America/Resolute +America/Rio_Branco +America/Rosario +America/Santa_Isabel +America/Santarem +America/Santiago +America/Santo_Domingo +America/Sao_Paulo +America/Scoresbysund +America/Shiprock +America/Sitka +America/St_Barthelemy +America/St_Johns +America/St_Kitts +America/St_Lucia +America/St_Thomas +America/St_Vincent +America/Swift_Current +America/Tegucigalpa +America/Thule +America/Thunder_Bay +America/Tijuana +America/Toronto +America/Tortola +America/Vancouver +America/Virgin +America/Whitehorse +America/Winnipeg +America/Yakutat +America/Yellowknife +Antarctica/Casey +Antarctica/Davis +Antarctica/DumontDUrville +Antarctica/Macquarie +Antarctica/Mawson +Antarctica/McMurdo +Antarctica/Palmer +Antarctica/Rothera +Antarctica/South_Pole +Antarctica/Syowa +Antarctica/Vostok +Arctic/Longyearbyen +Asia/Aden +Asia/Almaty +Asia/Amman +Asia/Anadyr +Asia/Aqtau +Asia/Aqtobe +Asia/Ashgabat +Asia/Ashkhabad +Asia/Baghdad +Asia/Bahrain +Asia/Baku +Asia/Bangkok +Asia/Beirut +Asia/Bishkek +Asia/Brunei +Asia/Calcutta +Asia/Choibalsan +Asia/Chongqing +Asia/Chungking +Asia/Colombo +Asia/Dacca +Asia/Damascus +Asia/Dhaka +Asia/Dili +Asia/Dubai +Asia/Dushanbe +Asia/Gaza +Asia/Harbin +Asia/Hebron +Asia/Ho_Chi_Minh +Asia/Hong_Kong +Asia/Hovd +Asia/Irkutsk +Asia/Istanbul +Asia/Jakarta +Asia/Jayapura +Asia/Jerusalem +Asia/Kabul +Asia/Kamchatka +Asia/Karachi +Asia/Kashgar +Asia/Kathmandu +Asia/Katmandu +Asia/Kolkata +Asia/Krasnoyarsk +Asia/Kuala_Lumpur +Asia/Kuching +Asia/Kuwait +Asia/Macao +Asia/Macau +Asia/Magadan +Asia/Makassar +Asia/Manila +Asia/Muscat +Asia/Nicosia +Asia/Novokuznetsk +Asia/Novosibirsk +Asia/Omsk +Asia/Oral +Asia/Phnom_Penh +Asia/Pontianak +Asia/Pyongyang +Asia/Qatar +Asia/Qyzylorda +Asia/Rangoon +Asia/Riyadh +Asia/Riyadh87 +Asia/Riyadh88 +Asia/Riyadh89 +Asia/Saigon +Asia/Sakhalin +Asia/Samarkand +Asia/Seoul +Asia/Shanghai +Asia/Singapore +Asia/Taipei +Asia/Tashkent +Asia/Tbilisi +Asia/Tehran +Asia/Tel_Aviv +Asia/Thimbu +Asia/Thimphu +Asia/Tokyo +Asia/Ujung_Pandang +Asia/Ulaanbaatar +Asia/Ulan_Bator +Asia/Urumqi +Asia/Vientiane +Asia/Vladivostok +Asia/Yakutsk +Asia/Yekaterinburg +Asia/Yerevan +Atlantic/Azores +Atlantic/Bermuda +Atlantic/Canary +Atlantic/Cape_Verde +Atlantic/Faeroe +Atlantic/Faroe +Atlantic/Jan_Mayen +Atlantic/Madeira +Atlantic/Reykjavik +Atlantic/South_Georgia +Atlantic/Stanley +Atlantic/St_Helena +Australia/ACT +Australia/Adelaide +Australia/Brisbane +Australia/Broken_Hill +Australia/Canberra +Australia/Currie +Australia/Darwin +Australia/Eucla +Australia/Hobart +Australia/LHI +Australia/Lindeman +Australia/Lord_Howe +Australia/Melbourne +Australia/North +Australia/NSW +Australia/Perth +Australia/Queensland +Australia/South +Australia/Sydney +Australia/Tasmania +Australia/Victoria +Australia/West +Australia/Yancowinna +Brazil/Acre +Brazil/DeNoronha +Brazil/East +Brazil/West +Canada/Atlantic +Canada/Central +Canada/Eastern +Canada/East-Saskatchewan +Canada/Mountain +Canada/Newfoundland +Canada/Pacific +Canada/Saskatchewan +Canada/Yukon +Chile/Continental +Chile/EasterIsland +Etc/GMT +Etc/GMT0 +Etc/GMT-0 +Etc/GMT+0 +Etc/GMT-1 +Etc/GMT+1 +Etc/GMT-10 +Etc/GMT+10 +Etc/GMT-11 +Etc/GMT+11 +Etc/GMT-12 +Etc/GMT+12 +Etc/GMT-13 +Etc/GMT-14 +Etc/GMT-2 +Etc/GMT+2 +Etc/GMT-3 +Etc/GMT+3 +Etc/GMT-4 +Etc/GMT+4 +Etc/GMT-5 +Etc/GMT+5 +Etc/GMT-6 +Etc/GMT+6 +Etc/GMT-7 +Etc/GMT+7 +Etc/GMT-8 +Etc/GMT+8 +Etc/GMT-9 +Etc/GMT+9 +Etc/Greenwich +Etc/UCT +Etc/Universal +Etc/UTC +Etc/Zulu +Europe/Amsterdam +Europe/Andorra +Europe/Athens +Europe/Belfast +Europe/Belgrade +Europe/Berlin +Europe/Bratislava +Europe/Brussels +Europe/Bucharest +Europe/Budapest +Europe/Chisinau +Europe/Copenhagen +Europe/Dublin +Europe/Gibraltar +Europe/Guernsey +Europe/Helsinki +Europe/Isle_of_Man +Europe/Istanbul +Europe/Jersey +Europe/Kaliningrad +Europe/Kiev +Europe/Lisbon +Europe/Ljubljana +Europe/London +Europe/Luxembourg +Europe/Madrid +Europe/Malta +Europe/Mariehamn +Europe/Minsk +Europe/Monaco +Europe/Moscow +Europe/Nicosia +Europe/Oslo +Europe/Paris +Europe/Podgorica +Europe/Prague +Europe/Riga +Europe/Rome +Europe/Samara +Europe/San_Marino +Europe/Sarajevo +Europe/Simferopol +Europe/Skopje +Europe/Sofia +Europe/Stockholm +Europe/Tallinn +Europe/Tirane +Europe/Tiraspol +Europe/Uzhgorod +Europe/Vaduz +Europe/Vatican +Europe/Vienna +Europe/Vilnius +Europe/Volgograd +Europe/Warsaw +Europe/Zagreb +Europe/Zaporozhye +Europe/Zurich +Indian/Antananarivo +Indian/Chagos +Indian/Christmas +Indian/Cocos +Indian/Comoro +Indian/Kerguelen +Indian/Mahe +Indian/Maldives +Indian/Mauritius +Indian/Mayotte +Indian/Reunion +Mexico/BajaNorte +Mexico/BajaSur +Mexico/General +Mideast/Riyadh87 +Mideast/Riyadh88 +Mideast/Riyadh89 +Pacific/Apia +Pacific/Auckland +Pacific/Chatham +Pacific/Chuuk +Pacific/Easter +Pacific/Efate +Pacific/Enderbury +Pacific/Fakaofo +Pacific/Fiji +Pacific/Funafuti +Pacific/Galapagos +Pacific/Gambier +Pacific/Guadalcanal +Pacific/Guam +Pacific/Honolulu +Pacific/Johnston +Pacific/Kiritimati +Pacific/Kosrae +Pacific/Kwajalein +Pacific/Majuro +Pacific/Marquesas +Pacific/Midway +Pacific/Nauru +Pacific/Niue +Pacific/Norfolk +Pacific/Noumea +Pacific/Pago_Pago +Pacific/Palau +Pacific/Pitcairn +Pacific/Pohnpei +Pacific/Ponape +Pacific/Port_Moresby +Pacific/Rarotonga +Pacific/Saipan +Pacific/Samoa +Pacific/Tahiti +Pacific/Tarawa +Pacific/Tongatapu +Pacific/Truk +Pacific/Wake +Pacific/Wallis +Pacific/Yap +US/Alaska +US/Aleutian +US/Arizona +US/Central +US/Eastern +US/East-Indiana +US/Hawaii +US/Indiana-Starke +US/Michigan +US/Mountain +US/Pacific +US/Pacific-New +US/Samoa