CLI: Add support for okon in offline HIBP checks

* Closes #5447
* Add option `--okon <okon-cli path>` to trigger the use of the okon cli tool to process a database's entries. When using this option the `-H, --hibp` option must point to a post-processed okon file instead of the standard HIBP text file.
* Updated documentation
This commit is contained in:
Jonathan White 2020-09-27 09:00:59 -04:00
parent e1c2537084
commit 7426693f1d
5 changed files with 92 additions and 13 deletions

View File

@ -37,7 +37,7 @@ It provides the ability to query and modify the entries of a KeePass database, d
The same password generation options as documented for the generate command can be used when the *-g* option is set. The same password generation options as documented for the generate command can be used when the *-g* option is set.
*analyze* [_options_] <__database__>:: *analyze* [_options_] <__database__>::
Analyzes passwords in a database for weaknesses. Analyzes passwords in a database for weaknesses using offline HIBP SHA-1 hash lookup.
*clip* [_options_] <__database__> <__entry__> [_timeout_]:: *clip* [_options_] <__database__> <__entry__> [_timeout_]::
Copies an attribute or the current TOTP (if the *-t* option is specified) of a database entry to the clipboard. Copies an attribute or the current TOTP (if the *-t* option is specified) of a database entry to the clipboard.
@ -199,6 +199,10 @@ The same password generation options as documented for the generate command can
Such files are available from https://haveibeenpwned.com/Passwords; Such files are available from https://haveibeenpwned.com/Passwords;
note that they are large, and so this operation typically takes some time (minutes up to an hour or so). note that they are large, and so this operation typically takes some time (minutes up to an hour or so).
*--okon* <__okon-cli path__>::
Use the specified okon-cli program to perform offline breach checks. You can obtain okon-cli from https://github.com/stryku/okon.
When using this option, *-H, --hibp* must point to a post-processed okon file (e.g. file.okon).
=== Clip options === Clip options
*-a*, *--attribute*:: *-a*, *--attribute*::
Copies the specified attribute to the clipboard. Copies the specified attribute to the clipboard.

View File

@ -34,11 +34,17 @@ const QCommandLineOption Analyze::HIBPDatabaseOption = QCommandLineOption(
"https://haveibeenpwned.com/Passwords."), "https://haveibeenpwned.com/Passwords."),
QObject::tr("FILENAME")); QObject::tr("FILENAME"));
const QCommandLineOption Analyze::OkonOption =
QCommandLineOption("okon",
QObject::tr("Path to okon-cli to search a formatted HIBP file"),
QObject::tr("okon-cli"));
Analyze::Analyze() Analyze::Analyze()
{ {
name = QString("analyze"); name = QString("analyze");
description = QObject::tr("Analyze passwords for weaknesses and problems."); description = QObject::tr("Analyze passwords for weaknesses and problems.");
options.append(Analyze::HIBPDatabaseOption); options.append(Analyze::HIBPDatabaseOption);
options.append(Analyze::OkonOption);
} }
int Analyze::executeWithDatabase(QSharedPointer<Database> database, QSharedPointer<QCommandLineParser> parser) int Analyze::executeWithDatabase(QSharedPointer<Database> database, QSharedPointer<QCommandLineParser> parser)
@ -46,20 +52,36 @@ int Analyze::executeWithDatabase(QSharedPointer<Database> database, QSharedPoint
auto& out = Utils::STDOUT; auto& out = Utils::STDOUT;
auto& err = Utils::STDERR; auto& err = Utils::STDERR;
QString hibpDatabase = parser->value(Analyze::HIBPDatabaseOption); QList<QPair<const Entry*, int>> findings;
QFile hibpFile(hibpDatabase); QString error;
if (!hibpFile.open(QFile::ReadOnly)) {
err << QObject::tr("Failed to open HIBP file %1: %2").arg(hibpDatabase).arg(hibpFile.errorString()) << endl; auto hibpDatabase = parser->value(Analyze::HIBPDatabaseOption);
if (!QFile::exists(hibpDatabase) || hibpDatabase.isEmpty()) {
err << QObject::tr("Cannot find HIBP file: %1").arg(hibpDatabase);
return EXIT_FAILURE; return EXIT_FAILURE;
} }
out << QObject::tr("Evaluating database entries against HIBP file, this will take a while...") << endl; auto okon = parser->value(Analyze::OkonOption);
if (!okon.isEmpty()) {
out << QObject::tr("Evaluating database entries using okon...") << endl;
QList<QPair<const Entry*, int>> findings; if (!HibpOffline::okonReport(database, okon, hibpDatabase, findings, &error)) {
QString error; err << error << endl;
if (!HibpOffline::report(database, hibpFile, findings, &error)) { return EXIT_FAILURE;
err << error << endl; }
return EXIT_FAILURE; } else {
QFile hibpFile(hibpDatabase);
if (!hibpFile.open(QFile::ReadOnly)) {
err << QObject::tr("Failed to open HIBP file %1: %2").arg(hibpDatabase).arg(hibpFile.errorString()) << endl;
return EXIT_FAILURE;
}
out << QObject::tr("Evaluating database entries against HIBP file, this will take a while...") << endl;
if (!HibpOffline::report(database, hibpFile, findings, &error)) {
err << error << endl;
return EXIT_FAILURE;
}
} }
for (auto& finding : findings) { for (auto& finding : findings) {
@ -76,5 +98,9 @@ void Analyze::printHibpFinding(const Entry* entry, int count, QTextStream& out)
path.prepend("/").prepend(g->name()); path.prepend("/").prepend(g->name());
} }
out << QObject::tr("Password for '%1' has been leaked %2 time(s)!", "", count).arg(path).arg(count) << endl; if (count > 0) {
out << QObject::tr("Password for '%1' has been leaked %2 time(s)!", "", count).arg(path).arg(count) << endl;
} else {
out << QObject::tr("Password for '%1' has been leaked!", "", count).arg(path) << endl;
}
} }

