/*************************************************************************** * Copyright (C) 2007, 2009 by Brad Hards * * * * 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 2 of the License, or * * (at your option) any later version. * ***************************************************************************/ #include "manifest.h" #include "debug.h" #include #include #include #include #include #include using namespace OOO; //--------------------------------------------------------------------- ManifestEntry::ManifestEntry( const QString &fileName ) : m_fileName( fileName ) { } void ManifestEntry::setMimeType( const QString &mimeType ) { m_mimeType = mimeType; } void ManifestEntry::setSize( const QString &size ) { m_size = size; } QString ManifestEntry::fileName() const { return m_fileName; } QString ManifestEntry::mimeType() const { return m_mimeType; } QString ManifestEntry::size() const { return m_size; } void ManifestEntry::setChecksumType( const QString &checksumType ) { m_checksumType = checksumType; } QString ManifestEntry::checksumType() const { return m_checksumType; } void ManifestEntry::setChecksum( const QString &checksum ) { m_checksum = QByteArray::fromBase64( checksum.toAscii() ); } QByteArray ManifestEntry::checksum() const { return m_checksum; } void ManifestEntry::setAlgorithm( const QString &algorithm ) { m_algorithm = algorithm; } QString ManifestEntry::algorithm() const { return m_algorithm; } void ManifestEntry::setInitialisationVector( const QString &initialisationVector ) { m_initialisationVector = QByteArray::fromBase64( initialisationVector.toAscii() ); } QByteArray ManifestEntry::initialisationVector() const { return m_initialisationVector; } void ManifestEntry::setKeyDerivationName( const QString &keyDerivationName ) { m_keyDerivationName = keyDerivationName; } QString ManifestEntry::keyDerivationName() const { return m_keyDerivationName; } void ManifestEntry::setIterationCount( const QString &iterationCount ) { m_iterationCount = iterationCount.toInt(); } int ManifestEntry::iterationCount() const { return m_iterationCount; } void ManifestEntry::setSalt( const QString &salt ) { m_salt = QByteArray::fromBase64( salt.toAscii() ); } QByteArray ManifestEntry::salt() const { return m_salt; } //--------------------------------------------------------------------- Manifest::Manifest( const QString &odfFileName, const QByteArray &manifestData, const QString &password ) : m_odfFileName( odfFileName ), m_haveGoodPassword( false ), m_password( password ) { // I don't know why the parser barfs on this. QByteArray manifestCopy = manifestData; manifestCopy.replace(QByteArray("DOCTYPE manifest:manifest"), QByteArray("DOCTYPE manifest")); QXmlStreamReader xml( manifestCopy ); ManifestEntry *currentEntry = 0; while ( ! xml.atEnd() ) { xml.readNext(); if ( (xml.tokenType() == QXmlStreamReader::NoToken) || (xml.tokenType() == QXmlStreamReader::Invalid) || (xml.tokenType() == QXmlStreamReader::StartDocument) || (xml.tokenType() == QXmlStreamReader::EndDocument) || (xml.tokenType() == QXmlStreamReader::DTD) || (xml.tokenType() == QXmlStreamReader::Characters) ) { continue; } if (xml.tokenType() == QXmlStreamReader::StartElement) { if ( xml.name().toString() == "manifest" ) { continue; } else if ( xml.name().toString() == "file-entry" ) { QXmlStreamAttributes attributes = xml.attributes(); if (currentEntry != 0) { kWarning(OooDebug) << "Got new StartElement for new file-entry, but haven't finished the last one yet!"; kWarning(OooDebug) << "processing" << currentEntry->fileName() << ", got" << attributes.value("manifest:full-path").toString(); } currentEntry = new ManifestEntry( attributes.value("manifest:full-path").toString() ); currentEntry->setMimeType( attributes.value("manifest:media-type").toString() ); currentEntry->setSize( attributes.value("manifest:size").toString() ); } else if ( xml.name().toString() == "encryption-data" ) { if (currentEntry == 0) { kWarning(OooDebug) << "Got encryption-data without valid file-entry at line" << xml.lineNumber(); continue; } QXmlStreamAttributes encryptionAttributes = xml.attributes(); currentEntry->setChecksumType( encryptionAttributes.value("manifest:checksum-type").toString() ); currentEntry->setChecksum( encryptionAttributes.value("manifest:checksum").toString() ); } else if ( xml.name().toString() == "algorithm" ) { if (currentEntry == 0) { kWarning(OooDebug) << "Got algorithm without valid file-entry at line" << xml.lineNumber(); continue; } QXmlStreamAttributes algorithmAttributes = xml.attributes(); currentEntry->setAlgorithm( algorithmAttributes.value("manifest:algorithm-name").toString() ); currentEntry->setInitialisationVector( algorithmAttributes.value("manifest:initialisation-vector").toString() ); } else if ( xml.name().toString() == "key-derivation" ) { if (currentEntry == 0) { kWarning(OooDebug) << "Got key-derivation without valid file-entry at line" << xml.lineNumber(); continue; } QXmlStreamAttributes kdfAttributes = xml.attributes(); currentEntry->setKeyDerivationName( kdfAttributes.value("manifest:key-derivation-name").toString() ); currentEntry->setIterationCount( kdfAttributes.value("manifest:iteration-count").toString() ); currentEntry->setSalt( kdfAttributes.value("manifest:salt").toString() ); } else { // handle other StartDocument types here kWarning(OooDebug) << "Unexpected start document type: " << xml.name().toString(); } } else if ( xml.tokenType() == QXmlStreamReader::EndElement ) { if ( xml.name().toString() == "manifest" ) { continue; } else if ( xml.name().toString() == "file-entry") { if (currentEntry == 0) { kWarning(OooDebug) << "Got EndElement for file-entry without valid StartElement at line" << xml.lineNumber(); continue; } // we're finished processing that file entry if ( mEntries.contains( currentEntry->fileName() ) ) { kWarning(OooDebug) << "Can't insert entry because of duplicate name:" << currentEntry->fileName(); delete currentEntry; } else { mEntries.insert( currentEntry->fileName(), currentEntry); } currentEntry = 0; } } } if (xml.hasError()) { kWarning(OooDebug) << "error: " << xml.errorString() << xml.lineNumber() << xml.columnNumber(); } } Manifest::~Manifest() { qDeleteAll( mEntries ); } ManifestEntry* Manifest::entryByName( const QString &filename ) { return mEntries.value( filename, 0 ); } bool Manifest::testIfEncrypted( const QString &filename ) { ManifestEntry *entry = entryByName( filename ); if (entry) { return ( entry->salt().length() > 0 ); } return false; } void Manifest::checkPassword( ManifestEntry *entry, const QByteArray &fileData, QByteArray *decryptedData ) { #ifdef QCA2 const QByteArray passhash = QCryptographicHash::hash(m_password.toLocal8Bit(), QCryptographicHash::Sha1 ); QCA::SymmetricKey key = QCA::PBKDF2( "sha1" ).makeKey( passhash, QCA::InitializationVector( entry->salt() ), 16, //128 bit key entry->iterationCount() ); QCA::Cipher decoder( "blowfish", QCA::Cipher::CFB, QCA::Cipher::DefaultPadding, QCA::Decode, key, QCA::InitializationVector( entry->initialisationVector() ) ); *decryptedData = decoder.update( QCA::MemoryRegion(fileData) ).toByteArray(); *decryptedData += decoder.final().toByteArray(); QByteArray csum; if ( entry->checksumType() == "SHA1/1K" ) { csum = QCryptographicHash::hash( decryptedData->left(1024), QCryptographicHash::Sha1 ); } else if ( entry->checksumType() == "SHA1" ) { csum = QCryptographicHash::hash( *decryptedData, QCryptographicHash::Sha1 ); } else { kDebug(OooDebug) << "unknown checksum type: " << entry->checksumType(); // we can only assume it will be OK. m_haveGoodPassword = true; return; } if ( entry->checksum() == csum ) { m_haveGoodPassword = true; } else { m_haveGoodPassword = false; } #else m_haveGoodPassword = false; #endif } QByteArray Manifest::decryptFile( const QString &filename, const QByteArray &fileData ) { // TODO: SHA-256 checksum type support: // http://docs.oasis-open.org/office/v1.2/os/OpenDocument-v1.2-os-part3.html#__RefHeading__752847_826425813 #ifdef QCA2 ManifestEntry *entry = entryByName( filename ); if ( ! QCA::isSupported( "sha1" ) ) { KMessageBox::error( 0, i18n("This document is encrypted, and crypto support is compiled in, but a hashing plugin could not be located") ); // in the hope that it wasn't really encrypted... return QByteArray( fileData ); } if ( ! QCA::isSupported( "pbkdf2(sha1)") ) { KMessageBox::error( 0, i18n("This document is encrypted, and crypto support is compiled in, but a key derivation plugin could not be located") ); // in the hope that it wasn't really encrypted... return QByteArray( fileData ); } if ( ! QCA::isSupported( "blowfish-cfb") ) { KMessageBox::error( 0, i18n("This document is encrypted, and crypto support is compiled in, but a cipher plugin could not be located") ); // in the hope that it wasn't really encrypted... return QByteArray( fileData ); } QByteArray decryptedData; checkPassword( entry, fileData, &decryptedData ); if (! m_haveGoodPassword ) { return QByteArray(); } QIODevice *decompresserDevice = KFilterDev::device( new QBuffer( &decryptedData, 0 ), "application/x-gzip", true ); if( !decompresserDevice ) { kDebug(OooDebug) << "Couldn't create decompressor"; // hopefully it isn't compressed then! return QByteArray( fileData ); } static_cast( decompresserDevice )->setSkipHeaders( ); decompresserDevice->open( QIODevice::ReadOnly ); return decompresserDevice->readAll(); #else // TODO: This should have a proper parent KMessageBox::error( 0, i18n("This document is encrypted, but Okular was compiled without crypto support. This document will probably not open.") ); // this is equivalent to what happened before all this Manifest stuff :-) return QByteArray( fileData ); #endif }