diff --git a/resources.qrc b/resources.qrc index 602774165..ee77f0f51 100644 --- a/resources.qrc +++ b/resources.qrc @@ -143,5 +143,6 @@ data/images/process-stop.png data/icons/tomahawk-icon-128x128-grayscale.png data/images/collection.png + data/misc/tomahawk_pubkey.pem diff --git a/src/libtomahawk/AtticaManager.cpp b/src/libtomahawk/AtticaManager.cpp index deeabeff6..b8636f4b6 100644 --- a/src/libtomahawk/AtticaManager.cpp +++ b/src/libtomahawk/AtticaManager.cpp @@ -23,8 +23,6 @@ #include "Pipeline.h" #include -#include -#include #include #include @@ -451,6 +449,7 @@ AtticaManager::installResolver( const Content& resolver, bool autoCreateAccount connect( job, SIGNAL( finished( Attica::BaseJob* ) ), this, SLOT( resolverDownloadFinished( Attica::BaseJob* ) ) ); job->setProperty( "resolverId", resolver.id() ); job->setProperty( "createAccount", autoCreateAccount ); + job->setProperty( "binarySignature", resolver.attribute("signature")); job->start(); } @@ -487,6 +486,7 @@ AtticaManager::resolverDownloadFinished ( BaseJob* j ) connect( reply, SIGNAL( finished() ), this, SLOT( payloadFetched() ) ); reply->setProperty( "resolverId", job->property( "resolverId" ) ); reply->setProperty( "createAccount", job->property( "createAccount" ) ); + reply->setProperty( "binarySignature", job->property( "binarySignature" ) ); } else { @@ -513,23 +513,50 @@ AtticaManager::payloadFetched() f.write( reply->readAll() ); f.close(); - QString resolverId = reply->property( "resolverId" ).toString(); - QDir dir( extractPayload( f.fileName(), resolverId ) ); - QString resolverPath = dir.absoluteFilePath( m_resolverStates[ resolverId ].scriptPath ); - - if ( !resolverPath.isEmpty() ) + bool installedSuccessfully = false; + const QString resolverId = reply->property( "resolverId" ).toString(); + if ( m_resolverStates[ resolverId ].binary ) { - // update with absolute, not relative, path - m_resolverStates[ resolverId ].scriptPath = resolverPath; - - if ( reply->property( "createAccount" ).toBool() ) + // First ensure the signature matches. If we can't verify it, abort! + const QString signature = reply->property( "binarySignature" ).toString(); + // Must have a signature for binary resolvers... + Q_ASSERT( !signature.isEmpty() ); + if ( signature.isEmpty() ) + return; + if ( !TomahawkUtils::verifyFile( f.fileName(), signature ) ) { - // Do the install / add to tomahawk - Tomahawk::Accounts::Account* resolver = Tomahawk::Accounts::ResolverAccountFactory::createFromPath( resolverPath, "resolveraccount", true ); - Tomahawk::Accounts::AccountManager::instance()->addAccount( resolver ); - TomahawkSettings::instance()->addAccount( resolver->accountId() ); + qWarning() << "FILE SIGNATURE FAILED FOR BINARY RESOLVER! WARNING! :" << f.fileName() << signature; + return; } +#ifdef Q_OS_MAC +#elif Q_OS_WIN +#endif + } + else + { + QDir dir( TomahawkUtils::extractScriptPayload( f.fileName(), resolverId ) ); + QString resolverPath = dir.absoluteFilePath( m_resolverStates[ resolverId ].scriptPath ); + + if ( !resolverPath.isEmpty() ) + { + // update with absolute, not relative, path + m_resolverStates[ resolverId ].scriptPath = resolverPath; + + if ( reply->property( "createAccount" ).toBool() ) + { + // Do the install / add to tomahawk + Tomahawk::Accounts::Account* resolver = Tomahawk::Accounts::ResolverAccountFactory::createFromPath( resolverPath, "resolveraccount", true ); + Tomahawk::Accounts::AccountManager::instance()->addAccount( resolver ); + TomahawkSettings::instance()->addAccount( resolver->accountId() ); + } + + installedSuccessfully = true; + } + } + + if ( installedSuccessfully ) + { m_resolverStates[ resolverId ].state = Installed; TomahawkSettingsGui::instanceGui()->setAtticaResolverStates( m_resolverStates ); emit resolverInstalled( resolverId ); @@ -543,75 +570,6 @@ AtticaManager::payloadFetched() } -QString -AtticaManager::extractPayload( const QString& filename, const QString& resolverId ) const -{ - // uses QuaZip to extract the temporary zip file to the user's tomahawk data/resolvers directory - QuaZip zipFile( filename ); - if ( !zipFile.open( QuaZip::mdUnzip ) ) - { - tLog() << "Failed to QuaZip open:" << zipFile.getZipError(); - return QString(); - } - - if ( !zipFile.goToFirstFile() ) - { - tLog() << "Failed to go to first file in zip archive: " << zipFile.getZipError(); - return QString(); - } - - QDir resolverDir = TomahawkUtils::appDataDir(); - if ( !resolverDir.mkpath( QString( "atticaresolvers/%1" ).arg( resolverId ) ) ) - { - tLog() << "Failed to mkdir resolver save dir: " << TomahawkUtils::appDataDir().absoluteFilePath( QString( "atticaresolvers/%1" ).arg( resolverId ) ); - return QString(); - } - resolverDir.cd( QString( "atticaresolvers/%1" ).arg( resolverId ) ); - tDebug() << "Installing resolver to:" << resolverDir.absolutePath(); - - QuaZipFile fileInZip( &zipFile ); - do - { - QuaZipFileInfo info; - zipFile.getCurrentFileInfo( &info ); - - if ( !fileInZip.open( QIODevice::ReadOnly ) ) - { - tLog() << "Failed to open file inside zip archive:" << info.name << zipFile.getZipName() << "with error:" << zipFile.getZipError(); - continue; - } - - QFile out( resolverDir.absoluteFilePath( fileInZip.getActualFileName() ) ); - - QStringList parts = fileInZip.getActualFileName().split( "/" ); - if ( parts.size() > 1 ) - { - QStringList dirs = parts.mid( 0, parts.size() - 1 ); - QString dirPath = dirs.join( "/" ); // QDir translates / to \ internally if necessary - resolverDir.mkpath( dirPath ); - } - - // make dir if there is one needed - QDir d( fileInZip.getActualFileName() ); - - tDebug() << "Writing to output file..." << out.fileName(); - if ( !out.open( QIODevice::WriteOnly ) ) - { - tLog() << "Failed to open resolver extract file:" << out.errorString() << info.name; - continue; - } - - - out.write( fileInZip.readAll() ); - out.close(); - fileInZip.close(); - - } while ( zipFile.goToNextFile() ); - - return resolverDir.absolutePath(); -} - - void AtticaManager::uninstallResolver( const QString& pathToResolver ) { diff --git a/src/libtomahawk/CMakeLists.txt b/src/libtomahawk/CMakeLists.txt index c77b302e5..af98f0fdc 100644 --- a/src/libtomahawk/CMakeLists.txt +++ b/src/libtomahawk/CMakeLists.txt @@ -391,6 +391,7 @@ IF( APPLE ) infosystem/infoplugins/mac/Adium.mm infosystem/infoplugins/mac/AdiumPlugin.cpp utils/TomahawkUtils_Mac.mm + mac/FileHelpers.mm thirdparty/Qocoa/qsearchfield_mac.mm ) SET_SOURCE_FILES_PROPERTIES(utils/TomahawkUtils_Mac.mm PROPERTIES COMPILE_FLAGS "-fvisibility=default") @@ -400,10 +401,11 @@ IF( APPLE ) # System ${COREAUDIO_LIBRARY} ${COREFOUNDATION_LIBRARY} - ${FOUNDATION_LIBRARY} + ${FOUNDATION_LIBRARY} ${SCRIPTINGBRIDGE_LIBRARY} /System/Library/Frameworks/AppKit.framework + /System/Library/Frameworks/Security.framework ) ELSE( APPLE ) SET( libGuiSources ${libGuiSources} thirdparty/Qocoa/qsearchfield.cpp ) diff --git a/src/libtomahawk/database/DatabaseCollection.cpp b/src/libtomahawk/database/DatabaseCollection.cpp index cb864cc66..2dca1980d 100644 --- a/src/libtomahawk/database/DatabaseCollection.cpp +++ b/src/libtomahawk/database/DatabaseCollection.cpp @@ -1,6 +1,7 @@ /* === This file is part of Tomahawk Player - === * * Copyright 2010-2011, Christian Muehlhaeuser + * Copyright 2010-2011, Leo Franchi * * Tomahawk is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/libtomahawk/database/DatabaseCollection.h b/src/libtomahawk/database/DatabaseCollection.h index a9e9fae65..de94bbd6c 100644 --- a/src/libtomahawk/database/DatabaseCollection.h +++ b/src/libtomahawk/database/DatabaseCollection.h @@ -1,6 +1,7 @@ /* === This file is part of Tomahawk Player - === * * Copyright 2010-2011, Christian Muehlhaeuser + * Copyright 2010-2011, Leo Franchi * * Tomahawk is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/libtomahawk/utils/TomahawkUtils.cpp b/src/libtomahawk/utils/TomahawkUtils.cpp index 8fcf170af..c2cc93b18 100644 --- a/src/libtomahawk/utils/TomahawkUtils.cpp +++ b/src/libtomahawk/utils/TomahawkUtils.cpp @@ -39,6 +39,9 @@ #include #include +#include +#include + #ifdef Q_WS_WIN #include #include @@ -49,6 +52,10 @@ #include #endif +#ifdef QCA2_FOUND +#include +#endif + namespace TomahawkUtils { static quint64 s_infosystemRequestId = 0; @@ -676,4 +683,160 @@ SharedTimeLine::disconnectNotify( const char* signal ) } +bool +verifyFile( const QString &filePath, const QString &signature ) +{ + QCA::Initializer init; + + if( !QCA::isSupported( "sha1" ) ) + { + qWarning() << "SHA1 not supported by QCA, aborting."; + return false; + } + + // The signature for the resolver.zip was created like so: + // openssl dgst -sha1 -binary < "#{tarball}" | openssl dgst -dss1 -sign "#{ARGV[2]}" | openssl enc -base64 + // which means we need to decode it with QCA's DSA public key signature verification tools + // The input data is: + // file -> SHA1 binary format -> DSS1/DSA signed -> base64 encoded. + + // Step 1: Load the public key + // Public key is in :/data/misc/tomahawk_pubkey.pem + QFile f( ":/data/misc/tomahawk_pubkey.pem" ); + if ( !f.open( QIODevice::ReadOnly ) ) + { + qWarning() << "Unable to read public key from resources!"; + return false; + } + + const QString pubkeyData = QString::fromUtf8( f.readAll() ); + QCA::ConvertResult conversionResult; + QCA::PublicKey publicKey = QCA::PublicKey::fromPEM( pubkeyData, &conversionResult ); + if ( QCA::ConvertGood != conversionResult) + { + qWarning() << "Public key reading/loading failed! Tried to load public key:" << pubkeyData; + return false; + } + + if ( !publicKey.canVerify() ) + { + qWarning() << "Loaded Tomahawk public key but cannot use it to verify! What is up...."; + return false; + } + + // Step 2: Get the SHA1 of the file contents + QFile toVerify( filePath ); + if ( !toVerify.exists() || !toVerify.open( QIODevice::ReadOnly ) ) + { + qWarning() << "Failed to open file we are trying to verify!" << filePath; + return false; + } + + QCA::Hash fileHash = QCA::Hash( "sha1 "); + //QCA::SecureArray fileData( toVerify.readAll() ); + //fileHash.update( fileData ); + const QByteArray fileHashData = QCA::Hash( "sha1" ).hash( toVerify.readAll() ).toByteArray(); + toVerify.close(); + + // Step 3: Base64 decode the signature + QCA::Base64 decoder( QCA::Decode ); + const QByteArray decodedSignature = decoder.decode( QCA::SecureArray( signature.trimmed().toUtf8() ) ).toByteArray(); + if ( decodedSignature.isEmpty() ) + { + qWarning() << "Got empty signature after we tried to decode it from Base64:" << signature.trimmed().toUtf8() << decodedSignature.toBase64(); + return false; + } + + // Step 4: Do the actual verifying! + const bool result = publicKey.verifyMessage( fileHashData, decodedSignature, QCA::EMSA1_SHA1, QCA::DERSequence ); + if ( !result ) + { + qWarning() << "File" << filePath << "FAILED VERIFICATION against our input signature!"; + return false; + } + + qDebug() << "Successfully verified signature of downloaded file:" << filePath; + + return true; +} + + +QString +extractScriptPayload( const QString& filename, const QString& resolverId ) +{ + // uses QuaZip to extract the temporary zip file to the user's tomahawk data/resolvers directory + QuaZip zipFile( filename ); + if ( !zipFile.open( QuaZip::mdUnzip ) ) + { + tLog() << "Failed to QuaZip open:" << zipFile.getZipError(); + return QString(); + } + + if ( !zipFile.goToFirstFile() ) + { + tLog() << "Failed to go to first file in zip archive: " << zipFile.getZipError(); + return QString(); + } + + QDir resolverDir = appDataDir(); + if ( !resolverDir.mkpath( QString( "atticaresolvers/%1" ).arg( resolverId ) ) ) + { + tLog() << "Failed to mkdir resolver save dir: " << TomahawkUtils::appDataDir().absoluteFilePath( QString( "atticaresolvers/%1" ).arg( resolverId ) ); + return QString(); + } + resolverDir.cd( QString( "atticaresolvers/%1" ).arg( resolverId ) ); + tDebug() << "Installing resolver to:" << resolverDir.absolutePath(); + + QuaZipFile fileInZip( &zipFile ); + do + { + QuaZipFileInfo info; + zipFile.getCurrentFileInfo( &info ); + + if ( !fileInZip.open( QIODevice::ReadOnly ) ) + { + tLog() << "Failed to open file inside zip archive:" << info.name << zipFile.getZipName() << "with error:" << zipFile.getZipError(); + continue; + } + + QFile out( resolverDir.absoluteFilePath( fileInZip.getActualFileName() ) ); + + QStringList parts = fileInZip.getActualFileName().split( "/" ); + if ( parts.size() > 1 ) + { + QStringList dirs = parts.mid( 0, parts.size() - 1 ); + QString dirPath = dirs.join( "/" ); // QDir translates / to \ internally if necessary + resolverDir.mkpath( dirPath ); + } + + // make dir if there is one needed + QDir d( fileInZip.getActualFileName() ); + + tDebug() << "Writing to output file..." << out.fileName(); + if ( !out.open( QIODevice::WriteOnly ) ) + { + tLog() << "Failed to open resolver extract file:" << out.errorString() << info.name; + continue; + } + + + out.write( fileInZip.readAll() ); + out.close(); + fileInZip.close(); + + } while ( zipFile.goToNextFile() ); + + return resolverDir.absolutePath(); +} + + +#if !defined(Q_OS_MAC) // && !defined(Q_OS_WIN) +void +extractBinaryResolver( const QString& zipFilename, const QString& resolverId, QObject* ) +{ + // No support for binary resolvers on linux! Shouldn't even have been allowed to see/install.. + Q_ASSERT( false ); +} +#endif + } // ns diff --git a/src/libtomahawk/utils/TomahawkUtils.h b/src/libtomahawk/utils/TomahawkUtils.h index 8269d3134..93f73cc5d 100644 --- a/src/libtomahawk/utils/TomahawkUtils.h +++ b/src/libtomahawk/utils/TomahawkUtils.h @@ -137,6 +137,13 @@ namespace TomahawkUtils DLLEXPORT QString md5( const QByteArray& data ); DLLEXPORT bool removeDirectory( const QString& dir ); + DLLEXPORT bool verifyFile( const QString& filePath, const QString& signature ); + DLLEXPORT QString extractScriptPayload( const QString& filename, const QString& resolverId ); + + // Extracting may be asynchronous, pass in a receiver object with the following slots: + // extractSucceeded( const QString& path ) and extractFailed() to be notified/ + DLLEXPORT void extractBinaryResolver( const QString& zipFilename, const QString& resolverId, QObject* receiver ); + /** * This helper is designed to help "update" an existing playlist with a newer revision of itself. * To avoid re-loading the whole playlist and re-resolving tracks that are the same in the old playlist, diff --git a/src/libtomahawk/utils/TomahawkUtils_Mac.h b/src/libtomahawk/utils/TomahawkUtils_Mac.h new file mode 100644 index 000000000..635db4117 --- /dev/null +++ b/src/libtomahawk/utils/TomahawkUtils_Mac.h @@ -0,0 +1,37 @@ +/* === This file is part of Tomahawk Player - === + * + * Copyright 2012, Leo Franchi . + */ + +#ifndef TOMAHAWKUTILS_MAC_H +#define TOMAHAWKUTILS_MAC_H + +#include + +#import "mac/FileHelpers.h" + +@interface MoveDelegate : NSObject +{ + QObject* receiver; + QString path; +} +- (void)setReceiver:(QObject*)receiver; +- (void)setMoveTo:(QString)path; +- (void)moveFinished; +- (void)moveFailedWithError:(NSError *)error; +@end + +#endif // TOMAHAWKUTILS_MAC_H diff --git a/src/libtomahawk/utils/TomahawkUtils_Mac.mm b/src/libtomahawk/utils/TomahawkUtils_Mac.mm index aeac94866..5bb6558e5 100644 --- a/src/libtomahawk/utils/TomahawkUtils_Mac.mm +++ b/src/libtomahawk/utils/TomahawkUtils_Mac.mm @@ -1,6 +1,54 @@ +/* === This file is part of Tomahawk Player - === + * + * Copyright 2012, Leo Franchi . + */ + #include "TomahawkUtils.h" +#include "TomahawkUtils_Mac.h" +#include "mac/FileHelpers.h" + +#include + #import +#import + +@implementation MoveDelegate + + +-(void) setReceiver:(QObject*) object +{ + receiver = object; +} + +-(void) setMoveTo:(QString) p +{ + path = p; +} + +- (void)moveFinished +{ + QMetaObject::invokeMethod(receiver, "installSucceeded", Qt::DirectConnection, Q_ARG(QString, path)); +} + +- (void)moveFailedWithError:(NSError *)error +{ + QMetaObject::invokeMethod(receiver, "installFailed", Qt::DirectConnection); +} +@end namespace TomahawkUtils { @@ -10,4 +58,35 @@ bringToFront() { [NSApp activateIgnoringOtherApps:YES]; } + +void +extractBinaryResolver( const QString& zipFilename, const QString& resolverId, QObject* receiver ) +{ + /** + On OS X, we have to do the following: + 2) Extract file in temporary location + 3) Authenticate to be able to have write access to the /Applications folder + 4) Copy the contents of the zipfile to the Tomahawk.app/Contents/MacOS/ folder + 5) Call result slots on receiver object + */ + + MoveDelegate* del = [[MoveDelegate alloc] init]; + [del setReceiver: receiver]; + + // Unzip in temporary folder and copy the contents to MacOS/ + NSError* err = NULL; + NSFileManager *manager = [[[NSFileManager alloc] init] autorelease]; + NSURL* tempDir = [manager URLForDirectory:NSCachesDirectory inDomain:NSUserDomainMask appropriateForURL:NULL create:YES error:&err]; + if ( err ) + { + qDebug() << "GOT ERROR trying to create temp dir to unzip in...:" << err; + return; + } + + qDebug() << "Using temporary directory:" << [tempDir absoluteString]; + + +// [del setMoveTo: to]; +} + } diff --git a/src/main.cpp b/src/main.cpp index fe0332d1f..2c786bf55 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -121,7 +121,7 @@ int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, int main( int argc, char *argv[] ) { - #ifdef Q_WS_MAC +#ifdef Q_WS_MAC // Do Mac specific startup to get media keys working. // This must go before QApplication initialisation. Tomahawk::macMain();