Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add YK support on CLI. #3416

Merged
merged 1 commit into from
Sep 22, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
967 changes: 484 additions & 483 deletions CHANGELOG.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ set(keepassx_SOURCES
keys/FileKey.cpp
keys/PasswordKey.cpp
keys/YkChallengeResponseKey.cpp
keys/YkChallengeResponseKeyCLI.cpp
streams/HashedBlockStream.cpp
streams/HmacBlockStream.cpp
streams/LayeredStream.cpp
Expand Down
6 changes: 6 additions & 0 deletions src/cli/Command.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ const QCommandLineOption Command::KeyFileOption = QCommandLineOption(QStringList
const QCommandLineOption Command::NoPasswordOption =
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;

Command::Command()
Expand Down
1 change: 1 addition & 0 deletions src/cli/Command.h
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ class Command
static const QCommandLineOption QuietOption;
static const QCommandLineOption KeyFileOption;
static const QCommandLineOption NoPasswordOption;
static const QCommandLineOption YubiKeyOption;
};

#endif // KEEPASSXC_COMMAND_H
4 changes: 4 additions & 0 deletions src/cli/DatabaseCommand.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ DatabaseCommand::DatabaseCommand()
positionalArguments.append({QString("database"), QObject::tr("Path of the database."), QString("")});
options.append(Command::KeyFileOption);
options.append(Command::NoPasswordOption);
#ifdef WITH_XC_YUBIKEY
options.append(Command::YubiKeyOption);
#endif
}

int DatabaseCommand::execute(const QStringList& arguments)
Expand All @@ -37,6 +40,7 @@ int DatabaseCommand::execute(const QStringList& arguments)
auto db = Utils::unlockDatabase(args.at(0),
!parser->isSet(Command::NoPasswordOption),
parser->value(Command::KeyFileOption),
parser->value(Command::YubiKeyOption),
parser->isSet(Command::QuietOption) ? Utils::DEVNULL : Utils::STDOUT,
Utils::STDERR);
if (!db) {
Expand Down
7 changes: 7 additions & 0 deletions src/cli/Merge.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ const QCommandLineOption Merge::DryRunOption =
QCommandLineOption(QStringList() << "dry-run",
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()
{
name = QString("merge");
Expand All @@ -51,6 +54,9 @@ Merge::Merge()
options.append(Merge::KeyFileFromOption);
options.append(Merge::NoPasswordFromOption);
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("")});
}

Expand All @@ -70,6 +76,7 @@ int Merge::executeWithDatabase(QSharedPointer<Database> database, QSharedPointer
db2 = Utils::unlockDatabase(fromDatabasePath,
!parser->isSet(Merge::NoPasswordFromOption),
parser->value(Merge::KeyFileFromOption),
parser->value(Merge::YubiKeyFromOption),
parser->isSet(Command::QuietOption) ? Utils::DEVNULL : Utils::STDOUT,
Utils::STDERR);
if (!db2) {
Expand Down
1 change: 1 addition & 0 deletions src/cli/Merge.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ class Merge : public DatabaseCommand
static const QCommandLineOption SameCredentialsOption;
static const QCommandLineOption KeyFileFromOption;
static const QCommandLineOption NoPasswordFromOption;
static const QCommandLineOption YubiKeyFromOption;
static const QCommandLineOption DryRunOption;
};

Expand Down
26 changes: 26 additions & 0 deletions src/cli/Utils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ namespace Utils
QSharedPointer<Database> unlockDatabase(const QString& databaseFilename,
const bool isPasswordProtected,
const QString& keyFilename,
const QString& yubiKeySlot,
FILE* outputDescriptor,
FILE* errorDescriptor)
{
Expand Down Expand Up @@ -153,6 +154,31 @@ namespace Utils
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();
QString error;
if (db->open(databaseFilename, compositeKey, &error, false)) {
Expand Down
7 changes: 7 additions & 0 deletions src/cli/Utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@
#include "keys/PasswordKey.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
{
extern FILE* STDOUT;
Expand All @@ -38,6 +44,7 @@ namespace Utils
QSharedPointer<Database> unlockDatabase(const QString& databaseFilename,
const bool isPasswordProtected = true,
const QString& keyFilename = {},
const QString& yubiKeySlot = {},
FILE* outputDescriptor = STDOUT,
FILE* errorDescriptor = STDERR);

Expand Down
6 changes: 6 additions & 0 deletions src/cli/keepassxc-cli.1
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ Specifies a path to a key file for unlocking the database. In a merge operation
.IP "--no-password"
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>"
Silence password prompt and other secondary outputs.

Expand All @@ -91,6 +94,9 @@ Path of the key file for the second database.
.IP "--no-password-from"
Deactivate password key for the database to merge from.

.IP "--yubikey-from <slot>"
Yubikey slot for the second database.

.IP "-s, --same-credentials"
Use the same credentials for unlocking both database.

Expand Down
4 changes: 2 additions & 2 deletions src/keys/YkChallengeResponseKey.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,9 @@ QByteArray YkChallengeResponseKey::rawKey() const
/**
* 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)
Expand Down
71 changes: 71 additions & 0 deletions src/keys/YkChallengeResponseKeyCLI.cpp
Original file line number Diff line number Diff line change
@@ -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 changes: 52 additions & 0 deletions src/keys/YkChallengeResponseKeyCLI.h
Original file line number Diff line number Diff line change
@@ -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
66 changes: 44 additions & 22 deletions src/keys/drivers/YubiKey.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -132,27 +132,17 @@ void YubiKey::detect()
{
bool found = false;

if (init()) {
droidmonkey marked this conversation as resolved.
Show resolved Hide resolved
YubiKey::ChallengeResult result;
QByteArray rand = randomGen()->randomArray(1);
QByteArray resp;

// Check slot 1 and 2 for Challenge-Response HMAC capability
for (int i = 1; i <= 2; ++i) {
result = challenge(i, false, rand, resp);
if (result == ALREADY_RUNNING) {
// Try this slot again after waiting
Tools::sleep(300);
result = challenge(i, false, rand, resp);
}

if (result != ALREADY_RUNNING && result != ERROR) {
emit detected(i, result == WOULDBLOCK);
found = true;
}
// Wait between slots to let the yubikey settle
Tools::sleep(150);
// Check slot 1 and 2 for Challenge-Response HMAC capability
for (int i = 1; i <= 2; ++i) {
QString errorMsg;
bool isBlocking = checkSlotIsBlocking(i, errorMsg);
if (errorMsg.isEmpty()) {
found = true;
emit detected(i, isBlocking);
}

// Wait between slots to let the yubikey settle.
Tools::sleep(150);
}

if (!found) {
Expand All @@ -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)
{
m_mutex.lock();
Expand Down Expand Up @@ -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;
QByteArray paddedChallenge = challenge;

// yk_challenge_response() insists on 64 byte response buffer */
// yk_challenge_response() insists on 64 bytes response buffer */
response.clear();
response.resize(64);

/* The challenge sent to the yubikey should always be 64 bytes for
* 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.
*/
const int padLen = 64 - paddedChallenge.size();
Expand Down
Loading