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.
*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_]::
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;
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
*-a*, *--attribute*::
Copies the specified attribute to the clipboard.

View File

@ -34,11 +34,17 @@ const QCommandLineOption Analyze::HIBPDatabaseOption = QCommandLineOption(
"https://haveibeenpwned.com/Passwords."),
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()
{
name = QString("analyze");
description = QObject::tr("Analyze passwords for weaknesses and problems.");
options.append(Analyze::HIBPDatabaseOption);
options.append(Analyze::OkonOption);
}
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& err = Utils::STDERR;
QString hibpDatabase = parser->value(Analyze::HIBPDatabaseOption);
QFile hibpFile(hibpDatabase);
if (!hibpFile.open(QFile::ReadOnly)) {
err << QObject::tr("Failed to open HIBP file %1: %2").arg(hibpDatabase).arg(hibpFile.errorString()) << endl;
QList<QPair<const Entry*, int>> findings;
QString error;
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;
}
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;
QString error;
if (!HibpOffline::report(database, hibpFile, findings, &error)) {
err << error << endl;
return EXIT_FAILURE;
if (!HibpOffline::okonReport(database, okon, hibpDatabase, findings, &error)) {
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) {
@ -76,5 +98,9 @@ void Analyze::printHibpFinding(const Entry* entry, int count, QTextStream& out)
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;
static const QCommandLineOption HIBPDatabaseOption;
static const QCommandLineOption OkonOption;
private:
void printHibpFinding(const Entry* entry, int count, QTextStream& out);

View File

@ -19,6 +19,7 @@
#include <QCryptographicHash>
#include <QMultiHash>
#include <QProcess>
#include "core/Database.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

View File

@ -31,6 +31,12 @@ namespace HibpOffline
QIODevice& hibpInput,
QList<QPair<const Entry*, int>>& findings,
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