mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2024-10-01 01:26:01 -04:00
CLI: Add Yubikey unlock support
This commit is contained in:
parent
77fcde875e
commit
964478e78f
@ -8,6 +8,7 @@
|
|||||||
- CLI: Add 'flatten' option to the 'ls' command [#3276](https://github.com/keepassxreboot/keepassxc/issues/3276)
|
- CLI: Add 'flatten' option to the 'ls' command [#3276](https://github.com/keepassxreboot/keepassxc/issues/3276)
|
||||||
- CLI: Add password generation options to `Add` and `Edit` commands [#3275](https://github.com/keepassxreboot/keepassxc/issues/3275)
|
- CLI: Add password generation options to `Add` and `Edit` commands [#3275](https://github.com/keepassxreboot/keepassxc/issues/3275)
|
||||||
- CLI: Add CSV export to the 'export' command [#3277]
|
- CLI: Add CSV export to the 'export' command [#3277]
|
||||||
|
- CLI: Add `-y --yubikey` option for YubiKey [#3416](https://github.com/keepassxreboot/keepassxc/issues/3416)
|
||||||
- Add 'Monospaced font' option to the Notes field [#3321](https://github.com/keepassxreboot/keepassxc/issues/3321)
|
- Add 'Monospaced font' option to the Notes field [#3321](https://github.com/keepassxreboot/keepassxc/issues/3321)
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
@ -160,6 +160,7 @@ set(keepassx_SOURCES
|
|||||||
keys/FileKey.cpp
|
keys/FileKey.cpp
|
||||||
keys/PasswordKey.cpp
|
keys/PasswordKey.cpp
|
||||||
keys/YkChallengeResponseKey.cpp
|
keys/YkChallengeResponseKey.cpp
|
||||||
|
keys/YkChallengeResponseKeyCLI.cpp
|
||||||
streams/HashedBlockStream.cpp
|
streams/HashedBlockStream.cpp
|
||||||
streams/HmacBlockStream.cpp
|
streams/HmacBlockStream.cpp
|
||||||
streams/LayeredStream.cpp
|
streams/LayeredStream.cpp
|
||||||
|
@ -52,6 +52,12 @@ const QCommandLineOption Command::KeyFileOption = QCommandLineOption(QStringList
|
|||||||
const QCommandLineOption Command::NoPasswordOption =
|
const QCommandLineOption Command::NoPasswordOption =
|
||||||
QCommandLineOption(QStringList() << "no-password", QObject::tr("Deactivate password key for the database."));
|
QCommandLineOption(QStringList() << "no-password", QObject::tr("Deactivate password key for the database."));
|
||||||
|
|
||||||
|
const QCommandLineOption Command::YubiKeyOption =
|
||||||
|
QCommandLineOption(QStringList() << "y"
|
||||||
|
<< "yubikey",
|
||||||
|
QObject::tr("Yubikey slot used to encrypt the database."),
|
||||||
|
QObject::tr("slot"));
|
||||||
|
|
||||||
QMap<QString, Command*> commands;
|
QMap<QString, Command*> commands;
|
||||||
|
|
||||||
Command::Command()
|
Command::Command()
|
||||||
|
@ -56,6 +56,7 @@ public:
|
|||||||
static const QCommandLineOption QuietOption;
|
static const QCommandLineOption QuietOption;
|
||||||
static const QCommandLineOption KeyFileOption;
|
static const QCommandLineOption KeyFileOption;
|
||||||
static const QCommandLineOption NoPasswordOption;
|
static const QCommandLineOption NoPasswordOption;
|
||||||
|
static const QCommandLineOption YubiKeyOption;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // KEEPASSXC_COMMAND_H
|
#endif // KEEPASSXC_COMMAND_H
|
||||||
|
@ -24,6 +24,9 @@ DatabaseCommand::DatabaseCommand()
|
|||||||
positionalArguments.append({QString("database"), QObject::tr("Path of the database."), QString("")});
|
positionalArguments.append({QString("database"), QObject::tr("Path of the database."), QString("")});
|
||||||
options.append(Command::KeyFileOption);
|
options.append(Command::KeyFileOption);
|
||||||
options.append(Command::NoPasswordOption);
|
options.append(Command::NoPasswordOption);
|
||||||
|
#ifdef WITH_XC_YUBIKEY
|
||||||
|
options.append(Command::YubiKeyOption);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
int DatabaseCommand::execute(const QStringList& arguments)
|
int DatabaseCommand::execute(const QStringList& arguments)
|
||||||
@ -37,6 +40,7 @@ int DatabaseCommand::execute(const QStringList& arguments)
|
|||||||
auto db = Utils::unlockDatabase(args.at(0),
|
auto db = Utils::unlockDatabase(args.at(0),
|
||||||
!parser->isSet(Command::NoPasswordOption),
|
!parser->isSet(Command::NoPasswordOption),
|
||||||
parser->value(Command::KeyFileOption),
|
parser->value(Command::KeyFileOption),
|
||||||
|
parser->value(Command::YubiKeyOption),
|
||||||
parser->isSet(Command::QuietOption) ? Utils::DEVNULL : Utils::STDOUT,
|
parser->isSet(Command::QuietOption) ? Utils::DEVNULL : Utils::STDOUT,
|
||||||
Utils::STDERR);
|
Utils::STDERR);
|
||||||
if (!db) {
|
if (!db) {
|
||||||
|
@ -43,6 +43,9 @@ const QCommandLineOption Merge::DryRunOption =
|
|||||||
QCommandLineOption(QStringList() << "dry-run",
|
QCommandLineOption(QStringList() << "dry-run",
|
||||||
QObject::tr("Only print the changes detected by the merge operation."));
|
QObject::tr("Only print the changes detected by the merge operation."));
|
||||||
|
|
||||||
|
const QCommandLineOption Merge::YubiKeyFromOption(QStringList() << "yubikey-from",
|
||||||
|
QObject::tr("Yubikey slot for the second database."),
|
||||||
|
QObject::tr("slot"));
|
||||||
Merge::Merge()
|
Merge::Merge()
|
||||||
{
|
{
|
||||||
name = QString("merge");
|
name = QString("merge");
|
||||||
@ -51,6 +54,9 @@ Merge::Merge()
|
|||||||
options.append(Merge::KeyFileFromOption);
|
options.append(Merge::KeyFileFromOption);
|
||||||
options.append(Merge::NoPasswordFromOption);
|
options.append(Merge::NoPasswordFromOption);
|
||||||
options.append(Merge::DryRunOption);
|
options.append(Merge::DryRunOption);
|
||||||
|
#ifdef WITH_XC_YUBIKEY
|
||||||
|
options.append(Merge::YubiKeyFromOption);
|
||||||
|
#endif
|
||||||
positionalArguments.append({QString("database2"), QObject::tr("Path of the database to merge from."), QString("")});
|
positionalArguments.append({QString("database2"), QObject::tr("Path of the database to merge from."), QString("")});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,6 +76,7 @@ int Merge::executeWithDatabase(QSharedPointer<Database> database, QSharedPointer
|
|||||||
db2 = Utils::unlockDatabase(fromDatabasePath,
|
db2 = Utils::unlockDatabase(fromDatabasePath,
|
||||||
!parser->isSet(Merge::NoPasswordFromOption),
|
!parser->isSet(Merge::NoPasswordFromOption),
|
||||||
parser->value(Merge::KeyFileFromOption),
|
parser->value(Merge::KeyFileFromOption),
|
||||||
|
parser->value(Merge::YubiKeyFromOption),
|
||||||
parser->isSet(Command::QuietOption) ? Utils::DEVNULL : Utils::STDOUT,
|
parser->isSet(Command::QuietOption) ? Utils::DEVNULL : Utils::STDOUT,
|
||||||
Utils::STDERR);
|
Utils::STDERR);
|
||||||
if (!db2) {
|
if (!db2) {
|
||||||
|
@ -30,6 +30,7 @@ public:
|
|||||||
static const QCommandLineOption SameCredentialsOption;
|
static const QCommandLineOption SameCredentialsOption;
|
||||||
static const QCommandLineOption KeyFileFromOption;
|
static const QCommandLineOption KeyFileFromOption;
|
||||||
static const QCommandLineOption NoPasswordFromOption;
|
static const QCommandLineOption NoPasswordFromOption;
|
||||||
|
static const QCommandLineOption YubiKeyFromOption;
|
||||||
static const QCommandLineOption DryRunOption;
|
static const QCommandLineOption DryRunOption;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -102,6 +102,7 @@ namespace Utils
|
|||||||
QSharedPointer<Database> unlockDatabase(const QString& databaseFilename,
|
QSharedPointer<Database> unlockDatabase(const QString& databaseFilename,
|
||||||
const bool isPasswordProtected,
|
const bool isPasswordProtected,
|
||||||
const QString& keyFilename,
|
const QString& keyFilename,
|
||||||
|
const QString& yubiKeySlot,
|
||||||
FILE* outputDescriptor,
|
FILE* outputDescriptor,
|
||||||
FILE* errorDescriptor)
|
FILE* errorDescriptor)
|
||||||
{
|
{
|
||||||
@ -153,6 +154,31 @@ namespace Utils
|
|||||||
compositeKey->addKey(fileKey);
|
compositeKey->addKey(fileKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef WITH_XC_YUBIKEY
|
||||||
|
if (!yubiKeySlot.isEmpty()) {
|
||||||
|
bool ok = false;
|
||||||
|
int slot = yubiKeySlot.toInt(&ok, 10);
|
||||||
|
if (!ok || (slot != 1 && slot != 2)) {
|
||||||
|
err << QObject::tr("Invalid YubiKey slot %1").arg(yubiKeySlot) << endl;
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
QString errorMessage;
|
||||||
|
bool blocking = YubiKey::instance()->checkSlotIsBlocking(slot, errorMessage);
|
||||||
|
if (!errorMessage.isEmpty()) {
|
||||||
|
err << errorMessage << endl;
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
auto key = QSharedPointer<YkChallengeResponseKeyCLI>(new YkChallengeResponseKeyCLI(
|
||||||
|
slot,
|
||||||
|
blocking,
|
||||||
|
QObject::tr("Please touch the button on your YubiKey to unlock %1").arg(databaseFilename),
|
||||||
|
outputDescriptor));
|
||||||
|
compositeKey->addChallengeResponseKey(key);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
auto db = QSharedPointer<Database>::create();
|
auto db = QSharedPointer<Database>::create();
|
||||||
QString error;
|
QString error;
|
||||||
if (db->open(databaseFilename, compositeKey, &error, false)) {
|
if (db->open(databaseFilename, compositeKey, &error, false)) {
|
||||||
|
@ -25,6 +25,12 @@
|
|||||||
#include "keys/PasswordKey.h"
|
#include "keys/PasswordKey.h"
|
||||||
#include <QtCore/qglobal.h>
|
#include <QtCore/qglobal.h>
|
||||||
|
|
||||||
|
#ifdef WITH_XC_YUBIKEY
|
||||||
|
#include "keys/YkChallengeResponseKey.h"
|
||||||
|
#include "keys/YkChallengeResponseKeyCLI.h"
|
||||||
|
#include "keys/drivers/YubiKey.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace Utils
|
namespace Utils
|
||||||
{
|
{
|
||||||
extern FILE* STDOUT;
|
extern FILE* STDOUT;
|
||||||
@ -38,6 +44,7 @@ namespace Utils
|
|||||||
QSharedPointer<Database> unlockDatabase(const QString& databaseFilename,
|
QSharedPointer<Database> unlockDatabase(const QString& databaseFilename,
|
||||||
const bool isPasswordProtected = true,
|
const bool isPasswordProtected = true,
|
||||||
const QString& keyFilename = {},
|
const QString& keyFilename = {},
|
||||||
|
const QString& yubiKeySlot = {},
|
||||||
FILE* outputDescriptor = STDOUT,
|
FILE* outputDescriptor = STDOUT,
|
||||||
FILE* errorDescriptor = STDERR);
|
FILE* errorDescriptor = STDERR);
|
||||||
|
|
||||||
|
@ -70,6 +70,9 @@ Specifies a path to a key file for unlocking the database. In a merge operation
|
|||||||
.IP "--no-password"
|
.IP "--no-password"
|
||||||
Deactivate password key for the database.
|
Deactivate password key for the database.
|
||||||
|
|
||||||
|
.IP "-y, --yubikey <slot>"
|
||||||
|
Specifies a yubikey slot for unlocking the database. In a merge operation this option is used to specify the yubikey slot for the first database.
|
||||||
|
|
||||||
.IP "-q, --quiet <path>"
|
.IP "-q, --quiet <path>"
|
||||||
Silence password prompt and other secondary outputs.
|
Silence password prompt and other secondary outputs.
|
||||||
|
|
||||||
@ -91,6 +94,9 @@ Path of the key file for the second database.
|
|||||||
.IP "--no-password-from"
|
.IP "--no-password-from"
|
||||||
Deactivate password key for the database to merge from.
|
Deactivate password key for the database to merge from.
|
||||||
|
|
||||||
|
.IP "--yubikey-from <slot>"
|
||||||
|
Yubikey slot for the second database.
|
||||||
|
|
||||||
.IP "-s, --same-credentials"
|
.IP "-s, --same-credentials"
|
||||||
Use the same credentials for unlocking both database.
|
Use the same credentials for unlocking both database.
|
||||||
|
|
||||||
|
@ -66,9 +66,9 @@ QByteArray YkChallengeResponseKey::rawKey() const
|
|||||||
/**
|
/**
|
||||||
* Assumes yubikey()->init() was called
|
* Assumes yubikey()->init() was called
|
||||||
*/
|
*/
|
||||||
bool YkChallengeResponseKey::challenge(const QByteArray& challenge)
|
bool YkChallengeResponseKey::challenge(const QByteArray& c)
|
||||||
{
|
{
|
||||||
return this->challenge(challenge, 2);
|
return challenge(c, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool YkChallengeResponseKey::challenge(const QByteArray& challenge, unsigned int retries)
|
bool YkChallengeResponseKey::challenge(const QByteArray& challenge, unsigned int retries)
|
||||||
|
71
src/keys/YkChallengeResponseKeyCLI.cpp
Normal file
71
src/keys/YkChallengeResponseKeyCLI.cpp
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2019 KeePassXC Team <team@keepassxc.org>
|
||||||
|
*
|
||||||
|
* 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 or (at your option)
|
||||||
|
* version 3 of the License.
|
||||||
|
*
|
||||||
|
* This program 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, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "keys/YkChallengeResponseKeyCLI.h"
|
||||||
|
#include "keys/drivers/YubiKey.h"
|
||||||
|
|
||||||
|
#include "core/Tools.h"
|
||||||
|
#include "crypto/CryptoHash.h"
|
||||||
|
#include "crypto/Random.h"
|
||||||
|
|
||||||
|
#include <QFile>
|
||||||
|
#include <QtCore/qglobal.h>
|
||||||
|
|
||||||
|
QUuid YkChallengeResponseKeyCLI::UUID("e2be77c0-c810-417a-8437-32f41d00bd1d");
|
||||||
|
|
||||||
|
YkChallengeResponseKeyCLI::YkChallengeResponseKeyCLI(int slot,
|
||||||
|
bool blocking,
|
||||||
|
QString messageInteraction,
|
||||||
|
FILE* outputDescriptor)
|
||||||
|
: ChallengeResponseKey(UUID)
|
||||||
|
, m_slot(slot)
|
||||||
|
, m_blocking(blocking)
|
||||||
|
, m_messageInteraction(messageInteraction)
|
||||||
|
, m_out(outputDescriptor)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray YkChallengeResponseKeyCLI::rawKey() const
|
||||||
|
{
|
||||||
|
return m_key;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assumes yubikey()->init() was called
|
||||||
|
*/
|
||||||
|
bool YkChallengeResponseKeyCLI::challenge(const QByteArray& c)
|
||||||
|
{
|
||||||
|
return challenge(c, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool YkChallengeResponseKeyCLI::challenge(const QByteArray& challenge, unsigned int retries)
|
||||||
|
{
|
||||||
|
QTextStream out(m_out, QIODevice::WriteOnly);
|
||||||
|
do {
|
||||||
|
--retries;
|
||||||
|
|
||||||
|
if (m_blocking) {
|
||||||
|
out << m_messageInteraction << endl;
|
||||||
|
}
|
||||||
|
YubiKey::ChallengeResult result = YubiKey::instance()->challenge(m_slot, m_blocking, challenge, m_key);
|
||||||
|
if (result == YubiKey::SUCCESS) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} while (retries > 0);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
52
src/keys/YkChallengeResponseKeyCLI.h
Normal file
52
src/keys/YkChallengeResponseKeyCLI.h
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2019 KeePassXC Team <team@keepassxc.org>
|
||||||
|
*
|
||||||
|
* 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 or (at your option)
|
||||||
|
* version 3 of the License.
|
||||||
|
*
|
||||||
|
* This program 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, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef KEEPASSX_YK_CHALLENGERESPONSEKEYCLI_H
|
||||||
|
#define KEEPASSX_YK_CHALLENGERESPONSEKEYCLI_H
|
||||||
|
|
||||||
|
#include "core/Global.h"
|
||||||
|
#include "keys/ChallengeResponseKey.h"
|
||||||
|
#include "keys/drivers/YubiKey.h"
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QTextStream>
|
||||||
|
|
||||||
|
class YkChallengeResponseKeyCLI : public QObject, public ChallengeResponseKey
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
static QUuid UUID;
|
||||||
|
|
||||||
|
explicit YkChallengeResponseKeyCLI(int slot,
|
||||||
|
bool blocking,
|
||||||
|
QString messageInteraction,
|
||||||
|
FILE* outputDescriptor);
|
||||||
|
|
||||||
|
QByteArray rawKey() const override;
|
||||||
|
bool challenge(const QByteArray& challenge) override;
|
||||||
|
bool challenge(const QByteArray& challenge, unsigned int retries);
|
||||||
|
|
||||||
|
private:
|
||||||
|
QByteArray m_key;
|
||||||
|
int m_slot;
|
||||||
|
bool m_blocking;
|
||||||
|
QString m_messageInteraction;
|
||||||
|
FILE* m_out;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // KEEPASSX_YK_CHALLENGERESPONSEKEYCLI_H
|
@ -132,28 +132,18 @@ void YubiKey::detect()
|
|||||||
{
|
{
|
||||||
bool found = false;
|
bool found = false;
|
||||||
|
|
||||||
if (init()) {
|
|
||||||
YubiKey::ChallengeResult result;
|
|
||||||
QByteArray rand = randomGen()->randomArray(1);
|
|
||||||
QByteArray resp;
|
|
||||||
|
|
||||||
// Check slot 1 and 2 for Challenge-Response HMAC capability
|
// Check slot 1 and 2 for Challenge-Response HMAC capability
|
||||||
for (int i = 1; i <= 2; ++i) {
|
for (int i = 1; i <= 2; ++i) {
|
||||||
result = challenge(i, false, rand, resp);
|
QString errorMsg;
|
||||||
if (result == ALREADY_RUNNING) {
|
bool isBlocking = checkSlotIsBlocking(i, errorMsg);
|
||||||
// Try this slot again after waiting
|
if (errorMsg.isEmpty()) {
|
||||||
Tools::sleep(300);
|
found = true;
|
||||||
result = challenge(i, false, rand, resp);
|
emit detected(i, isBlocking);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result != ALREADY_RUNNING && result != ERROR) {
|
// Wait between slots to let the yubikey settle.
|
||||||
emit detected(i, result == WOULDBLOCK);
|
|
||||||
found = true;
|
|
||||||
}
|
|
||||||
// Wait between slots to let the yubikey settle
|
|
||||||
Tools::sleep(150);
|
Tools::sleep(150);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (!found) {
|
if (!found) {
|
||||||
emit notFound();
|
emit notFound();
|
||||||
@ -162,6 +152,38 @@ void YubiKey::detect()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool YubiKey::checkSlotIsBlocking(int slot, QString& errorMessage)
|
||||||
|
{
|
||||||
|
if (!init()) {
|
||||||
|
errorMessage = QString("Could not initialize YubiKey.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
YubiKey::ChallengeResult result;
|
||||||
|
QByteArray rand = randomGen()->randomArray(1);
|
||||||
|
QByteArray resp;
|
||||||
|
|
||||||
|
result = challenge(slot, false, rand, resp);
|
||||||
|
if (result == ALREADY_RUNNING) {
|
||||||
|
// Try this slot again after waiting
|
||||||
|
Tools::sleep(300);
|
||||||
|
result = challenge(slot, false, rand, resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result == SUCCESS || result == WOULDBLOCK) {
|
||||||
|
return result == WOULDBLOCK;
|
||||||
|
} else if (result == ALREADY_RUNNING) {
|
||||||
|
errorMessage = QString("YubiKey busy");
|
||||||
|
return false;
|
||||||
|
} else if (result == ERROR) {
|
||||||
|
errorMessage = QString("YubiKey error");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
errorMessage = QString("Error while polling YubiKey");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
bool YubiKey::getSerial(unsigned int& serial)
|
bool YubiKey::getSerial(unsigned int& serial)
|
||||||
{
|
{
|
||||||
m_mutex.lock();
|
m_mutex.lock();
|
||||||
@ -190,14 +212,14 @@ YubiKey::ChallengeResult YubiKey::challenge(int slot, bool mayBlock, const QByte
|
|||||||
int yk_cmd = (slot == 1) ? SLOT_CHAL_HMAC1 : SLOT_CHAL_HMAC2;
|
int yk_cmd = (slot == 1) ? SLOT_CHAL_HMAC1 : SLOT_CHAL_HMAC2;
|
||||||
QByteArray paddedChallenge = challenge;
|
QByteArray paddedChallenge = challenge;
|
||||||
|
|
||||||
// yk_challenge_response() insists on 64 byte response buffer */
|
// yk_challenge_response() insists on 64 bytes response buffer */
|
||||||
response.clear();
|
response.clear();
|
||||||
response.resize(64);
|
response.resize(64);
|
||||||
|
|
||||||
/* The challenge sent to the yubikey should always be 64 bytes for
|
/* The challenge sent to the yubikey should always be 64 bytes for
|
||||||
* compatibility with all configurations. Follow PKCS7 padding.
|
* compatibility with all configurations. Follow PKCS7 padding.
|
||||||
*
|
*
|
||||||
* There is some question whether or not 64 byte fixed length
|
* There is some question whether or not 64 bytes fixed length
|
||||||
* configurations even work, some docs say avoid it.
|
* configurations even work, some docs say avoid it.
|
||||||
*/
|
*/
|
||||||
const int padLen = 64 - paddedChallenge.size();
|
const int padLen = 64 - paddedChallenge.size();
|
||||||
|
@ -90,6 +90,13 @@ public:
|
|||||||
*/
|
*/
|
||||||
void detect();
|
void detect();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param slot the yubikey slot.
|
||||||
|
* @param errorMessage populated if an error occured.
|
||||||
|
*
|
||||||
|
* @return whether the key is blocking or not.
|
||||||
|
*/
|
||||||
|
bool checkSlotIsBlocking(int slot, QString& errorMessage);
|
||||||
signals:
|
signals:
|
||||||
/** Emitted in response to detect() when a device is found
|
/** Emitted in response to detect() when a device is found
|
||||||
*
|
*
|
||||||
|
@ -89,6 +89,11 @@ void TestCli::initTestCase()
|
|||||||
QVERIFY(sourceDbFile4.open(QIODevice::ReadOnly));
|
QVERIFY(sourceDbFile4.open(QIODevice::ReadOnly));
|
||||||
QVERIFY(Tools::readAllFromDevice(&sourceDbFile4, m_keyFileProtectedNoPasswordDbData));
|
QVERIFY(Tools::readAllFromDevice(&sourceDbFile4, m_keyFileProtectedNoPasswordDbData));
|
||||||
sourceDbFile4.close();
|
sourceDbFile4.close();
|
||||||
|
|
||||||
|
QFile sourceDbFileYubiKeyProtected(QString(KEEPASSX_TEST_DATA_DIR).append("/YubiKeyProtectedPasswords.kdbx"));
|
||||||
|
QVERIFY(sourceDbFileYubiKeyProtected.open(QIODevice::ReadOnly));
|
||||||
|
QVERIFY(Tools::readAllFromDevice(&sourceDbFileYubiKeyProtected, m_yubiKeyProtectedDbData));
|
||||||
|
sourceDbFileYubiKeyProtected.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
void TestCli::init()
|
void TestCli::init()
|
||||||
@ -113,6 +118,11 @@ void TestCli::init()
|
|||||||
m_keyFileProtectedNoPasswordDbFile->write(m_keyFileProtectedNoPasswordDbData);
|
m_keyFileProtectedNoPasswordDbFile->write(m_keyFileProtectedNoPasswordDbData);
|
||||||
m_keyFileProtectedNoPasswordDbFile->close();
|
m_keyFileProtectedNoPasswordDbFile->close();
|
||||||
|
|
||||||
|
m_yubiKeyProtectedDbFile.reset(new TemporaryFile());
|
||||||
|
m_yubiKeyProtectedDbFile->open();
|
||||||
|
m_yubiKeyProtectedDbFile->write(m_yubiKeyProtectedDbData);
|
||||||
|
m_yubiKeyProtectedDbFile->close();
|
||||||
|
|
||||||
m_stdinFile.reset(new TemporaryFile());
|
m_stdinFile.reset(new TemporaryFile());
|
||||||
m_stdinFile->open();
|
m_stdinFile->open();
|
||||||
m_stdinHandle = fdopen(m_stdinFile->handle(), "r+");
|
m_stdinHandle = fdopen(m_stdinFile->handle(), "r+");
|
||||||
@ -155,7 +165,7 @@ void TestCli::cleanupTestCase()
|
|||||||
QSharedPointer<Database> TestCli::readTestDatabase() const
|
QSharedPointer<Database> TestCli::readTestDatabase() const
|
||||||
{
|
{
|
||||||
Utils::Test::setNextPassword("a");
|
Utils::Test::setNextPassword("a");
|
||||||
auto db = QSharedPointer<Database>(Utils::unlockDatabase(m_dbFile->fileName(), true, "", m_stdoutHandle));
|
auto db = QSharedPointer<Database>(Utils::unlockDatabase(m_dbFile->fileName(), true, "", "", m_stdoutHandle));
|
||||||
m_stdoutFile->seek(ftell(m_stdoutHandle)); // re-synchronize handles
|
m_stdoutFile->seek(ftell(m_stdoutHandle)); // re-synchronize handles
|
||||||
return db;
|
return db;
|
||||||
}
|
}
|
||||||
@ -226,14 +236,8 @@ void TestCli::testAdd()
|
|||||||
|
|
||||||
Utils::Test::setNextPassword("a");
|
Utils::Test::setNextPassword("a");
|
||||||
Utils::Test::setNextPassword("newpassword");
|
Utils::Test::setNextPassword("newpassword");
|
||||||
addCmd.execute({"add",
|
addCmd.execute(
|
||||||
"-u",
|
{"add", "-u", "newuser2", "--url", "https://example.net/", "-p", m_dbFile->fileName(), "/newuser-entry2"});
|
||||||
"newuser2",
|
|
||||||
"--url",
|
|
||||||
"https://example.net/",
|
|
||||||
"-p",
|
|
||||||
m_dbFile->fileName(),
|
|
||||||
"/newuser-entry2"});
|
|
||||||
|
|
||||||
db = readTestDatabase();
|
db = readTestDatabase();
|
||||||
entry = db->rootGroup()->findEntryByPath("/newuser-entry2");
|
entry = db->rootGroup()->findEntryByPath("/newuser-entry2");
|
||||||
@ -246,14 +250,7 @@ void TestCli::testAdd()
|
|||||||
pos = m_stdoutFile->pos();
|
pos = m_stdoutFile->pos();
|
||||||
posErr = m_stderrFile->pos();
|
posErr = m_stderrFile->pos();
|
||||||
Utils::Test::setNextPassword("a");
|
Utils::Test::setNextPassword("a");
|
||||||
addCmd.execute({"add",
|
addCmd.execute({"add", "-u", "newuser3", "-g", "-L", "34", m_dbFile->fileName(), "/newuser-entry3"});
|
||||||
"-u",
|
|
||||||
"newuser3",
|
|
||||||
"-g",
|
|
||||||
"-L",
|
|
||||||
"34",
|
|
||||||
m_dbFile->fileName(),
|
|
||||||
"/newuser-entry3"});
|
|
||||||
m_stdoutFile->seek(pos);
|
m_stdoutFile->seek(pos);
|
||||||
m_stderrFile->seek(posErr);
|
m_stderrFile->seek(posErr);
|
||||||
m_stdoutFile->readLine(); // skip password prompt
|
m_stdoutFile->readLine(); // skip password prompt
|
||||||
@ -433,7 +430,7 @@ void TestCli::testCreate()
|
|||||||
QCOMPARE(m_stdoutFile->readLine(), QByteArray("Successfully created new database.\n"));
|
QCOMPARE(m_stdoutFile->readLine(), QByteArray("Successfully created new database.\n"));
|
||||||
|
|
||||||
Utils::Test::setNextPassword("a");
|
Utils::Test::setNextPassword("a");
|
||||||
auto db = QSharedPointer<Database>(Utils::unlockDatabase(databaseFilename, true, "", Utils::DEVNULL));
|
auto db = QSharedPointer<Database>(Utils::unlockDatabase(databaseFilename, true, "", "", Utils::DEVNULL));
|
||||||
QVERIFY(db);
|
QVERIFY(db);
|
||||||
|
|
||||||
// Should refuse to create the database if it already exists.
|
// Should refuse to create the database if it already exists.
|
||||||
@ -462,7 +459,8 @@ void TestCli::testCreate()
|
|||||||
QCOMPARE(m_stdoutFile->readLine(), QByteArray("Successfully created new database.\n"));
|
QCOMPARE(m_stdoutFile->readLine(), QByteArray("Successfully created new database.\n"));
|
||||||
|
|
||||||
Utils::Test::setNextPassword("a");
|
Utils::Test::setNextPassword("a");
|
||||||
auto db2 = QSharedPointer<Database>(Utils::unlockDatabase(databaseFilename2, true, keyfilePath, Utils::DEVNULL));
|
auto db2 =
|
||||||
|
QSharedPointer<Database>(Utils::unlockDatabase(databaseFilename2, true, keyfilePath, "", Utils::DEVNULL));
|
||||||
QVERIFY(db2);
|
QVERIFY(db2);
|
||||||
|
|
||||||
// Testing with existing keyfile
|
// Testing with existing keyfile
|
||||||
@ -479,7 +477,8 @@ void TestCli::testCreate()
|
|||||||
QCOMPARE(m_stdoutFile->readLine(), QByteArray("Successfully created new database.\n"));
|
QCOMPARE(m_stdoutFile->readLine(), QByteArray("Successfully created new database.\n"));
|
||||||
|
|
||||||
Utils::Test::setNextPassword("a");
|
Utils::Test::setNextPassword("a");
|
||||||
auto db3 = QSharedPointer<Database>(Utils::unlockDatabase(databaseFilename3, true, keyfilePath, Utils::DEVNULL));
|
auto db3 =
|
||||||
|
QSharedPointer<Database>(Utils::unlockDatabase(databaseFilename3, true, keyfilePath, "", Utils::DEVNULL));
|
||||||
QVERIFY(db3);
|
QVERIFY(db3);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1473,3 +1472,76 @@ void TestCli::testInvalidDbFiles()
|
|||||||
QObject::tr("Failed to open database file %1: not readable").arg(path) + "\n");
|
QObject::tr("Failed to open database file %1: not readable").arg(path) + "\n");
|
||||||
#endif // Q_OS_WIN
|
#endif // Q_OS_WIN
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Secret key for the YubiKey slot used by the unit test is
|
||||||
|
* 1c e3 0f d7 8d 20 dc fa 40 b5 0c 18 77 9a fb 0f 02 28 8d b7
|
||||||
|
* This secret should be configured at slot 2, and the slot
|
||||||
|
* should be configured as passive.
|
||||||
|
*/
|
||||||
|
void TestCli::testYubiKeyOption()
|
||||||
|
{
|
||||||
|
if (!YubiKey::instance()->init()) {
|
||||||
|
QSKIP("Unable to connect to YubiKey");
|
||||||
|
}
|
||||||
|
|
||||||
|
QString errorMessage;
|
||||||
|
bool isBlocking = YubiKey::instance()->checkSlotIsBlocking(2, errorMessage);
|
||||||
|
if (isBlocking && errorMessage.isEmpty()) {
|
||||||
|
QSKIP("Skipping YubiKey in press mode.");
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray challenge("CLITest");
|
||||||
|
QByteArray response;
|
||||||
|
YubiKey::instance()->challenge(2, false, challenge, response);
|
||||||
|
QByteArray expected("\xA2\x3B\x94\x00\xBE\x47\x9A\x30\xA9\xEB\x50\x9B\x85\x56\x5B\x6B\x30\x25\xB4\x8E", 20);
|
||||||
|
QVERIFY2(response == expected, "YubiKey Slot 2 is not configured with correct secret key.");
|
||||||
|
|
||||||
|
List listCmd;
|
||||||
|
Add addCmd;
|
||||||
|
|
||||||
|
Utils::Test::setNextPassword("a");
|
||||||
|
listCmd.execute({"ls", "-y", "2", m_yubiKeyProtectedDbFile->fileName()});
|
||||||
|
m_stdoutFile->reset();
|
||||||
|
m_stderrFile->reset();
|
||||||
|
m_stdoutFile->readLine(); // skip password prompt
|
||||||
|
QCOMPARE(m_stdoutFile->readAll(),
|
||||||
|
QByteArray("entry1\n"
|
||||||
|
"entry2\n"));
|
||||||
|
|
||||||
|
// Should raise an error with no yubikey slot.
|
||||||
|
qint64 pos = m_stdoutFile->pos();
|
||||||
|
qint64 posErr = m_stderrFile->pos();
|
||||||
|
Utils::Test::setNextPassword("a");
|
||||||
|
listCmd.execute({"ls", m_yubiKeyProtectedDbFile->fileName()});
|
||||||
|
m_stdoutFile->seek(pos);
|
||||||
|
m_stdoutFile->readLine(); // skip password prompt
|
||||||
|
m_stderrFile->seek(posErr);
|
||||||
|
QCOMPARE(m_stdoutFile->readAll(), QByteArray(""));
|
||||||
|
QCOMPARE(m_stderrFile->readLine(),
|
||||||
|
QByteArray("Error while reading the database: Invalid credentials were provided, please try again.\n"));
|
||||||
|
QCOMPARE(m_stderrFile->readLine(),
|
||||||
|
QByteArray("If this reoccurs, then your database file may be corrupt. (HMAC mismatch)\n"));
|
||||||
|
|
||||||
|
// Should raise an error if yubikey slot is not a string
|
||||||
|
pos = m_stdoutFile->pos();
|
||||||
|
posErr = m_stderrFile->pos();
|
||||||
|
Utils::Test::setNextPassword("a");
|
||||||
|
listCmd.execute({"ls", "-y", "invalidslot", m_yubiKeyProtectedDbFile->fileName()});
|
||||||
|
m_stdoutFile->seek(pos);
|
||||||
|
m_stdoutFile->readLine(); // skip password prompt
|
||||||
|
m_stderrFile->seek(posErr);
|
||||||
|
QCOMPARE(m_stdoutFile->readAll(), QByteArray(""));
|
||||||
|
QCOMPARE(m_stderrFile->readAll().split(':').at(0), QByteArray("Invalid YubiKey slot invalidslot\n"));
|
||||||
|
|
||||||
|
// Should raise an error if yubikey slot is invalid.
|
||||||
|
pos = m_stdoutFile->pos();
|
||||||
|
posErr = m_stderrFile->pos();
|
||||||
|
Utils::Test::setNextPassword("a");
|
||||||
|
listCmd.execute({"ls", "-y", "3", m_yubiKeyProtectedDbFile->fileName()});
|
||||||
|
m_stdoutFile->seek(pos);
|
||||||
|
m_stdoutFile->readLine(); // skip password prompt
|
||||||
|
m_stderrFile->seek(posErr);
|
||||||
|
QCOMPARE(m_stdoutFile->readAll(), QByteArray(""));
|
||||||
|
QCOMPARE(m_stderrFile->readAll().split(':').at(0), QByteArray("Invalid YubiKey slot 3\n"));
|
||||||
|
}
|
||||||
|
@ -64,16 +64,19 @@ private slots:
|
|||||||
void testRemoveQuiet();
|
void testRemoveQuiet();
|
||||||
void testShow();
|
void testShow();
|
||||||
void testInvalidDbFiles();
|
void testInvalidDbFiles();
|
||||||
|
void testYubiKeyOption();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QByteArray m_dbData;
|
QByteArray m_dbData;
|
||||||
QByteArray m_dbData2;
|
QByteArray m_dbData2;
|
||||||
|
QByteArray m_yubiKeyProtectedDbData;
|
||||||
QByteArray m_keyFileProtectedDbData;
|
QByteArray m_keyFileProtectedDbData;
|
||||||
QByteArray m_keyFileProtectedNoPasswordDbData;
|
QByteArray m_keyFileProtectedNoPasswordDbData;
|
||||||
QScopedPointer<TemporaryFile> m_dbFile;
|
QScopedPointer<TemporaryFile> m_dbFile;
|
||||||
QScopedPointer<TemporaryFile> m_dbFile2;
|
QScopedPointer<TemporaryFile> m_dbFile2;
|
||||||
QScopedPointer<TemporaryFile> m_keyFileProtectedDbFile;
|
QScopedPointer<TemporaryFile> m_keyFileProtectedDbFile;
|
||||||
QScopedPointer<TemporaryFile> m_keyFileProtectedNoPasswordDbFile;
|
QScopedPointer<TemporaryFile> m_keyFileProtectedNoPasswordDbFile;
|
||||||
|
QScopedPointer<TemporaryFile> m_yubiKeyProtectedDbFile;
|
||||||
QScopedPointer<TemporaryFile> m_stdoutFile;
|
QScopedPointer<TemporaryFile> m_stdoutFile;
|
||||||
QScopedPointer<TemporaryFile> m_stderrFile;
|
QScopedPointer<TemporaryFile> m_stderrFile;
|
||||||
QScopedPointer<TemporaryFile> m_stdinFile;
|
QScopedPointer<TemporaryFile> m_stdinFile;
|
||||||
|
@ -74,7 +74,7 @@ void TestYubiKeyChalResp::keyIssueChallenge()
|
|||||||
/* TODO Determine if it's reasonable to provide a fixed secret key for
|
/* TODO Determine if it's reasonable to provide a fixed secret key for
|
||||||
* verification testing. Obviously simple technically, but annoying
|
* verification testing. Obviously simple technically, but annoying
|
||||||
* if devs need to re-program their yubikeys or have a spare test key
|
* if devs need to re-program their yubikeys or have a spare test key
|
||||||
* for unit tests to past.
|
* for unit tests to pass.
|
||||||
*
|
*
|
||||||
* Might be worth it for integrity verification though.
|
* Might be worth it for integrity verification though.
|
||||||
*/
|
*/
|
||||||
|
BIN
tests/data/YubiKeyProtectedPasswords.kdbx
Normal file
BIN
tests/data/YubiKeyProtectedPasswords.kdbx
Normal file
Binary file not shown.
Loading…
Reference in New Issue
Block a user