View File

@ -27,6 +27,7 @@ public:
int executeWithDatabase(QSharedPointer<Database> db, QSharedPointer<QCommandLineParser> parser) override; int executeWithDatabase(QSharedPointer<Database> db, QSharedPointer<QCommandLineParser> parser) override;
static const QCommandLineOption HIBPDatabaseOption; static const QCommandLineOption HIBPDatabaseOption;
static const QCommandLineOption OkonOption;
private: private:
void printHibpFinding(const Entry* entry, int count, QTextStream& out); void printHibpFinding(const Entry* entry, int count, QTextStream& out);

View File

@ -19,6 +19,7 @@
#include <QCryptographicHash> #include <QCryptographicHash>
#include <QMultiHash> #include <QMultiHash>
#include <QProcess>
#include "core/Database.h" #include "core/Database.h"
#include "core/Group.h" #include "core/Group.h"
@ -106,4 +107,45 @@ namespace HibpOffline
} }
} }
} }
bool okonReport(QSharedPointer<Database> db,
const QString& okon,
const QString& okonDatabase,
QList<QPair<const Entry*, int>>& findings,
QString* error)
{
if (!okonDatabase.endsWith(".okon")) {
*error = QObject::tr("To use okon you must provide a post-processed file (e.g. file.okon)");
return false;
}
QProcess okonProcess;
for (const auto* entry : db->rootGroup()->entriesRecursive()) {
if (!entry->isRecycled()) {
const auto sha1 = QCryptographicHash::hash(entry->password().toUtf8(), QCryptographicHash::Sha1);
okonProcess.start(okon, {"--path", okonDatabase, "--hash", QString::fromLatin1(sha1.toHex())});
if (!okonProcess.waitForStarted()) {
*error = QObject::tr("Could not start okon process: %1").arg(okon);
return false;
}
if (!okonProcess.waitForFinished()) {
*error = QObject::tr("Error: okon process did not finish");
return false;
}
switch (okonProcess.exitCode()) {
case 1:
findings.append({entry, -1});
break;
case 2:
*error = QObject::tr("Failed to load okon processed database: %1").arg(okonDatabase);
return false;
}
}
}
return true;
}
} // namespace HibpOffline } // namespace HibpOffline

View File

@ -31,6 +31,12 @@ namespace HibpOffline
QIODevice& hibpInput, QIODevice& hibpInput,
QList<QPair<const Entry*, int>>& findings, QList<QPair<const Entry*, int>>& findings,
QString* error); QString* error);
}
bool okonReport(QSharedPointer<Database> db,
const QString& okon,
const QString& okonDatabase,
QList<QPair<const Entry*, int>>& findings,
QString* error);
} // namespace HibpOffline
#endif // KEEPASSXC_HIBPOFFLINE_H #endif // KEEPASSXC_HIBPOFFLINE_H