2017-11-07 22:55:28 +00:00
/*************************************************************************
2018-03-22 16:41:49 +00:00
* Copyright ( C ) 2017 - 2018 by Andrius Š tikonas < andrius @ stikonas . eu > *
2017-11-07 22:55:28 +00:00
* *
* This program is free software ; you can redistribute it and / or *
* modify it under the terms of the GNU General Public License as *
* published by the Free Software Foundation ; either version 3 of *
* the License , or ( at your option ) any later version . *
* *
* This program is distributed in the hope that it will be useful , *
* but WITHOUT ANY WARRANTY ; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the *
* GNU General Public License for more details . *
* *
* You should have received a copy of the GNU General Public License *
* along with this program . If not , see < http : //www.gnu.org/licenses/>.*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
# include "externalcommandhelper.h"
2018-08-05 21:14:43 +01:00
# include "externalcommand_interface.h"
2018-08-11 20:32:02 +01:00
# include "externalcommand_whitelist.h"
2017-11-07 22:55:28 +00:00
2018-03-19 10:33:20 +00:00
# include <QtDBus>
2017-11-07 22:55:28 +00:00
# include <QDebug>
2018-02-03 15:57:47 +00:00
# include <QFile>
2018-01-24 15:22:42 +00:00
# include <QString>
2018-01-27 18:54:48 +00:00
# include <QTime>
# include <QVariant>
# include <KLocalizedString>
2018-03-19 10:33:20 +00:00
/** Initialize ExternalCommandHelper Daemon and prepare DBus interface
2018-07-15 00:09:39 +01:00
*
* KAuth helper runs in the background until application exits .
* To avoid forever running helper in case of application crash
2018-08-06 12:19:13 +01:00
* ExternalCommand class opens a DBus service that we monitor for changes .
* If helper is not busy then it exits when the client services gets
* unregistered . Otherwise ,
2018-07-15 00:09:39 +01:00
* we wait for the current job to finish before exiting , so even in case
* of main application crash , we do not leave partially moved data .
2018-08-06 12:19:13 +01:00
*
2018-07-15 00:09:39 +01:00
* This helper also starts another DBus interface where it listens to
* command execution requests from the application that started the helper .
* These requests are validated using public key cryptography , to prevent
* other unprivileged applications from gaining root privileges .
2018-03-19 10:33:20 +00:00
*/
ActionReply ExternalCommandHelper : : init ( const QVariantMap & args )
{
ActionReply reply ;
if ( ! QDBusConnection : : systemBus ( ) . isConnected ( ) ) {
2018-07-14 21:36:13 +01:00
qWarning ( ) < < " Could not connect to DBus system bus " ;
2018-03-19 10:33:20 +00:00
reply . addData ( QStringLiteral ( " success " ) , false ) ;
return reply ;
}
if ( ! QDBusConnection : : systemBus ( ) . registerService ( QStringLiteral ( " org.kde.kpmcore.helperinterface " ) ) ) {
qWarning ( ) < < QDBusConnection : : systemBus ( ) . lastError ( ) . message ( ) ;
reply . addData ( QStringLiteral ( " success " ) , false ) ;
return reply ;
}
2018-07-15 00:09:39 +01:00
if ( ! QDBusConnection : : systemBus ( ) . registerObject ( QStringLiteral ( " /Helper " ) , this , QDBusConnection : : ExportAllSlots ) ) {
qWarning ( ) < < QDBusConnection : : systemBus ( ) . lastError ( ) . message ( ) ;
reply . addData ( QStringLiteral ( " success " ) , false ) ;
return reply ;
}
m_publicKey = QCA : : PublicKey : : fromDER ( args [ QStringLiteral ( " pubkey " ) ] . toByteArray ( ) ) ;
2018-03-19 10:33:20 +00:00
2018-08-03 19:37:24 +01:00
m_loop = std : : make_unique < QEventLoop > ( ) ;
2018-03-23 22:08:23 +00:00
HelperSupport : : progressStep ( QVariantMap ( ) ) ;
2018-08-05 21:14:43 +01:00
2018-08-06 12:19:13 +01:00
// End the loop and return only once the client is done using us.
auto serviceWatcher =
new QDBusServiceWatcher ( QStringLiteral ( " org.kde.kpmcore.applicationinterface " ) ,
QDBusConnection : : systemBus ( ) ,
QDBusServiceWatcher : : WatchForUnregistration ,
this ) ;
connect ( serviceWatcher , & QDBusServiceWatcher : : serviceUnregistered ,
[ this ] ( ) {
m_loop - > exit ( ) ;
} ) ;
2018-07-15 00:09:39 +01:00
2018-08-03 19:37:24 +01:00
m_loop - > exec ( ) ;
2018-03-23 22:08:23 +00:00
reply . addData ( QStringLiteral ( " success " ) , true ) ;
2018-03-19 10:33:20 +00:00
return reply ;
}
2018-07-20 21:12:13 +01:00
/** Generates cryptographic nonce
* @ return nonce
*/
2018-07-21 11:03:25 +01:00
quint64 ExternalCommandHelper : : getNonce ( )
2018-07-20 21:12:13 +01:00
{
2018-07-21 11:03:25 +01:00
quint64 nonce = m_Generator . generate ( ) ;
m_Nonces . insert ( nonce ) ;
return nonce ;
2018-07-20 21:12:13 +01:00
}
2018-02-06 16:48:02 +00:00
/** Reads the given number of bytes from the sourceDevice into the given buffer.
@ param sourceDevice device or file to read from
@ param buffer buffer to store the bytes read in
@ param offset offset where to begin reading
@ param size the number of bytes to read
@ return true on success
*/
2018-03-21 17:01:40 +00:00
bool ExternalCommandHelper : : readData ( const QString & sourceDevice , QByteArray & buffer , qint64 offset , qint64 size )
2018-01-24 15:22:42 +00:00
{
2018-02-05 12:58:37 +00:00
QFile device ( sourceDevice ) ;
2018-01-24 15:22:42 +00:00
2018-02-05 12:58:37 +00:00
if ( ! device . open ( QIODevice : : ReadOnly | QIODevice : : Unbuffered ) ) {
qCritical ( ) < < xi18n ( " Could not open device <filename>%1</filename> for reading. " , sourceDevice ) ;
return false ;
}
2018-01-24 15:22:42 +00:00
2018-02-05 12:58:37 +00:00
if ( ! device . seek ( offset ) ) {
2018-08-21 09:35:39 +01:00
qCritical ( ) < < xi18n ( " Could not seek position %1 on device <filename>%2</filename>. " , offset , sourceDevice ) ;
2018-02-05 12:58:37 +00:00
return false ;
2018-01-24 15:22:42 +00:00
}
2018-02-05 12:58:37 +00:00
buffer = device . read ( size ) ;
if ( size ! = buffer . size ( ) ) {
qCritical ( ) < < xi18n ( " Could not read from device <filename>%1</filename>. " , sourceDevice ) ;
return false ;
}
return true ;
2018-01-24 15:22:42 +00:00
}
2018-02-06 16:48:02 +00:00
/** Writes the data from buffer to a given device or file.
@ param targetDevice device or file to write to
@ param buffer the data that we write
@ param offset offset where to begin writing
@ return true on success
*/
2018-03-21 17:01:40 +00:00
bool ExternalCommandHelper : : writeData ( const QString & targetDevice , const QByteArray & buffer , qint64 offset )
2018-01-24 15:22:42 +00:00
{
2018-02-03 15:57:47 +00:00
QFile device ( targetDevice ) ;
2018-02-05 12:46:10 +00:00
if ( ! device . open ( QIODevice : : WriteOnly | QIODevice : : Append | QIODevice : : Unbuffered ) ) {
2018-02-03 15:57:47 +00:00
qCritical ( ) < < xi18n ( " Could not open device <filename>%1</filename> for writing. " , targetDevice ) ;
return false ;
}
2018-01-24 15:22:42 +00:00
2018-02-03 15:57:47 +00:00
if ( ! device . seek ( offset ) ) {
2018-08-21 09:35:39 +01:00
qCritical ( ) < < xi18n ( " Could not seek position %1 on device <filename>%2</filename>. " , offset , targetDevice ) ;
2018-02-03 15:57:47 +00:00
return false ;
2018-01-24 15:22:42 +00:00
}
2018-02-03 15:57:47 +00:00
if ( device . write ( buffer ) ! = buffer . size ( ) ) {
qCritical ( ) < < xi18n ( " Could not write to device <filename>%1</filename>. " , targetDevice ) ;
return false ;
}
return true ;
2018-01-24 15:22:42 +00:00
}
2018-11-25 20:50:22 +00:00
// If targetDevice is empty then return QByteArray with data that was read from disk.
2018-10-28 17:22:12 +00:00
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 )
2018-01-24 15:22:42 +00:00
{
2018-10-28 17:22:12 +00:00
QVariantMap reply ;
reply [ QStringLiteral ( " success " ) ] = true ;
2018-07-21 11:03:25 +01:00
if ( m_Nonces . find ( nonce ) ! = m_Nonces . end ( ) )
m_Nonces . erase ( nonce ) ;
2018-10-28 17:22:12 +00:00
else {
reply [ QStringLiteral ( " success " ) ] = false ;
return reply ;
}
2018-07-21 11:03:25 +01:00
2018-04-13 12:24:05 +01:00
QByteArray request ;
2018-07-21 11:03:25 +01:00
request . setNum ( nonce ) ;
2018-04-13 12:24:05 +01:00
request . append ( sourceDevice . toUtf8 ( ) ) ;
request . append ( QByteArray : : number ( sourceFirstByte ) ) ;
request . append ( QByteArray : : number ( sourceLength ) ) ;
request . append ( targetDevice . toUtf8 ( ) ) ;
request . append ( QByteArray : : number ( targetFirstByte ) ) ;
request . append ( QByteArray : : number ( blockSize ) ) ;
QByteArray hash = QCryptographicHash : : hash ( request , QCryptographicHash : : Sha512 ) ;
if ( ! m_publicKey . verifyMessage ( hash , signature , QCA : : EMSA3_Raw ) ) {
qCritical ( ) < < xi18n ( " Invalid cryptographic signature " ) ;
2018-10-28 17:22:12 +00:00
reply [ QStringLiteral ( " success " ) ] = false ;
return reply ;
2018-04-13 12:24:05 +01:00
}
2018-01-24 15:22:42 +00:00
2018-03-21 17:01:40 +00:00
const qint64 blocksToCopy = sourceLength / blockSize ;
qint64 readOffset = sourceFirstByte ;
qint64 writeOffset = targetFirstByte ;
qint32 copyDirection = 1 ;
2018-01-24 15:22:42 +00:00
2018-03-21 17:01:40 +00:00
if ( targetFirstByte > sourceFirstByte ) {
readOffset = sourceFirstByte + sourceLength - blockSize ;
writeOffset = targetFirstByte + sourceLength - blockSize ;
copyDirection = - 1 ;
}
const qint64 lastBlock = sourceLength % blockSize ;
2018-01-24 15:22:42 +00:00
2018-01-27 18:54:48 +00:00
qint64 bytesWritten = 0 ;
2018-01-24 15:22:42 +00:00
qint64 blocksCopied = 0 ;
2018-01-24 15:35:11 +00:00
QByteArray buffer ;
2018-01-24 15:22:42 +00:00
int percent = 0 ;
2018-01-27 18:54:48 +00:00
QTime t ;
t . start ( ) ;
QVariantMap report ;
report [ QStringLiteral ( " report " ) ] = xi18nc ( " @info:progress " , " Copying %1 blocks (%2 bytes) from %3 to %4, direction: %5. " , blocksToCopy ,
sourceLength , readOffset , writeOffset , copyDirection = = 1 ? i18nc ( " direction: left " , " left " )
: i18nc ( " direction: right " , " right " ) ) ;
HelperSupport : : progressStep ( report ) ;
2018-01-24 15:22:42 +00:00
bool rval = true ;
2018-01-27 18:54:48 +00:00
2018-11-25 20:50:22 +00:00
while ( blocksCopied < blocksToCopy & & ! targetDevice . isEmpty ( ) ) {
2018-02-03 15:57:47 +00:00
if ( ! ( rval = readData ( sourceDevice , buffer , readOffset + blockSize * blocksCopied * copyDirection , blockSize ) ) )
2018-01-24 15:22:42 +00:00
break ;
2018-02-03 15:57:47 +00:00
if ( ! ( rval = writeData ( targetDevice , buffer , writeOffset + blockSize * blocksCopied * copyDirection ) ) )
2018-01-24 15:22:42 +00:00
break ;
2018-01-27 18:54:48 +00:00
bytesWritten + = buffer . size ( ) ;
2018-01-24 15:22:42 +00:00
if ( + + blocksCopied * 100 / blocksToCopy ! = percent ) {
percent = blocksCopied * 100 / blocksToCopy ;
2018-01-27 18:54:48 +00:00
if ( percent % 5 = = 0 & & t . elapsed ( ) > 1000 ) {
const qint64 mibsPerSec = ( blocksCopied * blockSize / 1024 / 1024 ) / ( t . elapsed ( ) / 1000 ) ;
const qint64 estSecsLeft = ( 100 - percent ) * t . elapsed ( ) / percent / 1000 ;
report [ QStringLiteral ( " report " ) ] = xi18nc ( " @info:progress " , " Copying %1 MiB/second, estimated time left: %2 " , mibsPerSec , QTime ( 0 , 0 ) . addSecs ( estSecsLeft ) . toString ( ) ) ;
HelperSupport : : progressStep ( report ) ;
}
2018-01-24 15:22:42 +00:00
HelperSupport : : progressStep ( percent ) ;
}
}
// copy the remainder
if ( rval & & lastBlock > 0 ) {
Q_ASSERT ( lastBlock < blockSize ) ;
const qint64 lastBlockReadOffset = copyDirection > 0 ? readOffset + blockSize * blocksCopied : sourceFirstByte ;
const qint64 lastBlockWriteOffset = copyDirection > 0 ? writeOffset + blockSize * blocksCopied : targetFirstByte ;
2018-01-27 18:54:48 +00:00
report [ QStringLiteral ( " report " ) ] = xi18nc ( " @info:progress " , " Copying remainder of block size %1 from %2 to %3. " , lastBlock , lastBlockReadOffset , lastBlockWriteOffset ) ;
HelperSupport : : progressStep ( report ) ;
2018-02-03 15:57:47 +00:00
rval = readData ( sourceDevice , buffer , lastBlockReadOffset , lastBlock ) ;
2018-01-24 15:22:42 +00:00
2018-10-28 17:22:12 +00:00
if ( rval ) {
if ( targetDevice . isEmpty ( ) )
reply [ QStringLiteral ( " targetByteArray " ) ] = buffer ;
else
rval = writeData ( targetDevice , buffer , lastBlockWriteOffset ) ;
}
2018-01-24 15:22:42 +00:00
2018-01-27 18:54:48 +00:00
if ( rval ) {
HelperSupport : : progressStep ( 100 ) ;
bytesWritten + = buffer . size ( ) ;
}
2018-01-24 15:22:42 +00:00
}
2018-01-27 18:54:48 +00:00
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 ) ) ;
HelperSupport : : progressStep ( report ) ;
2018-01-24 15:22:42 +00:00
2018-10-28 17:22:12 +00:00
reply [ QStringLiteral ( " success " ) ] = rval ;
return reply ;
2018-01-24 15:22:42 +00:00
}
2017-11-07 22:55:28 +00:00
2018-11-29 22:32:07 +00:00
bool ExternalCommandHelper : : writeData ( const QByteArray & signature , const quint64 nonce , const QByteArray & buffer , const QString & targetDevice , const qint64 targetFirstByte )
2018-11-25 20:50:22 +00:00
{
if ( m_Nonces . find ( nonce ) ! = m_Nonces . end ( ) )
m_Nonces . erase ( nonce ) ;
else
return false ;
QByteArray request ;
request . setNum ( nonce ) ;
request . append ( buffer ) ;
request . append ( targetDevice . toUtf8 ( ) ) ;
request . append ( QByteArray : : number ( targetFirstByte ) ) ;
// Do not allow using this helper for writing to arbitrary location
if ( targetDevice . left ( 5 ) ! = QStringLiteral ( " /dev/ " ) & & ! targetDevice . contains ( QStringLiteral ( " /etc/fstab " ) ) )
return false ;
QByteArray hash = QCryptographicHash : : hash ( request , QCryptographicHash : : Sha512 ) ;
if ( ! m_publicKey . verifyMessage ( hash , signature , QCA : : EMSA3_Raw ) ) {
qCritical ( ) < < xi18n ( " Invalid cryptographic signature " ) ;
return false ;
}
return writeData ( targetDevice , buffer , targetFirstByte ) ;
}
2018-07-21 11:03:25 +01:00
QVariantMap ExternalCommandHelper : : start ( const QByteArray & signature , const quint64 nonce , const QString & command , const QStringList & arguments , const QByteArray & input , const int processChannelMode )
2017-11-07 22:55:28 +00:00
{
2018-04-13 00:44:12 +01:00
QTextCodec : : setCodecForLocale ( QTextCodec : : codecForName ( " UTF-8 " ) ) ;
2018-03-19 10:33:20 +00:00
QVariantMap reply ;
2018-07-20 21:12:13 +01:00
reply [ QStringLiteral ( " success " ) ] = true ;
2018-04-12 22:43:12 +01:00
2018-07-21 11:03:25 +01:00
if ( m_Nonces . find ( nonce ) ! = m_Nonces . end ( ) )
m_Nonces . erase ( nonce ) ;
else {
reply [ QStringLiteral ( " success " ) ] = false ;
return reply ;
}
2018-08-16 17:59:47 +01:00
if ( command . isEmpty ( ) ) {
reply [ QStringLiteral ( " success " ) ] = false ;
return reply ;
}
2018-04-12 22:43:12 +01:00
QByteArray request ;
2018-07-21 11:03:25 +01:00
request . setNum ( nonce ) ;
2018-04-13 12:24:05 +01:00
request . append ( command . toUtf8 ( ) ) ;
2018-04-12 22:43:12 +01:00
for ( const auto & argument : arguments )
request . append ( argument . toUtf8 ( ) ) ;
request . append ( input ) ;
2018-07-15 17:37:15 +01:00
request . append ( processChannelMode ) ;
2018-04-12 22:43:12 +01:00
QByteArray hash = QCryptographicHash : : hash ( request , QCryptographicHash : : Sha512 ) ;
if ( ! m_publicKey . verifyMessage ( hash , signature , QCA : : EMSA3_Raw ) ) {
qCritical ( ) < < xi18n ( " Invalid cryptographic signature " ) ;
2018-03-22 16:41:49 +00:00
reply [ QStringLiteral ( " success " ) ] = false ;
return reply ;
}
2017-11-07 22:55:28 +00:00
2018-08-11 20:32:02 +01:00
// Compare with command whitelist
QString basename = command . mid ( command . lastIndexOf ( QLatin1Char ( ' / ' ) ) + 1 ) ;
2018-08-24 16:14:49 +01:00
if ( std : : find ( std : : begin ( allowedCommands ) , std : : end ( allowedCommands ) , basename ) = = std : : end ( allowedCommands ) ) {
2018-08-11 20:32:02 +01:00
// TODO: notify the user
m_loop - > exit ( ) ;
2018-08-24 16:14:49 +01:00
reply [ QStringLiteral ( " success " ) ] = false ;
return reply ;
2018-08-11 20:32:02 +01:00
}
2017-11-07 22:55:28 +00:00
// connect(&cmd, &QProcess::readyReadStandardOutput, this, &ExternalCommandHelper::onReadOutput);
2018-04-13 00:44:12 +01:00
m_cmd . setEnvironment ( { QStringLiteral ( " LVM_SUPPRESS_FD_WARNINGS=1 " ) } ) ;
2018-07-15 17:37:15 +01:00
m_cmd . setProcessChannelMode ( static_cast < QProcess : : ProcessChannelMode > ( processChannelMode ) ) ;
2018-03-19 10:33:20 +00:00
m_cmd . start ( command , arguments ) ;
m_cmd . write ( input ) ;
m_cmd . closeWriteChannel ( ) ;
m_cmd . waitForFinished ( - 1 ) ;
QByteArray output = m_cmd . readAllStandardOutput ( ) ;
reply [ QStringLiteral ( " output " ) ] = output ;
reply [ QStringLiteral ( " exitCode " ) ] = m_cmd . exitCode ( ) ;
2017-11-07 22:55:28 +00:00
return reply ;
}
2018-07-21 11:03:25 +01:00
void ExternalCommandHelper : : exit ( const QByteArray & signature , const quint64 nonce )
2018-03-21 17:01:40 +00:00
{
2018-04-13 12:24:05 +01:00
QByteArray request ;
2018-07-21 11:03:25 +01:00
if ( m_Nonces . find ( nonce ) = = m_Nonces . end ( ) )
return ;
request . setNum ( nonce ) ;
2018-04-13 12:24:05 +01:00
QByteArray hash = QCryptographicHash : : hash ( request , QCryptographicHash : : Sha512 ) ;
if ( ! m_publicKey . verifyMessage ( hash , signature , QCA : : EMSA3_Raw ) ) {
2018-04-13 12:26:44 +01:00
qCritical ( ) < < xi18n ( " Invalid cryptographic signature " ) ;
2018-04-13 12:24:05 +01:00
return ;
2018-03-21 17:01:40 +00:00
}
2018-08-03 19:37:24 +01:00
m_loop - > exit ( ) ;
2018-03-22 05:32:59 +00:00
QDBusConnection : : systemBus ( ) . unregisterObject ( QStringLiteral ( " /Helper " ) ) ;
2018-07-15 00:35:55 +01:00
QDBusConnection : : systemBus ( ) . unregisterService ( QStringLiteral ( " org.kde.kpmcore.helperinterface " ) ) ;
2018-03-19 10:33:20 +00:00
}
2017-11-07 22:55:28 +00:00
void ExternalCommandHelper : : onReadOutput ( )
{
// const QByteArray s = cmd.readAllStandardOutput();
// if(output.length() > 10*1024*1024) { // prevent memory overflow for badly corrupted file systems
// if (report())
// report()->line() << xi18nc("@info:status", "(Command is printing too much output)");
// return;
// }
// output += s;
// if (report())
// *report() << QString::fromLocal8Bit(s);
}
KAUTH_HELPER_MAIN ( " org.kde.kpmcore.externalcommand " , ExternalCommandHelper )