/****************************************************************************** * Copyright (C) 2011-2013 Frank Osterfeld * * * * 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. For licensing and distribution * * details, check the accompanying file 'COPYING'. * *****************************************************************************/ #include "keychain_p.h" #include "gnomekeyring_p.h" #include #include using namespace QKeychain; static QString typeKey( const QString& key ) { return QString::fromLatin1( "%1/type" ).arg( key ); } static QString dataKey( const QString& key ) { return QString::fromLatin1( "%1/data" ).arg( key ); } enum KeyringBackend { Backend_GnomeKeyring, Backend_Kwallet }; static KeyringBackend detectKeyringBackend() { if ( !qgetenv( "GNOME_KEYRING_CONTROL" ).isNull() && GnomeKeyring::isSupported() ) return Backend_GnomeKeyring; else return Backend_Kwallet; } static KeyringBackend getKeyringBackend() { static KeyringBackend backend = detectKeyringBackend(); return backend; } void ReadPasswordJobPrivate::scheduledStart() { switch ( getKeyringBackend() ) { case Backend_GnomeKeyring: if ( !GnomeKeyring::find_network_password( key.toUtf8().constData(), q->service().toUtf8().constData(), reinterpret_cast( &ReadPasswordJobPrivate::gnomeKeyring_cb ), this, 0 ) ) q->emitFinishedWithError( OtherError, tr("Unknown error") ); break; case Backend_Kwallet: if ( QDBusConnection::sessionBus().isConnected() ) { iface = new org::kde::KWallet( QLatin1String("org.kde.kwalletd"), QLatin1String("/modules/kwalletd"), QDBusConnection::sessionBus(), this ); const QDBusPendingReply reply = iface->networkWallet(); QDBusPendingCallWatcher* watcher = new QDBusPendingCallWatcher( reply, this ); connect( watcher, SIGNAL(finished(QDBusPendingCallWatcher*)), this, SLOT(kwalletWalletFound(QDBusPendingCallWatcher*)) ); } else { // D-Bus is not reachable so none can tell us something about KWalletd QDBusError err( QDBusError::NoServer, tr("D-Bus is not running") ); fallbackOnError( err ); } break; } } void ReadPasswordJobPrivate::kwalletWalletFound(QDBusPendingCallWatcher *watcher) { watcher->deleteLater(); const QDBusPendingReply reply = *watcher; const QDBusPendingReply pendingReply = iface->open( reply.value(), 0, q->service() ); QDBusPendingCallWatcher* pendingWatcher = new QDBusPendingCallWatcher( pendingReply, this ); connect( pendingWatcher, SIGNAL(finished(QDBusPendingCallWatcher*)), this, SLOT(kwalletOpenFinished(QDBusPendingCallWatcher*)) ); } static QPair mapGnomeKeyringError( int result ) { Q_ASSERT( result != GnomeKeyring::RESULT_OK ); switch ( result ) { case GnomeKeyring::RESULT_DENIED: return qMakePair( AccessDenied, QObject::tr("Access to keychain denied") ); case GnomeKeyring::RESULT_NO_KEYRING_DAEMON: return qMakePair( NoBackendAvailable, QObject::tr("No keyring daemon") ); case GnomeKeyring::RESULT_ALREADY_UNLOCKED: return qMakePair( OtherError, QObject::tr("Already unlocked") ); case GnomeKeyring::RESULT_NO_SUCH_KEYRING: return qMakePair( OtherError, QObject::tr("No such keyring") ); case GnomeKeyring::RESULT_BAD_ARGUMENTS: return qMakePair( OtherError, QObject::tr("Bad arguments") ); case GnomeKeyring::RESULT_IO_ERROR: return qMakePair( OtherError, QObject::tr("I/O error") ); case GnomeKeyring::RESULT_CANCELLED: return qMakePair( OtherError, QObject::tr("Cancelled") ); case GnomeKeyring::RESULT_KEYRING_ALREADY_EXISTS: return qMakePair( OtherError, QObject::tr("Keyring already exists") ); case GnomeKeyring::RESULT_NO_MATCH: return qMakePair( EntryNotFound, QObject::tr("No match") ); default: break; } return qMakePair( OtherError, QObject::tr("Unknown error") ); } void ReadPasswordJobPrivate::gnomeKeyring_cb( int result, const char* string, ReadPasswordJobPrivate* self ) { if ( result == GnomeKeyring::RESULT_OK ) { if ( self->dataType == ReadPasswordJobPrivate::Text ) self->data = string; else self->data = QByteArray::fromBase64( string ); self->q->emitFinished(); } else { const QPair errorResult = mapGnomeKeyringError( result ); self->q->emitFinishedWithError( errorResult.first, errorResult.second ); } } void ReadPasswordJobPrivate::fallbackOnError(const QDBusError& err ) { QScopedPointer local( !q->settings() ? new QSettings( q->service() ) : 0 ); QSettings* actual = q->settings() ? q->settings() : local.data(); if ( q->insecureFallback() && actual->contains( dataKey( key ) ) ) { const WritePasswordJobPrivate::Mode mode = WritePasswordJobPrivate::stringToMode( actual->value( typeKey( key ) ).toString() ); if (mode == WritePasswordJobPrivate::Binary) dataType = Binary; else dataType = Text; data = actual->value( dataKey( key ) ).toByteArray(); q->emitFinished(); } else { if ( err.type() == QDBusError::ServiceUnknown ) //KWalletd not running q->emitFinishedWithError( NoBackendAvailable, tr("No keychain service available") ); else q->emitFinishedWithError( OtherError, tr("Could not open wallet: %1; %2").arg( QDBusError::errorString( err.type() ), err.message() ) ); } } void ReadPasswordJobPrivate::kwalletOpenFinished( QDBusPendingCallWatcher* watcher ) { watcher->deleteLater(); const QDBusPendingReply reply = *watcher; QScopedPointer local( !q->settings() ? new QSettings( q->service() ) : 0 ); QSettings* actual = q->settings() ? q->settings() : local.data(); if ( reply.isError() ) { fallbackOnError( reply.error() ); return; } if ( actual->contains( dataKey( key ) ) ) { // We previously stored data in the insecure QSettings, but now have KWallet available. // Do the migration data = actual->value( dataKey( key ) ).toByteArray(); const WritePasswordJobPrivate::Mode mode = WritePasswordJobPrivate::stringToMode( actual->value( typeKey( key ) ).toString() ); actual->remove( key ); q->emitFinished(); WritePasswordJob* j = new WritePasswordJob( q->service(), 0 ); j->setSettings( q->settings() ); j->setKey( key ); j->setAutoDelete( true ); if ( mode == WritePasswordJobPrivate::Binary ) j->setBinaryData( data ); else if ( mode == WritePasswordJobPrivate::Text ) j->setTextData( QString::fromUtf8( data ) ); else Q_ASSERT( false ); j->start(); return; } walletHandle = reply.value(); if ( walletHandle < 0 ) { q->emitFinishedWithError( AccessDenied, tr("Access to keychain denied") ); return; } const QDBusPendingReply nextReply = iface->entryType( walletHandle, q->service(), key, q->service() ); QDBusPendingCallWatcher* nextWatcher = new QDBusPendingCallWatcher( nextReply, this ); connect( nextWatcher, SIGNAL(finished(QDBusPendingCallWatcher*)), this, SLOT(kwalletEntryTypeFinished(QDBusPendingCallWatcher*)) ); } //Must be in sync with KWallet::EntryType (kwallet.h) enum KWalletEntryType { Unknown=0, Password, Stream, Map }; void ReadPasswordJobPrivate::kwalletEntryTypeFinished( QDBusPendingCallWatcher* watcher ) { watcher->deleteLater(); if ( watcher->isError() ) { const QDBusError err = watcher->error(); q->emitFinishedWithError( OtherError, tr("Could not determine data type: %1; %2").arg( QDBusError::errorString( err.type() ), err.message() ) ); return; } const QDBusPendingReply reply = *watcher; const int value = reply.value(); switch ( value ) { case Unknown: q->emitFinishedWithError( EntryNotFound, tr("Entry not found") ); return; case Password: dataType = Text; break; case Stream: dataType = Binary; break; case Map: q->emitFinishedWithError( EntryNotFound, tr("Unsupported entry type 'Map'") ); return; default: q->emitFinishedWithError( OtherError, tr("Unknown kwallet entry type '%1'").arg( value ) ); return; } const QDBusPendingCall nextReply = dataType == Text ? QDBusPendingCall( iface->readPassword( walletHandle, q->service(), key, q->service() ) ) : QDBusPendingCall( iface->readEntry( walletHandle, q->service(), key, q->service() ) ); QDBusPendingCallWatcher* nextWatcher = new QDBusPendingCallWatcher( nextReply, this ); connect( nextWatcher, SIGNAL(finished(QDBusPendingCallWatcher*)), this, SLOT(kwalletReadFinished(QDBusPendingCallWatcher*)) ); } void ReadPasswordJobPrivate::kwalletReadFinished( QDBusPendingCallWatcher* watcher ) { watcher->deleteLater(); if ( watcher->isError() ) { const QDBusError err = watcher->error(); q->emitFinishedWithError( OtherError, tr("Could not read password: %1; %2").arg( QDBusError::errorString( err.type() ), err.message() ) ); return; } if ( dataType == Binary ) { QDBusPendingReply reply = *watcher; data = reply.value(); } else { QDBusPendingReply reply = *watcher; data = reply.value().toUtf8(); } q->emitFinished(); } void WritePasswordJobPrivate::scheduledStart() { switch ( getKeyringBackend() ) { case Backend_GnomeKeyring: if ( mode == WritePasswordJobPrivate::Delete ) { if ( !GnomeKeyring::delete_network_password( key.toUtf8().constData(), q->service().toUtf8().constData(), reinterpret_cast( &WritePasswordJobPrivate::gnomeKeyring_cb ), this, 0 ) ) q->emitFinishedWithError( OtherError, tr("Unknown error") ); } else { QByteArray password = mode == WritePasswordJobPrivate::Text ? textData.toUtf8() : binaryData.toBase64(); QByteArray service = q->service().toUtf8(); if ( !GnomeKeyring::store_network_password( GnomeKeyring::GNOME_KEYRING_DEFAULT, service.constData(), key.toUtf8().constData(), service.constData(), password.constData(), reinterpret_cast( &WritePasswordJobPrivate::gnomeKeyring_cb ), this, 0 ) ) q->emitFinishedWithError( OtherError, tr("Unknown error") ); } break; case Backend_Kwallet: if ( QDBusConnection::sessionBus().isConnected() ) { iface = new org::kde::KWallet( QLatin1String("org.kde.kwalletd"), QLatin1String("/modules/kwalletd"), QDBusConnection::sessionBus(), this ); const QDBusPendingReply reply = iface->open( QLatin1String("kdewallet"), 0, q->service() ); QDBusPendingCallWatcher* watcher = new QDBusPendingCallWatcher( reply, this ); connect( watcher, SIGNAL(finished(QDBusPendingCallWatcher*)), this, SLOT(kwalletOpenFinished(QDBusPendingCallWatcher*)) ); } else { // D-Bus is not reachable so none can tell us something about KWalletd QDBusError err( QDBusError::NoServer, tr("D-Bus is not running") ); fallbackOnError( err ); } } } QString WritePasswordJobPrivate::modeToString(Mode m) { switch (m) { case Delete: return QLatin1String("Delete"); case Text: return QLatin1String("Text"); case Binary: return QLatin1String("Binary"); } Q_ASSERT_X(false, Q_FUNC_INFO, "Unhandled Mode value"); return QString(); } WritePasswordJobPrivate::Mode WritePasswordJobPrivate::stringToMode(const QString& s) { if (s == QLatin1String("Delete") || s == QLatin1String("0")) return Delete; if (s == QLatin1String("Text") || s == QLatin1String("1")) return Text; if (s == QLatin1String("Binary") || s == QLatin1String("2")) return Binary; qCritical("Unexpected mode string '%s'", qPrintable(s)); return Text; } void WritePasswordJobPrivate::fallbackOnError(const QDBusError &err) { QScopedPointer local( !q->settings() ? new QSettings( q->service() ) : 0 ); QSettings* actual = q->settings() ? q->settings() : local.data(); if ( !q->insecureFallback() ) { q->emitFinishedWithError( OtherError, tr("Could not open wallet: %1; %2").arg( QDBusError::errorString( err.type() ), err.message() ) ); return; } if ( mode == Delete ) { actual->remove( key ); actual->sync(); q->emitFinished(); return; } actual->setValue( QString::fromLatin1( "%1/type" ).arg( key ), mode ); if ( mode == Text ) actual->setValue( QString::fromLatin1( "%1/data" ).arg( key ), textData.toUtf8() ); else if ( mode == Binary ) actual->setValue( QString::fromLatin1( "%1/data" ).arg( key ), binaryData ); actual->sync(); q->emitFinished(); } void WritePasswordJobPrivate::gnomeKeyring_cb( int result, WritePasswordJobPrivate* self ) { if ( result == GnomeKeyring::RESULT_OK ) { self->q->emitFinished(); } else { const QPair errorResult = mapGnomeKeyringError( result ); self->q->emitFinishedWithError( errorResult.first, errorResult.second ); } } void WritePasswordJobPrivate::kwalletOpenFinished( QDBusPendingCallWatcher* watcher ) { watcher->deleteLater(); QDBusPendingReply reply = *watcher; QScopedPointer local( !q->settings() ? new QSettings( q->service() ) : 0 ); QSettings* actual = q->settings() ? q->settings() : local.data(); if ( reply.isError() ) { fallbackOnError( reply.error() ); return; } if ( actual->contains( key ) ) { // If we had previously written to QSettings, but we now have a kwallet available, migrate and delete old insecure data actual->remove( key ); actual->sync(); } const int handle = reply.value(); if ( handle < 0 ) { q->emitFinishedWithError( AccessDenied, tr("Access to keychain denied") ); return; } QDBusPendingReply nextReply; if ( !textData.isEmpty() ) nextReply = iface->writePassword( handle, q->service(), key, textData, q->service() ); else if ( !binaryData.isEmpty() ) nextReply = iface->writeEntry( handle, q->service(), key, binaryData, q->service() ); else nextReply = iface->removeEntry( handle, q->service(), key, q->service() ); QDBusPendingCallWatcher* nextWatcher = new QDBusPendingCallWatcher( nextReply, this ); connect( nextWatcher, SIGNAL(finished(QDBusPendingCallWatcher*)), this, SLOT(kwalletWriteFinished(QDBusPendingCallWatcher*)) ); } void WritePasswordJobPrivate::kwalletWriteFinished( QDBusPendingCallWatcher* watcher ) { watcher->deleteLater(); QDBusPendingReply reply = *watcher; if ( reply.isError() ) { const QDBusError err = reply.error(); q->emitFinishedWithError( OtherError, tr("Could not open wallet: %1; %2").arg( QDBusError::errorString( err.type() ), err.message() ) ); return; } q->emitFinished(); }