Changeset View
Changeset View
Standalone View
Standalone View
src/util/externalcommandhelper.cpp
Show All 33 Lines | |||||
34 | * ExternalCommand class opens a DBus service that we monitor for changes. | 34 | * ExternalCommand class opens a DBus service that we monitor for changes. | ||
35 | * If helper is not busy then it exits when the client services gets | 35 | * If helper is not busy then it exits when the client services gets | ||
36 | * unregistered. Otherwise, | 36 | * unregistered. Otherwise, | ||
37 | * we wait for the current job to finish before exiting, so even in case | 37 | * we wait for the current job to finish before exiting, so even in case | ||
38 | * of main application crash, we do not leave partially moved data. | 38 | * of main application crash, we do not leave partially moved data. | ||
39 | * | 39 | * | ||
40 | * This helper also starts another DBus interface where it listens to | 40 | * This helper also starts another DBus interface where it listens to | ||
41 | * command execution requests from the application that started the helper. | 41 | * command execution requests from the application that started the helper. | ||
42 | * These requests are validated using public key cryptography, to prevent | 42 | * | ||
43 | * other unprivileged applications from gaining root privileges. | | |||
44 | */ | 43 | */ | ||
45 | ActionReply ExternalCommandHelper::init(const QVariantMap& args) | 44 | ActionReply ExternalCommandHelper::init(const QVariantMap& args) | ||
46 | { | 45 | { | ||
46 | Q_UNUSED(args) | ||||
47 | | ||||
47 | ActionReply reply; | 48 | ActionReply reply; | ||
48 | 49 | | |||
49 | if (!QDBusConnection::systemBus().isConnected() || !QDBusConnection::systemBus().registerService(QStringLiteral("org.kde.kpmcore.helperinterface")) || | 50 | if (!QDBusConnection::systemBus().isConnected() || !QDBusConnection::systemBus().registerService(QStringLiteral("org.kde.kpmcore.helperinterface")) || | ||
50 | !QDBusConnection::systemBus().registerObject(QStringLiteral("/Helper"), this, QDBusConnection::ExportAllSlots)) { | 51 | !QDBusConnection::systemBus().registerObject(QStringLiteral("/Helper"), this, QDBusConnection::ExportAllSlots)) { | ||
51 | qWarning() << QDBusConnection::systemBus().lastError().message(); | 52 | qWarning() << QDBusConnection::systemBus().lastError().message(); | ||
52 | reply.addData(QStringLiteral("success"), false); | 53 | reply.addData(QStringLiteral("success"), false); | ||
53 | return reply; | 54 | return reply; | ||
54 | } | 55 | } | ||
55 | 56 | | |||
56 | m_publicKey = QCA::PublicKey::fromDER(args[QStringLiteral("pubkey")].toByteArray()); | | |||
57 | | ||||
58 | m_loop = std::make_unique<QEventLoop>(); | 57 | m_loop = std::make_unique<QEventLoop>(); | ||
59 | HelperSupport::progressStep(QVariantMap()); | 58 | HelperSupport::progressStep(QVariantMap()); | ||
60 | 59 | | |||
61 | // End the loop and return only once the client is done using us. | 60 | // End the loop and return only once the client is done using us. | ||
62 | auto serviceWatcher = | 61 | auto serviceWatcher = | ||
63 | new QDBusServiceWatcher(QStringLiteral("org.kde.kpmcore.applicationinterface"), | 62 | new QDBusServiceWatcher(QStringLiteral("org.kde.kpmcore.applicationinterface"), | ||
64 | QDBusConnection::systemBus(), | 63 | QDBusConnection::systemBus(), | ||
65 | QDBusServiceWatcher::WatchForUnregistration, | 64 | QDBusServiceWatcher::WatchForUnregistration, | ||
66 | this); | 65 | this); | ||
67 | connect(serviceWatcher, &QDBusServiceWatcher::serviceUnregistered, | 66 | connect(serviceWatcher, &QDBusServiceWatcher::serviceUnregistered, | ||
68 | [this]() { | 67 | [this]() { | ||
69 | m_loop->exit(); | 68 | m_loop->exit(); | ||
70 | }); | 69 | }); | ||
71 | 70 | | |||
72 | m_loop->exec(); | 71 | m_loop->exec(); | ||
73 | reply.addData(QStringLiteral("success"), true); | 72 | reply.addData(QStringLiteral("success"), true); | ||
74 | 73 | | |||
75 | return reply; | 74 | return reply; | ||
76 | } | 75 | } | ||
77 | 76 | | |||
78 | /** Generates cryptographic nonce | | |||
79 | * @return nonce | | |||
80 | */ | | |||
81 | quint64 ExternalCommandHelper::getNonce() | | |||
82 | { | | |||
83 | const quint64 nonce = m_Generator.generate(); | | |||
84 | m_Nonces.insert(nonce); | | |||
85 | return nonce; | | |||
86 | } | | |||
87 | | ||||
88 | /** Reads the given number of bytes from the sourceDevice into the given buffer. | 77 | /** Reads the given number of bytes from the sourceDevice into the given buffer. | ||
89 | @param sourceDevice device or file to read from | 78 | @param sourceDevice device or file to read from | ||
90 | @param buffer buffer to store the bytes read in | 79 | @param buffer buffer to store the bytes read in | ||
91 | @param offset offset where to begin reading | 80 | @param offset offset where to begin reading | ||
92 | @param size the number of bytes to read | 81 | @param size the number of bytes to read | ||
93 | @return true on success | 82 | @return true on success | ||
94 | */ | 83 | */ | ||
95 | bool ExternalCommandHelper::readData(const QString& sourceDevice, QByteArray& buffer, const qint64 offset, const qint64 size) | 84 | bool ExternalCommandHelper::readData(const QString& sourceDevice, QByteArray& buffer, const qint64 offset, const qint64 size) | ||
▲ Show 20 Lines • Show All 44 Lines • ▼ Show 20 Line(s) | 128 | if (device.write(buffer) != buffer.size()) { | |||
140 | qCritical() << xi18n("Could not write to device <filename>%1</filename>.", targetDevice); | 129 | qCritical() << xi18n("Could not write to device <filename>%1</filename>.", targetDevice); | ||
141 | return false; | 130 | return false; | ||
142 | } | 131 | } | ||
143 | 132 | | |||
144 | return true; | 133 | return true; | ||
145 | } | 134 | } | ||
146 | 135 | | |||
147 | // If targetDevice is empty then return QByteArray with data that was read from disk. | 136 | // If targetDevice is empty then return QByteArray with data that was read from disk. | ||
148 | QVariantMap ExternalCommandHelper::copyblocks(const QByteArray& signature, const quint64 nonce, const QString& sourceDevice, const qint64 sourceFirstByte, const qint64 sourceLength, const QString& targetDevice, const qint64 targetFirstByte, const qint64 blockSize) | 137 | QVariantMap ExternalCommandHelper::copyblocks(const QString& sourceDevice, const qint64 sourceFirstByte, const qint64 sourceLength, const QString& targetDevice, const qint64 targetFirstByte, const qint64 blockSize) | ||
149 | { | 138 | { | ||
150 | QVariantMap reply; | 139 | QVariantMap reply; | ||
151 | reply[QStringLiteral("success")] = true; | 140 | reply[QStringLiteral("success")] = true; | ||
152 | 141 | | |||
153 | if (m_Nonces.find(nonce) != m_Nonces.end()) | | |||
154 | m_Nonces.erase( nonce ); | | |||
155 | else { | | |||
156 | reply[QStringLiteral("success")] = false; | | |||
157 | return reply; | | |||
158 | } | | |||
159 | | ||||
160 | QByteArray request; | | |||
161 | | ||||
162 | request.setNum(nonce); | | |||
163 | request.append(sourceDevice.toUtf8()); | | |||
164 | request.append(QByteArray::number(sourceFirstByte)); | | |||
165 | request.append(QByteArray::number(sourceLength)); | | |||
166 | request.append(targetDevice.toUtf8()); | | |||
167 | request.append(QByteArray::number(targetFirstByte)); | | |||
168 | request.append(QByteArray::number(blockSize)); | | |||
169 | | ||||
170 | QByteArray hash = QCryptographicHash::hash(request, QCryptographicHash::Sha512); | | |||
171 | if (!m_publicKey.verifyMessage(hash, signature, QCA::EMSA3_Raw)) { | | |||
172 | qCritical() << xi18n("Invalid cryptographic signature"); | | |||
173 | reply[QStringLiteral("success")] = false; | | |||
174 | return reply; | | |||
175 | } | | |||
176 | | ||||
177 | const qint64 blocksToCopy = sourceLength / blockSize; | 142 | const qint64 blocksToCopy = sourceLength / blockSize; | ||
178 | qint64 readOffset = sourceFirstByte; | 143 | qint64 readOffset = sourceFirstByte; | ||
179 | qint64 writeOffset = targetFirstByte; | 144 | qint64 writeOffset = targetFirstByte; | ||
180 | qint32 copyDirection = 1; | 145 | qint32 copyDirection = 1; | ||
181 | 146 | | |||
182 | if (targetFirstByte > sourceFirstByte) { | 147 | if (targetFirstByte > sourceFirstByte) { | ||
183 | readOffset = sourceFirstByte + sourceLength - blockSize; | 148 | readOffset = sourceFirstByte + sourceLength - blockSize; | ||
184 | writeOffset = targetFirstByte + sourceLength - blockSize; | 149 | writeOffset = targetFirstByte + sourceLength - blockSize; | ||
▲ Show 20 Lines • Show All 68 Lines • ▼ Show 20 Line(s) | |||||
253 | 218 | | |||
254 | report[QStringLiteral("report")] = xi18ncp("@info:progress argument 2 is a string such as 7 bytes (localized accordingly)", "Copying 1 block (%2) finished.", "Copying %1 blocks (%2) finished.", blocksCopied, i18np("1 byte", "%1 bytes", bytesWritten)); | 219 | report[QStringLiteral("report")] = xi18ncp("@info:progress argument 2 is a string such as 7 bytes (localized accordingly)", "Copying 1 block (%2) finished.", "Copying %1 blocks (%2) finished.", blocksCopied, i18np("1 byte", "%1 bytes", bytesWritten)); | ||
255 | HelperSupport::progressStep(report); | 220 | HelperSupport::progressStep(report); | ||
256 | 221 | | |||
257 | reply[QStringLiteral("success")] = rval; | 222 | reply[QStringLiteral("success")] = rval; | ||
258 | return reply; | 223 | return reply; | ||
259 | } | 224 | } | ||
260 | 225 | | |||
261 | bool ExternalCommandHelper::writeData(const QByteArray& signature, const quint64 nonce, const QByteArray& buffer, const QString& targetDevice, const qint64 targetFirstByte) | 226 | bool ExternalCommandHelper::writeData(const QByteArray& buffer, const QString& targetDevice, const qint64 targetFirstByte) | ||
262 | { | 227 | { | ||
263 | if (m_Nonces.find(nonce) != m_Nonces.end()) | | |||
264 | m_Nonces.erase( nonce ); | | |||
265 | else | | |||
266 | return false; | | |||
267 | | ||||
268 | QByteArray request; | | |||
269 | request.setNum(nonce); | | |||
270 | request.append(buffer); | | |||
271 | request.append(targetDevice.toUtf8()); | | |||
272 | request.append(QByteArray::number(targetFirstByte)); | | |||
273 | | ||||
274 | // Do not allow using this helper for writing to arbitrary location | 228 | // Do not allow using this helper for writing to arbitrary location | ||
275 | if ( targetDevice.left(5) != QStringLiteral("/dev/") && !targetDevice.contains(QStringLiteral("/etc/fstab"))) | 229 | if ( targetDevice.left(5) != QStringLiteral("/dev/") && !targetDevice.contains(QStringLiteral("/etc/fstab"))) | ||
276 | return false; | 230 | return false; | ||
277 | 231 | | |||
278 | QByteArray hash = QCryptographicHash::hash(request, QCryptographicHash::Sha512); | | |||
279 | if (!m_publicKey.verifyMessage(hash, signature, QCA::EMSA3_Raw)) { | | |||
280 | qCritical() << xi18n("Invalid cryptographic signature"); | | |||
281 | return false; | | |||
282 | } | | |||
283 | | ||||
284 | return writeData(targetDevice, buffer, targetFirstByte); | 232 | return writeData(targetDevice, buffer, targetFirstByte); | ||
285 | } | 233 | } | ||
286 | 234 | | |||
287 | 235 | | |||
288 | QVariantMap ExternalCommandHelper::start(const QByteArray& signature, const quint64 nonce, const QString& command, const QStringList& arguments, const QByteArray& input, const int processChannelMode) | 236 | QVariantMap ExternalCommandHelper::start(const QString& command, const QStringList& arguments, const QByteArray& input, const int processChannelMode) | ||
289 | { | 237 | { | ||
290 | QTextCodec::setCodecForLocale(QTextCodec::codecForName("UTF-8")); | 238 | QTextCodec::setCodecForLocale(QTextCodec::codecForName("UTF-8")); | ||
291 | QVariantMap reply; | 239 | QVariantMap reply; | ||
292 | reply[QStringLiteral("success")] = true; | 240 | reply[QStringLiteral("success")] = true; | ||
293 | 241 | | |||
294 | if (m_Nonces.find(nonce) != m_Nonces.end()) | | |||
295 | m_Nonces.erase( nonce ); | | |||
296 | else { | | |||
297 | reply[QStringLiteral("success")] = false; | | |||
298 | return reply; | | |||
299 | } | | |||
300 | | ||||
301 | if (command.isEmpty()) { | 242 | if (command.isEmpty()) { | ||
302 | reply[QStringLiteral("success")] = false; | 243 | reply[QStringLiteral("success")] = false; | ||
303 | return reply; | 244 | return reply; | ||
304 | } | 245 | } | ||
305 | 246 | | |||
306 | QByteArray request; | | |||
307 | request.setNum(nonce); | | |||
308 | request.append(command.toUtf8()); | | |||
309 | for (const auto &argument : arguments) | | |||
310 | request.append(argument.toUtf8()); | | |||
311 | request.append(input); | | |||
312 | request.append(processChannelMode); | | |||
313 | QByteArray hash = QCryptographicHash::hash(request, QCryptographicHash::Sha512); | | |||
314 | if (!m_publicKey.verifyMessage(hash, signature, QCA::EMSA3_Raw)) { | | |||
315 | qCritical() << xi18n("Invalid cryptographic signature"); | | |||
316 | reply[QStringLiteral("success")] = false; | | |||
317 | return reply; | | |||
318 | } | | |||
319 | | ||||
320 | // Compare with command whitelist | 247 | // Compare with command whitelist | ||
321 | QString basename = command.mid(command.lastIndexOf(QLatin1Char('/')) + 1); | 248 | QString basename = command.mid(command.lastIndexOf(QLatin1Char('/')) + 1); | ||
322 | if (std::find(std::begin(allowedCommands), std::end(allowedCommands), basename) == std::end(allowedCommands)) { | 249 | if (std::find(std::begin(allowedCommands), std::end(allowedCommands), basename) == std::end(allowedCommands)) { | ||
323 | // TODO: notify the user | 250 | // TODO: notify the user | ||
324 | m_loop->exit(); | 251 | m_loop->exit(); | ||
325 | reply[QStringLiteral("success")] = false; | 252 | reply[QStringLiteral("success")] = false; | ||
326 | return reply; | 253 | return reply; | ||
327 | } | 254 | } | ||
328 | 255 | | |||
329 | // connect(&cmd, &QProcess::readyReadStandardOutput, this, &ExternalCommandHelper::onReadOutput); | 256 | // connect(&cmd, &QProcess::readyReadStandardOutput, this, &ExternalCommandHelper::onReadOutput); | ||
330 | 257 | | |||
331 | m_cmd.setEnvironment( { QStringLiteral("LVM_SUPPRESS_FD_WARNINGS=1") } ); | 258 | m_cmd.setEnvironment( { QStringLiteral("LVM_SUPPRESS_FD_WARNINGS=1") } ); | ||
332 | m_cmd.setProcessChannelMode(static_cast<QProcess::ProcessChannelMode>(processChannelMode)); | 259 | m_cmd.setProcessChannelMode(static_cast<QProcess::ProcessChannelMode>(processChannelMode)); | ||
333 | m_cmd.start(command, arguments); | 260 | m_cmd.start(command, arguments); | ||
334 | m_cmd.write(input); | 261 | m_cmd.write(input); | ||
335 | m_cmd.closeWriteChannel(); | 262 | m_cmd.closeWriteChannel(); | ||
336 | m_cmd.waitForFinished(-1); | 263 | m_cmd.waitForFinished(-1); | ||
337 | QByteArray output = m_cmd.readAllStandardOutput(); | 264 | QByteArray output = m_cmd.readAllStandardOutput(); | ||
338 | reply[QStringLiteral("output")] = output; | 265 | reply[QStringLiteral("output")] = output; | ||
339 | reply[QStringLiteral("exitCode")] = m_cmd.exitCode(); | 266 | reply[QStringLiteral("exitCode")] = m_cmd.exitCode(); | ||
340 | 267 | | |||
341 | return reply; | 268 | return reply; | ||
342 | } | 269 | } | ||
343 | 270 | | |||
344 | void ExternalCommandHelper::exit(const QByteArray& signature, const quint64 nonce) | 271 | void ExternalCommandHelper::exit() | ||
345 | { | 272 | { | ||
346 | QByteArray request; | | |||
347 | if (m_Nonces.find(nonce) == m_Nonces.end()) | | |||
348 | return; | | |||
349 | | ||||
350 | request.setNum(nonce); | | |||
351 | QByteArray hash = QCryptographicHash::hash(request, QCryptographicHash::Sha512); | | |||
352 | if (!m_publicKey.verifyMessage(hash, signature, QCA::EMSA3_Raw)) { | | |||
353 | qCritical() << xi18n("Invalid cryptographic signature"); | | |||
354 | return; | | |||
355 | } | | |||
356 | | ||||
357 | m_loop->exit(); | 273 | m_loop->exit(); | ||
358 | 274 | | |||
359 | QDBusConnection::systemBus().unregisterObject(QStringLiteral("/Helper")); | 275 | QDBusConnection::systemBus().unregisterObject(QStringLiteral("/Helper")); | ||
360 | QDBusConnection::systemBus().unregisterService(QStringLiteral("org.kde.kpmcore.helperinterface")); | 276 | QDBusConnection::systemBus().unregisterService(QStringLiteral("org.kde.kpmcore.helperinterface")); | ||
361 | } | 277 | } | ||
362 | 278 | | |||
363 | void ExternalCommandHelper::onReadOutput() | 279 | void ExternalCommandHelper::onReadOutput() | ||
364 | { | 280 | { | ||
365 | // const QByteArray s = cmd.readAllStandardOutput(); | 281 | /* const QByteArray s = cmd.readAllStandardOutput(); | ||
366 | 282 | | |||
367 | // if(output.length() > 10*1024*1024) { // prevent memory overflow for badly corrupted file systems | 283 | if(output.length() > 10*1024*1024) { // prevent memory overflow for badly corrupted file systems | ||
368 | // if (report()) | 284 | if (report()) | ||
369 | // report()->line() << xi18nc("@info:status", "(Command is printing too much output)"); | 285 | report()->line() << xi18nc("@info:status", "(Command is printing too much output)"); | ||
370 | // return; | 286 | return; | ||
371 | // } | 287 | } | ||
372 | 288 | | |||
373 | // output += s; | 289 | output += s; | ||
374 | 290 | | |||
375 | // if (report()) | 291 | if (report()) | ||
376 | // *report() << QString::fromLocal8Bit(s); | 292 | *report() << QString::fromLocal8Bit(s);*/ | ||
377 | } | 293 | } | ||
378 | 294 | | |||
379 | KAUTH_HELPER_MAIN("org.kde.kpmcore.externalcommand", ExternalCommandHelper) | 295 | KAUTH_HELPER_MAIN("org.kde.kpmcore.externalcommand", ExternalCommandHelper) |