/* -*- mode: c++; c-basic-offset:4 -*- smartcard/readerstatus.cpp This file is part of Kleopatra, the KDE keymanager Copyright (c) 2009 Klarälvdalens Datakonsult AB Kleopatra 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 2 of the License, or (at your option) any later version. Kleopatra 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, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA In addition, as a special exception, the copyright holders give permission to link the code of this program with any edition of the Qt library by Trolltech AS, Norway (or with modified versions of Qt that use the same license as Qt), and distribute linked combinations including the two. You must obey the GNU General Public License in all respects for all of the code used other than Qt. If you modify this file, you may extend this exception to your version of the file, but you are not obligated to do so. If you do not wish to do so, delete this exception statement from your version. */ #include #include "readerstatus.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace Kleo; using namespace Kleo::SmartCard; using namespace GpgME; using namespace boost; static const unsigned int RETRY_WAIT = 2; // seconds static const unsigned int CHECK_INTERVAL = 2000; // msecs static ReaderStatus * self = 0; struct CardInfo { CardInfo() : fileName(), status( ReaderStatus::NoCard ), appType( ReaderStatus::UnknownApplication ), appVersion( -1 ) { } CardInfo( const QString & fn, ReaderStatus::Status s ) : fileName( fn ), status( s ), appType( ReaderStatus::UnknownApplication ), appVersion( -1 ) { } QString fileName; ReaderStatus::Status status; std::string serialNumber; ReaderStatus::AppType appType; int appVersion; std::vector pinStates; }; static const char * flags[] = { "NOCARD", "PRESENT", "ACTIVE", "USABLE", }; BOOST_STATIC_ASSERT(( sizeof flags/sizeof *flags == ReaderStatus::_NumScdStates )); static const char * prettyFlags[] = { "NoCard", "CardPresent", "CardActive", "CardUsable", "CardCanLearnKeys", "CardHasNullPin", "CardError", }; BOOST_STATIC_ASSERT(( sizeof prettyFlags/sizeof *prettyFlags == ReaderStatus::NumStates )); static QByteArray read_file( const QString & fileName ) { QFile file( fileName ); if ( !file.exists() ) { kDebug() << "read_file: file" << fileName << "does not exist"; return QByteArray(); } if ( !file.open( QIODevice::ReadOnly ) ) { kDebug() << "read_file: failed to open" << fileName << ':' << file.errorString(); return QByteArray(); } return file.readAll().trimmed(); } static unsigned int parseFileName( const QString & fileName, bool * ok ) { QRegExp rx( QLatin1String( "reader_(\\d+)\\.status" ) ); if ( ok ) *ok = false; if ( rx.exactMatch( QFileInfo( fileName ).fileName() ) ) return rx.cap(1).toUInt( ok, 10 ); return 0; } namespace { template std::auto_ptr dynamic_pointer_cast( std::auto_ptr & in ) { if ( T_Target * const target = dynamic_cast( in.get() ) ) { in.release(); return std::auto_ptr( target ); } else { return std::auto_ptr(); } } template const T & _trace__impl( const T & t, const char * msg ) { kDebug() << msg << t; return t; } #define TRACE( x ) _trace__impl( x, #x ) } static QDebug operator<<( QDebug s, const std::vector< std::pair > & v ) { typedef std::pair pair; s << '('; Q_FOREACH( const pair & p, v ) s << "status(" << QString::fromStdString( p.first ) << ") =" << QString::fromStdString( p.second ) << endl; return s << ')'; } static const char * app_types[] = { "_", // will hopefully never be used as an app-type :) "openpgp", "nks", "p15", "dinsig", "geldkarte", }; BOOST_STATIC_ASSERT(( sizeof app_types / sizeof *app_types == ReaderStatus::NumAppTypes )); static ReaderStatus::AppType parse_app_type( const std::string & s ) { kDebug() << "parse_app_type(" << s.c_str() << ")"; const char ** it = std::find( begin( app_types ), end( app_types ), to_lower_copy( s ) ); if ( it == end( app_types ) ) return TRACE( ReaderStatus::UnknownApplication ); return TRACE( static_cast( it - begin( app_types ) ) ); } static int parse_app_version( const std::string & s ) { return std::atoi( s.c_str() ); } static ReaderStatus::PinState parse_pin_state( const std::string & s ) { switch ( int i = std::atoi( s.c_str() ) ) { case -4: return ReaderStatus::NullPin; case -3: return ReaderStatus::PinBlocked; case -2: return ReaderStatus::NoPin; case -1: return ReaderStatus::UnknownPinState; default: if ( i < 0 ) return ReaderStatus::UnknownPinState; else return ReaderStatus::PinOk; } } static std::auto_ptr gpgagent_transact( shared_ptr & gpgAgent, const char * command, Error & err ) { #ifdef DEBUG_SCREADER kDebug() << "gpgagent_transact(" << command << ")"; #endif const AssuanResult res = gpgAgent->assuanTransact( command ); err = res.error(); if ( !err.code() ) err = res.assuanError(); if ( err.code() ) { #ifdef DEBUG_SCREADER kDebug() << "gpgagent_transact(" << command << "):" << QString::fromLocal8Bit( err.asString() ); #endif if ( err.code() >= GPG_ERR_ASS_GENERAL && err.code() <= GPG_ERR_ASS_UNKNOWN_INQUIRE ) { kDebug() << "Assuan problem, killing context"; gpgAgent.reset(); } return std::auto_ptr(); } std::auto_ptr t = gpgAgent->takeLastAssuanTransaction(); return dynamic_pointer_cast( t ); } // returns const std::string so template deduction in boost::split works, and we don't need a temporary static const std::string scd_getattr_status( shared_ptr & gpgAgent, const char * what, Error & err ) { std::string cmd = "SCD GETATTR "; cmd += what; const std::auto_ptr t = gpgagent_transact( gpgAgent, cmd.c_str(), err ); if ( t.get() ) { kDebug() << "scd_getattr_status(" << what << "): got" << t->statusLines(); return t->firstStatusLine( what ); } else { kDebug() << "scd_getattr_status(" << what << "): t == NULL"; return std::string(); } } static unsigned int parse_event_counter( const std::string & str ) { unsigned int result; if ( sscanf( str.c_str(), "%*u %*u %u ", &result ) == 1 ) return result; return -1; } static unsigned int get_event_counter( shared_ptr & gpgAgent ) { Error err; const std::auto_ptr t = gpgagent_transact( gpgAgent, "GETEVENTCOUNTER", err ); if ( err.code() ) kDebug() << "get_event_counter(): got error" << err.asString(); if ( t.get() ) { #ifdef DEBUG_SCREADER kDebug() << "get_event_counter(): got" << t->statusLines(); #endif return parse_event_counter( t->firstStatusLine( "EVENTCOUNTER" ) ); } else { kDebug() << "scd_getattr_status(): t == NULL"; return -1; } } // returns const std::string so template deduction in boost::split works, and we don't need a temporary static const std::string gpgagent_data( shared_ptr & gpgAgent, const char * what, Error & err ) { const std::auto_ptr t = gpgagent_transact( gpgAgent, what, err ); if ( t.get() ) return t->data(); else return std::string(); } static std::string parse_keypairinfo( const std::string & kpi ) { static const char hexchars[] = "0123456789abcdefABCDEF"; return '&' + kpi.substr( 0, kpi.find_first_not_of( hexchars ) ); } static bool parse_keypairinfo_and_lookup_key( Context * ctx, const std::string & kpi ) { if ( !ctx ) return false; const std::string pattern = parse_keypairinfo( kpi ); kDebug() << "parse_keypairinfo_and_lookup_key: pattern=" << pattern.c_str(); if ( const Error err = ctx->startKeyListing( pattern.c_str() ) ) { kDebug() << "parse_keypairinfo_and_lookup_key: startKeyListing failed:" << err.asString(); return false; } Error e; const Key key = ctx->nextKey( e ); ctx->endKeyListing(); kDebug() << "parse_keypairinfo_and_lookup_key: e=" << e.code() << "; key.isNull()" << key.isNull(); return !e && !key.isNull(); } static CardInfo get_card_status( const QString & fileName, unsigned int idx, shared_ptr & gpg_agent ) { #ifdef DEBUG_SCREADER kDebug() << "get_card_status(" << fileName << ',' << idx << ',' << gpg_agent.get() << ')'; #endif CardInfo ci( fileName, ReaderStatus::CardUsable ); if ( idx != 0 || !gpg_agent ) return ci; Error err; ci.serialNumber = gpgagent_data( gpg_agent, "SCD SERIALNO", err ); if ( err.code() == GPG_ERR_CARD_NOT_PRESENT || err.code() == GPG_ERR_CARD_REMOVED ) { ci.status = ReaderStatus::NoCard; return ci; } if ( err.code() ) { ci.status = ReaderStatus::CardError; return ci; } ci.appType = parse_app_type( scd_getattr_status( gpg_agent, "APPTYPE", err ) ); if ( err.code() ) return ci; if ( ci.appType != ReaderStatus::NksApplication ) { kDebug() << "get_card_status: not a NetKey card, giving up"; return ci; } ci.appVersion = parse_app_version( scd_getattr_status( gpg_agent, "NKS-VERSION", err ) ); if ( err.code() ) return ci; if ( ci.appVersion != 3 ) { kDebug() << "get_card_status: not a NetKey v3 card, giving up"; return ci; } // the following only works for NKS v3... std::vector chvStatus; chvStatus.reserve( 4 ); // expected number of fields split( chvStatus, scd_getattr_status( gpg_agent, "CHV-STATUS", err ), is_any_of( " \t" ), token_compress_on ); if ( err.code() ) return ci; std::transform( chvStatus.begin(), chvStatus.end(), std::back_inserter( ci.pinStates ), parse_pin_state ); if ( kdtools::contains( ci.pinStates, ReaderStatus::NullPin ) ) { ci.status = ReaderStatus::CardHasNullPin; return ci; } // check for keys to learn: const std::auto_ptr result = gpgagent_transact( gpg_agent, "SCD LEARN --keypairinfo", err ); if ( err.code() || !result.get() ) return ci; const std::vector keyPairInfos = result->statusLine( "KEYPAIRINFO" ); if ( keyPairInfos.empty() ) return ci; // check that any of the const std::auto_ptr klc( Context::createForProtocol( CMS ) ); // what about OpenPGP? if ( !klc.get() ) return ci; klc->setKeyListMode( Ephemeral ); if ( kdtools::any( keyPairInfos, !boost::bind( &parse_keypairinfo_and_lookup_key, klc.get(), _1 ) ) ) ci.status = ReaderStatus::CardCanLearnKeys; #ifdef DEBUG_SCREADER kDebug() << "get_card_status: ci.status " << prettyFlags[ci.status]; #endif return ci; } static std::vector update_cardinfo( const QString & gnupgHomePath, shared_ptr & gpgAgent ) { #ifdef DEBUG_SCREADER kDebug() << ""; #endif const QDir gnupgHome( gnupgHomePath ); if ( !gnupgHome.exists() ) kWarning() << "gnupg home" << gnupgHomePath << "does not exist!"; const CardInfo ci = get_card_status( gnupgHome.absoluteFilePath( QLatin1String( "reader_0.status" ) ), 0, gpgAgent ); #ifdef DEBUG_SCREADER kDebug() << ""; #endif return std::vector( 1, ci ); } static bool check_event_counter_changed( shared_ptr & gpg_agent, unsigned int & counter ) { const unsigned int oldCounter = counter; counter = get_event_counter( gpg_agent ); if ( oldCounter != counter ) { #ifdef DEBUG_SCREADER kDebug() << "ReaderStatusThread[2nd]: events:" << oldCounter << "->" << counter ; #endif return true; } else { return false; } } struct Transaction { QByteArray command; QPointer receiver; const char * slot; GpgME::Error error; }; static const Transaction checkTransaction = { "__check__", 0, 0, Error() }; static const Transaction updateTransaction = { "__update__", 0, 0, Error() }; static const Transaction quitTransaction = { "__quit__", 0, 0, Error() }; namespace { class ReaderStatusThread : public QThread { Q_OBJECT public: explicit ReaderStatusThread( QObject * parent=0 ) : QThread( parent ), m_gnupgHomePath( Kleo::gnupgHomeDirectory() ), m_transactions( 1, updateTransaction ) // force initial scan { connect( this, SIGNAL(oneTransactionFinished()), this, SLOT(slotOneTransactionFinished()) ); } std::vector cardInfos() const { const QMutexLocker locker( &m_mutex ); return m_cardInfos; } ReaderStatus::Status cardStatus( unsigned int slot ) const { const QMutexLocker locker( &m_mutex ); if ( slot < m_cardInfos.size() ) return m_cardInfos[slot].status; else return ReaderStatus::NoCard; } void addTransaction( const Transaction & t ) { const QMutexLocker locker( &m_mutex ); m_transactions.push_back( t ); m_waitForTransactions.wakeOne(); } // make QThread::sleep public using QThread::sleep; Q_SIGNALS: void anyCardHasNullPinChanged( bool ); void anyCardCanLearnKeysChanged( bool ); void cardStatusChanged( unsigned int, Kleo::SmartCard::ReaderStatus::Status ); void oneTransactionFinished(); public Q_SLOTS: void ping() { kDebug() << "ReaderStatusThread[GUI]::ping()"; addTransaction( updateTransaction ); } void stop() { const QMutexLocker locker( &m_mutex ); m_transactions.push_front( quitTransaction ); m_waitForTransactions.wakeOne(); } void slotReaderStatusFileChanged() { const QDir gnupgHome( m_gnupgHomePath ); if ( !gnupgHome.exists() ) { kWarning() << "gnupg home" << m_gnupgHomePath << "does not exist!"; return; } QStringList files = gnupgHome.entryList( QStringList( QLatin1String( "reader_*.status" ) ), QDir::Files, QDir::Name ); bool * dummy = 0; kdtools::sort( files, boost::bind( parseFileName, _1, dummy ) < boost::bind( parseFileName, _2, dummy ) ); std::vector contents; Q_FOREACH( const QString & file, files ) { bool ok = false; const unsigned int idx = parseFileName( file, &ok ); if ( !ok ) { kDebug() << "filename" << file << ": cannot parse reader slot number"; continue; } assert( idx >= contents.size() ); contents.resize( idx ); contents.push_back( read_file( gnupgHome.absoluteFilePath( file ) ) ); } // canonicalise by removing empty stuff from the end while ( !contents.empty() && contents.back().isEmpty() ) contents.pop_back(); if ( contents != readerStatusFileContents ) ping(); readerStatusFileContents.swap( contents ); } private Q_SLOTS: void slotOneTransactionFinished() { std::list ft; KDAB_SYNCHRONIZED( m_mutex ) ft.splice( ft.begin(), m_finishedTransactions ); Q_FOREACH( const Transaction & t, ft ) if ( t.receiver && t.slot && *t.slot ) QMetaObject::invokeMethod( t.receiver, t.slot, Qt::DirectConnection, Q_ARG( GpgME::Error, t.error ) ); } private: /* reimp */ void run() { shared_ptr gpgAgent; unsigned int eventCounter = -1; while ( true ) { QByteArray command; bool nullSlot; std::list item; std::vector oldCardInfos; if ( !gpgAgent ) { Error err; std::auto_ptr c = Context::createForEngine( AssuanEngine, &err ); if ( err.code() == GPG_ERR_NOT_SUPPORTED ) return; gpgAgent = c; } KDAB_SYNCHRONIZED( m_mutex ) { while ( m_transactions.empty() ) { // go to sleep waiting for more work: #ifdef DEBUG_SCREADER kDebug() << "ReaderStatusThread[2nd]: .zZZ"; #endif if ( !m_waitForTransactions.wait( &m_mutex, CHECK_INTERVAL ) ) m_transactions.push_front( checkTransaction ); #ifdef DEBUG_SCREADER kDebug() << "ReaderStatusThread[2nd]: .oOO"; #endif } // splice off the first transaction without // copying, so we own it without really importing // it into this thread (the QPointer isn't // thread-safe): item.splice( item.end(), m_transactions, m_transactions.begin() ); // make local copies of the interesting stuff so // we can release the mutex again: command = item.front().command; nullSlot = !item.front().slot; oldCardInfos = m_cardInfos; } #ifdef DEBUG_SCREADER kDebug() << "ReaderStatusThread[2nd]: new iteration command=" << command << " ; nullSlot=" << nullSlot; #endif // now, let's see what we got: if ( nullSlot && command == quitTransaction.command ) return; // quit if ( nullSlot && command == updateTransaction.command || nullSlot && command == checkTransaction.command ) { if ( nullSlot && command == checkTransaction.command && !check_event_counter_changed( gpgAgent, eventCounter ) ) continue; // early out std::vector newCardInfos = update_cardinfo( m_gnupgHomePath, gpgAgent ); newCardInfos.resize( std::max( newCardInfos.size(), oldCardInfos.size() ) ); oldCardInfos.resize( std::max( newCardInfos.size(), oldCardInfos.size() ) ); KDAB_SYNCHRONIZED( m_mutex ) m_cardInfos = newCardInfos; std::vector::const_iterator nit = newCardInfos.begin(), nend = newCardInfos.end(), oit = oldCardInfos.begin(), oend = oldCardInfos.end() ; unsigned int idx = 0; bool anyLC = false; bool anyNP = false; bool anyError = false; while ( nit != nend && oit != oend ) { if ( nit->status != oit->status ) { #ifdef DEBUG_SCREADER kDebug() << "ReaderStatusThread[2nd]: slot" << idx << ":" << prettyFlags[oit->status] << "->" << prettyFlags[nit->status]; #endif emit cardStatusChanged( idx, nit->status ); } if ( nit->status == ReaderStatus::CardCanLearnKeys ) anyLC = true; if ( nit->status == ReaderStatus::CardHasNullPin ) anyNP = true; if ( nit->status == ReaderStatus::CardError ) anyError = true; ++nit; ++oit; ++idx; } emit anyCardHasNullPinChanged( anyNP ); emit anyCardCanLearnKeysChanged( anyLC ); if ( anyError ) gpgAgent.reset(); } else { (void)gpgagent_transact( gpgAgent, command.constData(), item.front().error ); KDAB_SYNCHRONIZED( m_mutex ) // splice 'item' into m_finishedTransactions: m_finishedTransactions.splice( m_finishedTransactions.end(), item ); emit oneTransactionFinished(); } // update event counter in case anything above changed // it: if ( gpgAgent ) eventCounter = get_event_counter( gpgAgent ); else eventCounter = -1; #ifdef DEBUG_SCREADER kDebug() << "eventCounter:" << eventCounter; #endif } } private: mutable QMutex m_mutex; QWaitCondition m_waitForTransactions; const QString m_gnupgHomePath; std::vector readerStatusFileContents; // protected by m_mutex: std::vector m_cardInfos; std::list m_transactions, m_finishedTransactions; }; } class ReaderStatus::Private : ReaderStatusThread { friend class Kleo::SmartCard::ReaderStatus; ReaderStatus * const q; public: explicit Private( ReaderStatus * qq ) : ReaderStatusThread( qq ), q( qq ), watcher() { KDAB_SET_OBJECT_NAME( watcher ); qRegisterMetaType( "Kleo::SmartCard::ReaderStatus::Status" ); watcher.whitelistFiles( QStringList( QLatin1String( "reader_*.status" ) ) ); watcher.addPath( Kleo::gnupgHomeDirectory() ); watcher.setDelay( 100 ); connect( this, SIGNAL(cardStatusChanged(uint,Kleo::SmartCard::ReaderStatus::Status)), q, SIGNAL(cardStatusChanged(uint,Kleo::SmartCard::ReaderStatus::Status)) ); connect( this, SIGNAL(anyCardHasNullPinChanged(bool)), q, SIGNAL(anyCardHasNullPinChanged(bool)) ); connect( this, SIGNAL(anyCardCanLearnKeysChanged(bool)), q, SIGNAL(anyCardCanLearnKeysChanged(bool)) ); connect( &watcher, SIGNAL(triggered()), this, SLOT(slotReaderStatusFileChanged()) ); } ~Private() { stop(); if ( !wait( 100 ) ) { terminate(); wait(); } } private: bool anyCardHasNullPinImpl() const { return kdtools::any( cardInfos(), boost::bind( &CardInfo::status, _1 ) == CardHasNullPin ); } bool anyCardCanLearnKeysImpl() const { return kdtools::any( cardInfos(), boost::bind( &CardInfo::status, _1 ) == CardCanLearnKeys ); } private: FileSystemWatcher watcher; }; ReaderStatus::ReaderStatus( QObject * parent ) : QObject( parent ), d( new Private( this ) ) { self = this; } ReaderStatus::~ReaderStatus() { self = 0; } // slot void ReaderStatus::startMonitoring() { d->start(); } // static ReaderStatus * ReaderStatus::mutableInstance() { return self; } // static const ReaderStatus * ReaderStatus::instance() { return self; } ReaderStatus::Status ReaderStatus::cardStatus( unsigned int slot ) const { return d->cardStatus( slot ); } bool ReaderStatus::anyCardHasNullPin() const { return d->anyCardHasNullPinImpl(); } bool ReaderStatus::anyCardCanLearnKeys() const { return d->anyCardCanLearnKeysImpl(); } std::vector ReaderStatus::pinStates( unsigned int slot ) const { const std::vector ci = d->cardInfos(); if ( slot < ci.size() ) return ci[slot].pinStates; else return std::vector(); } void ReaderStatus::startSimpleTransaction( const QByteArray & command, QObject * receiver, const char * slot ) { const Transaction t = { command, receiver, slot, Error() }; d->addTransaction( t ); } void ReaderStatus::updateStatus() { d->ping(); } #include "readerstatus.moc"