1 /**************************************************************************
2 *   Copyright (C) 2009-2011 Matthias Fuchs <mat69@gmx.net>                *
3 *                                                                         *
4 *   This program is free software; you can redistribute it and/or modify  *
5 *   it under the terms of the GNU General Public License as published by  *
6 *   the Free Software Foundation; either version 2 of the License, or     *
7 *   (at your option) any later version.                                   *
8 *                                                                         *
9 *   This program is distributed in the hope that it will be useful,       *
10 *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
11 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
12 *   GNU General Public License for more details.                          *
13 *                                                                         *
14 *   You should have received a copy of the GNU General Public License     *
15 *   along with this program; if not, write to the                         *
16 *   Free Software Foundation, Inc.,                                       *
17 *   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA .        *
18 ***************************************************************************/
19 
20 #include "verifier_p.h"
21 #include "verificationmodel.h"
22 #include "../dbus/dbusverifierwrapper.h"
23 #include "verifieradaptor.h"
24 #include "settings.h"
25 
26 #include <QFile>
27 #include <QDomElement>
28 
29 #include "kget_debug.h"
30 #include <QDebug>
31 
32 #ifdef HAVE_QCA2
33 #include <qca_basic.h>
34 #endif
35 
36 //TODO use mutable to make some methods const?
37 const QStringList VerifierPrivate::SUPPORTED = (QStringList() << "sha512" << "sha384" << "sha256" << "ripmed160" << "sha1" << "md5" << "md4");
38 const QString VerifierPrivate::MD5 = QString("md5");
39 const int VerifierPrivate::DIGGESTLENGTH[] = {128, 96, 64, 40, 40, 32, 32};
40 const int VerifierPrivate::MD5LENGTH = 32;
41 const int VerifierPrivate::PARTSIZE = 500 * 1024;
42 
~VerifierPrivate()43 VerifierPrivate::~VerifierPrivate()
44 {
45     delete model;
46     qDeleteAll(partialSums.begin(), partialSums.end());
47 }
48 
calculatePartialChecksum(QFile * file,const QString & type,KIO::fileoffset_t startOffset,int pieceLength,KIO::filesize_t fileSize,bool * abortPtr)49 QString VerifierPrivate::calculatePartialChecksum(QFile *file, const QString &type, KIO::fileoffset_t startOffset, int pieceLength, KIO::filesize_t fileSize, bool *abortPtr)
50 {
51     if (!file)
52     {
53         return QString();
54     }
55 
56     if (!fileSize)
57     {
58         fileSize = file->size();
59     }
60     //longer than the file, so adapt it
61     if (static_cast<KIO::fileoffset_t>(fileSize) < startOffset + pieceLength)
62     {
63         pieceLength = fileSize - startOffset;
64     }
65 
66 #ifdef HAVE_QCA2
67     QCA::Hash hash(type);
68 
69     //it can be that QCA2 does not support md5, e.g. when Qt is compiled locally
70     QCryptographicHash md5Hash(QCryptographicHash::Md5);
71     const bool useMd5 = (type == MD5);
72 #else //NO QCA2
73     if (type != MD5)
74     {
75         return QString();
76     }
77     QCryptographicHash hash(QCryptographicHash::Md5);
78 #endif //HAVE_QCA2
79 
80     //we only read 512kb each time, to save RAM
81     int numData = pieceLength / PARTSIZE;
82     KIO::fileoffset_t dataRest = pieceLength % PARTSIZE;
83 
84     if (!numData && !dataRest)
85     {
86         return QString();
87     }
88 
89     int k = 0;
90     for (k = 0; k < numData; ++k)
91     {
92         if (!file->seek(startOffset + PARTSIZE * k))
93         {
94             return QString();
95         }
96 
97         if (abortPtr && *abortPtr)
98         {
99             return QString();
100         }
101 
102         QByteArray data = file->read(PARTSIZE);
103 #ifdef HAVE_QCA2
104         if (useMd5) {
105             md5Hash.addData(data);
106         } else {
107             hash.update(data);
108         }
109 #else //NO QCA2
110         hash.addData(data);
111 #endif //HAVE_QCA2
112     }
113 
114     //now read the rest
115     if (dataRest)
116     {
117         if (!file->seek(startOffset + PARTSIZE * k))
118         {
119             return QString();
120         }
121 
122         QByteArray data = file->read(dataRest);
123 #ifdef HAVE_QCA2
124         if (useMd5) {
125             md5Hash.addData(data);
126         } else {
127             hash.update(data);
128         }
129 #else //NO QCA2
130         hash.addData(data);
131 #endif //HAVE_QCA2
132     }
133 
134 #ifdef HAVE_QCA2
135     return (useMd5 ? QString(md5Hash.result()) : QString(QCA::arrayToHex(hash.final().toByteArray())));
136 #else //NO QCA2
137     return QString(hash.result());
138 #endif //HAVE_QCA2
139 }
140 
orderChecksumTypes(Verifier::ChecksumStrength strength) const141 QStringList VerifierPrivate::orderChecksumTypes(Verifier::ChecksumStrength strength) const
142 {
143     QStringList checksumTypes;
144     if (strength == Verifier::Weak) {
145         for (int i = SUPPORTED.count() - 1; i >= 0; --i) {
146             checksumTypes.append(SUPPORTED.at(i));
147         }
148         checksumTypes.move(0, 1); //md4 second position
149     } else if (strength == Verifier::Strong) {
150         for (int i = SUPPORTED.count() - 1; i >= 0; --i) {
151             checksumTypes.append(SUPPORTED.at(i));
152         }
153         checksumTypes.move(1, checksumTypes.count() - 1); //md5 second last position
154         checksumTypes.move(0, checksumTypes.count() - 1); //md4 last position
155     } else if (strength == Verifier::Strongest) {
156         checksumTypes = SUPPORTED;
157     }
158 
159     return checksumTypes;
160 }
161 
Verifier(const QUrl & dest,QObject * parent)162 Verifier::Verifier(const QUrl &dest, QObject *parent)
163   : QObject(parent),
164     d(new VerifierPrivate(this))
165 {
166     d->dest = dest;
167     d->status = NoResult;
168 
169     static int dBusObjIdx = 0;
170     d->dBusObjectPath = "/KGet/Verifiers/" + QString::number(dBusObjIdx++);
171 
172     auto *wrapper = new DBusVerifierWrapper(this);
173     new VerifierAdaptor(wrapper);
174     QDBusConnection::sessionBus().registerObject(d->dBusObjectPath, wrapper);
175 
176     qRegisterMetaType<KIO::filesize_t>("KIO::filesize_t");
177     qRegisterMetaType<KIO::fileoffset_t>("KIO::fileoffset_t");
178     qRegisterMetaType<QList<KIO::fileoffset_t> >("QList<KIO::fileoffset_t>");
179 
180     d->model = new VerificationModel();
181     connect(&d->thread, SIGNAL(verified(QString,bool,QUrl)), this, SLOT(changeStatus(QString,bool)));
182     connect(&d->thread, SIGNAL(brokenPieces(QList<KIO::fileoffset_t>,KIO::filesize_t)), this, SIGNAL(brokenPieces(QList<KIO::fileoffset_t>,KIO::filesize_t)));
183 }
184 
~Verifier()185 Verifier::~Verifier()
186 {
187     delete d;
188 }
189 
dBusObjectPath() const190 QString Verifier::dBusObjectPath() const
191 {
192     return d->dBusObjectPath;
193 }
194 
destination() const195 QUrl Verifier::destination() const
196 {
197     return d->dest;
198 }
199 
setDestination(const QUrl & destination)200 void Verifier::setDestination(const QUrl &destination)
201 {
202     d->dest = destination;
203 }
204 
status() const205 Verifier::VerificationStatus Verifier::status() const
206 {
207     return d->status;
208 }
209 
model()210 VerificationModel *Verifier::model()
211 {
212     return d->model;
213 }
214 
supportedVerficationTypes()215 QStringList Verifier::supportedVerficationTypes()
216 {
217     QStringList supported;
218 #ifdef HAVE_QCA2
219     QStringList supportedTypes = QCA::Hash::supportedTypes();
220     for (int i = 0; i < VerifierPrivate::SUPPORTED.count(); ++i)
221     {
222         if (supportedTypes.contains(VerifierPrivate::SUPPORTED.at(i)))
223         {
224             supported << VerifierPrivate::SUPPORTED.at(i);
225         }
226     }
227 #endif //HAVE_QCA2
228 
229     if (!supported.contains(VerifierPrivate::MD5))
230     {
231         supported << VerifierPrivate::MD5;
232     }
233 
234     return supported;
235 
236 }
237 
diggestLength(const QString & type)238 int Verifier::diggestLength(const QString &type)
239 {
240     if (type == VerifierPrivate::MD5)
241     {
242         return VerifierPrivate::MD5LENGTH;
243     }
244 
245 #ifdef HAVE_QCA2
246     if (QCA::isSupported(type.toLatin1()))
247     {
248         return VerifierPrivate::DIGGESTLENGTH[VerifierPrivate::SUPPORTED.indexOf(type)];
249     }
250 #endif //HAVE_QCA2
251 
252     return 0;
253 }
254 
isChecksum(const QString & type,const QString & checksum)255 bool Verifier::isChecksum(const QString &type, const QString &checksum)
256 {
257     const int length = diggestLength(type);
258     const QString pattern = QString("[0-9a-z]{%1}").arg(length);
259     //needs correct length and only word characters
260     if (length && (checksum.length() == length) && checksum.toLower().contains(QRegExp(pattern)))
261     {
262         return true;
263     }
264 
265     return false;
266 }
267 
cleanChecksumType(const QString & type)268 QString Verifier::cleanChecksumType(const QString &type)
269 {
270     QString hashType = type.toUpper();
271     if (hashType.contains(QRegExp("^SHA\\d+"))) {
272         hashType.insert(3, '-');
273     }
274 
275     return hashType;
276 }
277 
isVerifyable() const278 bool Verifier::isVerifyable() const
279 {
280     return QFile::exists(d->dest.toString()) && d->model->rowCount();
281 }
282 
isVerifyable(const QModelIndex & index) const283 bool Verifier::isVerifyable(const QModelIndex &index) const
284 {
285     int row = -1;
286     if (index.isValid())
287     {
288         row = index.row();
289     }
290     if (QFile::exists(d->dest.toString()) && (row >= 0) && (row < d->model->rowCount()))
291     {
292         return true;
293     }
294     return false;
295 }
296 
availableChecksum(Verifier::ChecksumStrength strength) const297 Checksum Verifier::availableChecksum(Verifier::ChecksumStrength strength) const
298 {
299     Checksum pair;
300 
301     //check if there is at least one entry
302     QModelIndex index = d->model->index(0, 0);
303     if (!index.isValid())
304     {
305         return pair;
306     }
307 
308     const QStringList available = supportedVerficationTypes();
309     const QStringList supported = d->orderChecksumTypes(strength);
310     for (int i = 0; i < supported.count(); ++i) {
311         QModelIndexList indexList = d->model->match(index, Qt::DisplayRole, supported.at(i));
312         if (!indexList.isEmpty() && available.contains(supported.at(i))) {
313             QModelIndex match = d->model->index(indexList.first().row(), VerificationModel::Checksum);
314             pair.first = supported.at(i);
315             pair.second = match.data().toString();
316             break;
317         }
318     }
319 
320     return pair;
321 }
322 
availableChecksums() const323 QList<Checksum> Verifier::availableChecksums() const
324 {
325     QList<Checksum> checksums;
326 
327     for (int i = 0; i < d->model->rowCount(); ++i) {
328         const QString type = d->model->index(i, VerificationModel::Type).data().toString();
329         const QString hash = d->model->index(i, VerificationModel::Checksum).data().toString();
330         checksums << qMakePair(type, hash);
331     }
332 
333     return checksums;
334 }
335 
availablePartialChecksum(Verifier::ChecksumStrength strength) const336 QPair<QString, PartialChecksums*> Verifier::availablePartialChecksum(Verifier::ChecksumStrength strength) const
337 {
338     QPair<QString, PartialChecksums*> pair;
339     QString type;
340     PartialChecksums *checksum = nullptr;
341 
342     const QStringList available = supportedVerficationTypes();
343     const QStringList supported = d->orderChecksumTypes(strength);
344     for (int i = 0; i < supported.size(); ++i) {
345         if (d->partialSums.contains(supported.at(i)) && available.contains(supported.at(i))) {
346             type = supported.at(i);
347             checksum =  d->partialSums[type];
348             break;
349         }
350     }
351 
352     return QPair<QString, PartialChecksums*>(type, checksum);
353 }
354 
changeStatus(const QString & type,bool isVerified)355 void Verifier::changeStatus(const QString &type, bool isVerified)
356 {
357     qCDebug(KGET_DEBUG) << "Verified:" << isVerified;
358     d->status = isVerified ? Verifier::Verified : Verifier::NotVerified;
359     d->model->setVerificationStatus(type, d->status);
360     Q_EMIT verified(isVerified);
361 }
362 
verify(const QModelIndex & index)363 void Verifier::verify(const QModelIndex &index)
364 {
365     int row = -1;
366     if (index.isValid()) {
367         row = index.row();
368     }
369 
370     QString type;
371     QString checksum;
372 
373     if (row == -1) {
374         Checksum pair = availableChecksum(static_cast<Verifier::ChecksumStrength>(Settings::checksumStrength()));
375         type = pair.first;
376         checksum = pair.second;
377     } else if ((row >= 0) && (row < d->model->rowCount())) {
378         type = d->model->index(row, VerificationModel::Type).data().toString();
379         checksum = d->model->index(row, VerificationModel::Checksum).data().toString();
380     }
381 
382     d->thread.verify(type, checksum, d->dest);
383 }
384 
brokenPieces() const385 void Verifier::brokenPieces() const
386 {
387     QPair<QString, PartialChecksums*> pair = availablePartialChecksum(static_cast<Verifier::ChecksumStrength>(Settings::checksumStrength()));
388     QList<QString> checksums;
389     KIO::filesize_t length = 0;
390     if (pair.second) {
391         checksums = pair.second->checksums();
392         length = pair.second->length();
393     }
394     d->thread.findBrokenPieces(pair.first, checksums, length, d->dest);
395 }
396 
checksum(const QUrl & dest,const QString & type,bool * abortPtr)397 QString Verifier::checksum(const QUrl &dest, const QString &type, bool *abortPtr)
398 {
399     QStringList supported = supportedVerficationTypes();
400     if (!supported.contains(type))
401     {
402         return QString();
403     }
404 
405     QFile file(dest.toString());
406     if (!file.open(QIODevice::ReadOnly))
407     {
408         return QString();
409     }
410 
411     if (type == VerifierPrivate::MD5) {
412         QCryptographicHash hash(QCryptographicHash::Md5);
413         hash.addData(&file);
414         QString final = QString(hash.result());
415         file.close();
416         return final;
417     }
418 
419 
420 #ifdef HAVE_QCA2
421     QCA::Hash hash(type);
422 
423     //BEGIN taken from qca_basic.h and slightly adopted to allow abort
424     char buffer[1024];
425     int len;
426 
427     while ((len=file.read(reinterpret_cast<char*>(buffer), sizeof(buffer))) > 0)
428     {
429         hash.update(buffer, len);
430         if (abortPtr && *abortPtr)
431         {
432             hash.final();
433             file.close();
434             return QString();
435         }
436     }
437     //END
438 
439     QString final = QString(QCA::arrayToHex(hash.final().toByteArray()));
440     file.close();
441     return final;
442 #endif //HAVE_QCA2
443 
444     return QString();
445 }
446 
partialChecksums(const QUrl & dest,const QString & type,KIO::filesize_t length,bool * abortPtr)447 PartialChecksums Verifier::partialChecksums(const QUrl &dest, const QString &type, KIO::filesize_t length, bool *abortPtr)
448 {
449     QStringList checksums;
450 
451     QStringList supported = supportedVerficationTypes();
452     if (!supported.contains(type))
453     {
454         return PartialChecksums();
455     }
456 
457     QFile file(dest.toString());
458     if (!file.open(QIODevice::ReadOnly))
459     {
460         return PartialChecksums();
461     }
462 
463     const KIO::filesize_t fileSize = file.size();
464     if (!fileSize)
465     {
466         return PartialChecksums();
467     }
468 
469     int numPieces = 0;
470 
471     //the piece length has been defined
472     if (length)
473     {
474         numPieces = fileSize / length;
475     }
476     else
477     {
478         length = VerifierPrivate::PARTSIZE;
479         numPieces = fileSize / length;
480         if (numPieces > 100)
481         {
482             numPieces = 100;
483             length = fileSize / numPieces;
484         }
485     }
486 
487     //there is a rest, so increase numPieces by one
488     if (fileSize % length)
489     {
490         ++numPieces;
491     }
492 
493     PartialChecksums partialChecksums;
494 
495     //create all the checksums for the pieces
496     for (int i = 0; i < numPieces; ++i)
497     {
498         QString hash = VerifierPrivate::calculatePartialChecksum(&file, type, length * i, length, fileSize, abortPtr);
499         if (hash.isEmpty())
500         {
501             file.close();
502             return PartialChecksums();
503         }
504         checksums.append(hash);
505     }
506 
507     partialChecksums.setLength(length);
508     partialChecksums.setChecksums(checksums);
509     file.close();
510     return partialChecksums;
511 }
512 
addChecksum(const QString & type,const QString & checksum,int verified)513 void Verifier::addChecksum(const QString &type, const QString &checksum, int verified)
514 {
515     d->model->addChecksum(type, checksum, verified);
516 }
517 
addChecksums(const QHash<QString,QString> & checksums)518 void Verifier::addChecksums(const QHash<QString, QString> &checksums)
519 {
520     d->model->addChecksums(checksums);
521 }
522 
addPartialChecksums(const QString & type,KIO::filesize_t length,const QStringList & checksums)523 void Verifier::addPartialChecksums(const QString &type, KIO::filesize_t length, const QStringList &checksums)
524 {
525     if (!d->partialSums.contains(type) && length && !checksums.isEmpty())
526     {
527         d->partialSums[type] = new PartialChecksums(length, checksums);
528     }
529 }
530 
partialChunkLength() const531 KIO::filesize_t Verifier::partialChunkLength() const
532 {
533     QStringList::const_iterator it;
534     QStringList::const_iterator itEnd = VerifierPrivate::SUPPORTED.constEnd();
535     for (it = VerifierPrivate::SUPPORTED.constBegin(); it != itEnd; ++it)
536     {
537         if (d->partialSums.contains(*it))
538         {
539             return d->partialSums[*it]->length();
540         }
541     }
542 
543     return 0;
544 }
545 
save(const QDomElement & element)546 void Verifier::save(const QDomElement &element)
547 {
548     QDomElement e = element;
549     e.setAttribute("verificationStatus", d->status);
550 
551     QDomElement verification = e.ownerDocument().createElement("verification");
552     for (int i = 0; i < d->model->rowCount(); ++i)
553     {
554         QDomElement hash = e.ownerDocument().createElement("hash");
555         hash.setAttribute("type", d->model->index(i, VerificationModel::Type).data().toString());
556         hash.setAttribute("verified", d->model->index(i, VerificationModel::Verified).data(Qt::EditRole).toInt());
557         QDomText value = e.ownerDocument().createTextNode(d->model->index(i, VerificationModel::Checksum).data().toString());
558         hash.appendChild(value);
559         verification.appendChild(hash);
560     }
561 
562     QHash<QString, PartialChecksums*>::const_iterator it;
563     QHash<QString, PartialChecksums*>::const_iterator itEnd = d->partialSums.constEnd();
564     for (it = d->partialSums.constBegin(); it != itEnd; ++it)
565     {
566         QDomElement pieces = e.ownerDocument().createElement("pieces");
567         pieces.setAttribute("type", it.key());
568         pieces.setAttribute("length", (*it)->length());
569         QList<QString> checksums = (*it)->checksums();
570         for (int i = 0; i < checksums.size(); ++i)
571         {
572             QDomElement hash = e.ownerDocument().createElement("hash");
573             hash.setAttribute("piece", i);
574             QDomText value = e.ownerDocument().createTextNode(checksums[i]);
575             hash.appendChild(value);
576             pieces.appendChild(hash);
577         }
578         verification.appendChild(pieces);
579     }
580     e.appendChild(verification);
581 }
582 
load(const QDomElement & e)583 void Verifier::load(const QDomElement &e)
584 {
585     if (e.hasAttribute("verificationStatus"))
586     {
587         const int status = e.attribute("verificationStatus").toInt();
588         switch (status)
589         {
590             case NoResult:
591                 d->status = NoResult;
592                 break;
593             case NotVerified:
594                 d->status = NotVerified;
595                 break;
596             case Verified:
597                 d->status = Verified;
598                 break;
599             default:
600                 d->status = NotVerified;
601                 break;
602         }
603     }
604 
605     QDomElement verification = e.firstChildElement("verification");
606     QDomNodeList const hashList = verification.elementsByTagName("hash");
607 
608     for (int i = 0; i < hashList.length(); ++i)
609     {
610         const QDomElement hash = hashList.item(i).toElement();
611         const QString value = hash.text();
612         const QString type = hash.attribute("type");
613         const int verificationStatus = hash.attribute("verified").toInt();
614         if (!type.isEmpty() && !value.isEmpty())
615         {
616             d->model->addChecksum(type, value, verificationStatus);
617         }
618     }
619 
620     QDomNodeList const piecesList = verification.elementsByTagName("pieces");
621 
622     for (int i = 0; i < piecesList.length(); ++i)
623     {
624         QDomElement pieces = piecesList.at(i).toElement();
625 
626         const QString type = pieces.attribute("type");
627         const KIO::filesize_t length = pieces.attribute("length").toULongLong();
628         QStringList partialChecksums;
629 
630         const QDomNodeList partialHashList = pieces.elementsByTagName("hash");
631         for (int j = 0; j < partialHashList.size(); ++j)//TODO give this function the size of the file, to calculate how many hashs are needed as an additional check, do that check in addPartialChecksums?!
632         {
633             const QString hash = partialHashList.at(j).toElement().text();
634             if (hash.isEmpty())
635             {
636                 break;
637             }
638             partialChecksums.append(hash);
639         }
640 
641         addPartialChecksums(type, length, partialChecksums);
642     }
643 }
644 
645 
646