mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2024-12-25 23:39:45 -05:00
Add support to remember quick unlock on Windows and macOS
This commit is contained in:
parent
f71cca4eba
commit
0ee002d963
@ -148,7 +148,7 @@ static const QHash<Config::ConfigKey, ConfigDirective> configStrings = {
|
|||||||
{Config::Security_NoConfirmMoveEntryToRecycleBin,{QS("Security/NoConfirmMoveEntryToRecycleBin"), Roaming, true}},
|
{Config::Security_NoConfirmMoveEntryToRecycleBin,{QS("Security/NoConfirmMoveEntryToRecycleBin"), Roaming, true}},
|
||||||
{Config::Security_EnableCopyOnDoubleClick,{QS("Security/EnableCopyOnDoubleClick"), Roaming, false}},
|
{Config::Security_EnableCopyOnDoubleClick,{QS("Security/EnableCopyOnDoubleClick"), Roaming, false}},
|
||||||
{Config::Security_QuickUnlock, {QS("Security/QuickUnlock"), Local, true}},
|
{Config::Security_QuickUnlock, {QS("Security/QuickUnlock"), Local, true}},
|
||||||
{Config::Security_DatabasePasswordMinimumQuality, {QS("Security/DatabasePasswordMinimumQuality"), Local, 0}},
|
{Config::Security_QuickUnlockRemember, {QS("Security/QuickUnlockRemember"), Local, true}},
|
||||||
|
|
||||||
// Browser
|
// Browser
|
||||||
{Config::Browser_Enabled, {QS("Browser/Enabled"), Roaming, false}},
|
{Config::Browser_Enabled, {QS("Browser/Enabled"), Roaming, false}},
|
||||||
|
@ -129,7 +129,7 @@ public:
|
|||||||
Security_NoConfirmMoveEntryToRecycleBin,
|
Security_NoConfirmMoveEntryToRecycleBin,
|
||||||
Security_EnableCopyOnDoubleClick,
|
Security_EnableCopyOnDoubleClick,
|
||||||
Security_QuickUnlock,
|
Security_QuickUnlock,
|
||||||
Security_DatabasePasswordMinimumQuality,
|
Security_QuickUnlockRemember,
|
||||||
|
|
||||||
Browser_Enabled,
|
Browser_Enabled,
|
||||||
Browser_ShowNotification,
|
Browser_ShowNotification,
|
||||||
|
@ -147,6 +147,10 @@ ApplicationSettingsWidget::ApplicationSettingsWidget(QWidget* parent)
|
|||||||
m_secUi->lockDatabaseMinimizeCheckBox->setEnabled(!state);
|
m_secUi->lockDatabaseMinimizeCheckBox->setEnabled(!state);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
connect(m_secUi->quickUnlockCheckBox, &QCheckBox::toggled, this, [this](bool state) {
|
||||||
|
m_secUi->quickUnlockRememberCheckBox->setEnabled(state);
|
||||||
|
});
|
||||||
|
|
||||||
// Set Auto-Type shortcut when changed
|
// Set Auto-Type shortcut when changed
|
||||||
connect(
|
connect(
|
||||||
m_generalUi->autoTypeShortcutWidget, &ShortcutWidget::shortcutChanged, this, [this](auto key, auto modifiers) {
|
m_generalUi->autoTypeShortcutWidget, &ShortcutWidget::shortcutChanged, this, [this](auto key, auto modifiers) {
|
||||||
@ -344,6 +348,15 @@ void ApplicationSettingsWidget::loadSettings()
|
|||||||
|
|
||||||
m_secUi->quickUnlockCheckBox->setEnabled(getQuickUnlock()->isAvailable());
|
m_secUi->quickUnlockCheckBox->setEnabled(getQuickUnlock()->isAvailable());
|
||||||
m_secUi->quickUnlockCheckBox->setChecked(config()->get(Config::Security_QuickUnlock).toBool());
|
m_secUi->quickUnlockCheckBox->setChecked(config()->get(Config::Security_QuickUnlock).toBool());
|
||||||
|
m_secUi->quickUnlockCheckBox->setToolTip(
|
||||||
|
m_secUi->quickUnlockCheckBox->isEnabled() ? QString() : tr("Quick unlock is not available on your device."));
|
||||||
|
|
||||||
|
m_secUi->quickUnlockRememberCheckBox->setEnabled(getQuickUnlock()->isAvailable()
|
||||||
|
&& getQuickUnlock()->canRemember());
|
||||||
|
m_secUi->quickUnlockRememberCheckBox->setChecked(config()->get(Config::Security_QuickUnlockRemember).toBool());
|
||||||
|
m_secUi->quickUnlockRememberCheckBox->setToolTip(m_secUi->quickUnlockRememberCheckBox->isEnabled()
|
||||||
|
? QString()
|
||||||
|
: tr("Quick unlock cannot be remembered on your device."));
|
||||||
|
|
||||||
for (const ExtraPage& page : asConst(m_extraPages)) {
|
for (const ExtraPage& page : asConst(m_extraPages)) {
|
||||||
page.loadSettings();
|
page.loadSettings();
|
||||||
@ -460,6 +473,7 @@ void ApplicationSettingsWidget::saveSettings()
|
|||||||
|
|
||||||
if (m_secUi->quickUnlockCheckBox->isEnabled()) {
|
if (m_secUi->quickUnlockCheckBox->isEnabled()) {
|
||||||
config()->set(Config::Security_QuickUnlock, m_secUi->quickUnlockCheckBox->isChecked());
|
config()->set(Config::Security_QuickUnlock, m_secUi->quickUnlockCheckBox->isChecked());
|
||||||
|
config()->set(Config::Security_QuickUnlockRemember, m_secUi->quickUnlockRememberCheckBox->isChecked());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Security: clear storage if related settings are disabled
|
// Security: clear storage if related settings are disabled
|
||||||
|
@ -148,7 +148,7 @@
|
|||||||
<property name="sizeHint" stdset="0">
|
<property name="sizeHint" stdset="0">
|
||||||
<size>
|
<size>
|
||||||
<width>40</width>
|
<width>40</width>
|
||||||
<height>20</height>
|
<height>0</height>
|
||||||
</size>
|
</size>
|
||||||
</property>
|
</property>
|
||||||
</spacer>
|
</spacer>
|
||||||
@ -174,7 +174,7 @@
|
|||||||
<property name="sizeHint" stdset="0">
|
<property name="sizeHint" stdset="0">
|
||||||
<size>
|
<size>
|
||||||
<width>30</width>
|
<width>30</width>
|
||||||
<height>20</height>
|
<height>0</height>
|
||||||
</size>
|
</size>
|
||||||
</property>
|
</property>
|
||||||
</spacer>
|
</spacer>
|
||||||
@ -210,7 +210,7 @@
|
|||||||
<property name="sizeHint" stdset="0">
|
<property name="sizeHint" stdset="0">
|
||||||
<size>
|
<size>
|
||||||
<width>30</width>
|
<width>30</width>
|
||||||
<height>20</height>
|
<height>0</height>
|
||||||
</size>
|
</size>
|
||||||
</property>
|
</property>
|
||||||
</spacer>
|
</spacer>
|
||||||
@ -250,7 +250,7 @@
|
|||||||
<property name="sizeHint" stdset="0">
|
<property name="sizeHint" stdset="0">
|
||||||
<size>
|
<size>
|
||||||
<width>30</width>
|
<width>30</width>
|
||||||
<height>20</height>
|
<height>0</height>
|
||||||
</size>
|
</size>
|
||||||
</property>
|
</property>
|
||||||
</spacer>
|
</spacer>
|
||||||
@ -315,7 +315,7 @@
|
|||||||
<property name="sizeHint" stdset="0">
|
<property name="sizeHint" stdset="0">
|
||||||
<size>
|
<size>
|
||||||
<width>40</width>
|
<width>40</width>
|
||||||
<height>20</height>
|
<height>0</height>
|
||||||
</size>
|
</size>
|
||||||
</property>
|
</property>
|
||||||
</spacer>
|
</spacer>
|
||||||
@ -483,7 +483,7 @@
|
|||||||
<property name="sizeHint" stdset="0">
|
<property name="sizeHint" stdset="0">
|
||||||
<size>
|
<size>
|
||||||
<width>30</width>
|
<width>30</width>
|
||||||
<height>20</height>
|
<height>0</height>
|
||||||
</size>
|
</size>
|
||||||
</property>
|
</property>
|
||||||
</spacer>
|
</spacer>
|
||||||
@ -513,7 +513,7 @@
|
|||||||
<property name="sizeHint" stdset="0">
|
<property name="sizeHint" stdset="0">
|
||||||
<size>
|
<size>
|
||||||
<width>40</width>
|
<width>40</width>
|
||||||
<height>20</height>
|
<height>0</height>
|
||||||
</size>
|
</size>
|
||||||
</property>
|
</property>
|
||||||
</spacer>
|
</spacer>
|
||||||
@ -593,7 +593,7 @@
|
|||||||
<property name="sizeHint" stdset="0">
|
<property name="sizeHint" stdset="0">
|
||||||
<size>
|
<size>
|
||||||
<width>30</width>
|
<width>30</width>
|
||||||
<height>20</height>
|
<height>0</height>
|
||||||
</size>
|
</size>
|
||||||
</property>
|
</property>
|
||||||
</spacer>
|
</spacer>
|
||||||
@ -626,7 +626,7 @@
|
|||||||
<property name="sizeHint" stdset="0">
|
<property name="sizeHint" stdset="0">
|
||||||
<size>
|
<size>
|
||||||
<width>30</width>
|
<width>30</width>
|
||||||
<height>20</height>
|
<height>0</height>
|
||||||
</size>
|
</size>
|
||||||
</property>
|
</property>
|
||||||
</spacer>
|
</spacer>
|
||||||
@ -688,7 +688,7 @@
|
|||||||
<property name="sizeHint" stdset="0">
|
<property name="sizeHint" stdset="0">
|
||||||
<size>
|
<size>
|
||||||
<width>40</width>
|
<width>40</width>
|
||||||
<height>20</height>
|
<height>0</height>
|
||||||
</size>
|
</size>
|
||||||
</property>
|
</property>
|
||||||
</spacer>
|
</spacer>
|
||||||
@ -890,7 +890,7 @@
|
|||||||
<property name="sizeHint" stdset="0">
|
<property name="sizeHint" stdset="0">
|
||||||
<size>
|
<size>
|
||||||
<width>30</width>
|
<width>30</width>
|
||||||
<height>20</height>
|
<height>0</height>
|
||||||
</size>
|
</size>
|
||||||
</property>
|
</property>
|
||||||
</spacer>
|
</spacer>
|
||||||
@ -935,7 +935,7 @@
|
|||||||
<property name="sizeHint" stdset="0">
|
<property name="sizeHint" stdset="0">
|
||||||
<size>
|
<size>
|
||||||
<width>40</width>
|
<width>40</width>
|
||||||
<height>20</height>
|
<height>0</height>
|
||||||
</size>
|
</size>
|
||||||
</property>
|
</property>
|
||||||
</spacer>
|
</spacer>
|
||||||
@ -992,7 +992,7 @@
|
|||||||
<property name="sizeHint" stdset="0">
|
<property name="sizeHint" stdset="0">
|
||||||
<size>
|
<size>
|
||||||
<width>30</width>
|
<width>30</width>
|
||||||
<height>20</height>
|
<height>0</height>
|
||||||
</size>
|
</size>
|
||||||
</property>
|
</property>
|
||||||
</spacer>
|
</spacer>
|
||||||
|
@ -138,7 +138,7 @@
|
|||||||
<property name="sizeHint" stdset="0">
|
<property name="sizeHint" stdset="0">
|
||||||
<size>
|
<size>
|
||||||
<width>40</width>
|
<width>40</width>
|
||||||
<height>20</height>
|
<height>0</height>
|
||||||
</size>
|
</size>
|
||||||
</property>
|
</property>
|
||||||
</spacer>
|
</spacer>
|
||||||
@ -168,10 +168,40 @@
|
|||||||
<item>
|
<item>
|
||||||
<widget class="QCheckBox" name="quickUnlockCheckBox">
|
<widget class="QCheckBox" name="quickUnlockCheckBox">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Enable database quick unlock (Touch ID / Windows Hello)</string>
|
<string>Enable database quick unlock (Touch ID / Windows Hello / Polkit)</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||||
|
<property name="spacing">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<item>
|
||||||
|
<spacer name="horizontalSpacer_2">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeType">
|
||||||
|
<enum>QSizePolicy::Fixed</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>30</width>
|
||||||
|
<height>0</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QCheckBox" name="quickUnlockRememberCheckBox">
|
||||||
|
<property name="text">
|
||||||
|
<string>Remember quick unlock after database is closed</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QCheckBox" name="lockDatabaseOnScreenLockCheckBox">
|
<widget class="QCheckBox" name="lockDatabaseOnScreenLockCheckBox">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
|
@ -356,7 +356,10 @@ void DatabaseOpenWidget::openDatabase()
|
|||||||
// Save Quick Unlock credentials if available
|
// Save Quick Unlock credentials if available
|
||||||
if (!blockQuickUnlock && isQuickUnlockAvailable()) {
|
if (!blockQuickUnlock && isQuickUnlockAvailable()) {
|
||||||
auto keyData = databaseKey->serialize();
|
auto keyData = databaseKey->serialize();
|
||||||
getQuickUnlock()->setKey(m_db->publicUuid(), keyData);
|
if (!getQuickUnlock()->setKey(m_db->publicUuid(), keyData) && !getQuickUnlock()->errorString().isEmpty()) {
|
||||||
|
getMainWindow()->displayTabMessage(getQuickUnlock()->errorString(),
|
||||||
|
MessageWidget::MessageType::Warning);
|
||||||
|
}
|
||||||
m_ui->messageWidget->hideMessage();
|
m_ui->messageWidget->hideMessage();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1917,7 +1917,10 @@ void DatabaseWidget::closeEvent(QCloseEvent* event)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
m_databaseOpenWidget->resetQuickUnlock();
|
// Reset quick unlock if we are not remembering it
|
||||||
|
if (!config()->get(Config::Security_QuickUnlockRemember).toBool()) {
|
||||||
|
m_databaseOpenWidget->resetQuickUnlock();
|
||||||
|
}
|
||||||
event->accept();
|
event->accept();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -110,14 +110,14 @@ bool Polkit::setKey(const QUuid& dbUuid, const QByteArray& key)
|
|||||||
|
|
||||||
SymmetricCipher aes256Encrypt;
|
SymmetricCipher aes256Encrypt;
|
||||||
if (!aes256Encrypt.init(SymmetricCipher::Aes256_GCM, SymmetricCipher::Encrypt, randomKey, randomIV)) {
|
if (!aes256Encrypt.init(SymmetricCipher::Aes256_GCM, SymmetricCipher::Encrypt, randomKey, randomIV)) {
|
||||||
m_error = QObject::tr("AES initialization failed");
|
m_error = QObject::tr("Failed to init KeePassXC crypto.");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Encrypt the master password
|
// Encrypt the master password
|
||||||
QByteArray encryptedMasterKey = key;
|
QByteArray encryptedMasterKey = key;
|
||||||
if (!aes256Encrypt.finish(encryptedMasterKey)) {
|
if (!aes256Encrypt.finish(encryptedMasterKey)) {
|
||||||
m_error = QObject::tr("AES encrypt failed");
|
m_error = QObject::tr("Failed to encrypt key data.");
|
||||||
qDebug() << "polkit aes encrypt failed: " << aes256Encrypt.errorString();
|
qDebug() << "polkit aes encrypt failed: " << aes256Encrypt.errorString();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -129,7 +129,7 @@ bool Polkit::setKey(const QUuid& dbUuid, const QByteArray& key)
|
|||||||
keychainKeyValue.size(),
|
keychainKeyValue.size(),
|
||||||
KEY_SPEC_PROCESS_KEYRING);
|
KEY_SPEC_PROCESS_KEYRING);
|
||||||
if (key_serial < 0) {
|
if (key_serial < 0) {
|
||||||
m_error = QObject::tr("Failed to store in Linux Keyring");
|
m_error = QObject::tr("Failed to store key in Linux Keyring. Quick unlock has not been enabled.");
|
||||||
qDebug() << "polkit keyring failed to store: " << errno;
|
qDebug() << "polkit keyring failed to store: " << errno;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -181,7 +181,7 @@ bool Polkit::getKey(const QUuid& dbUuid, QByteArray& key)
|
|||||||
find_key_by_type_and_desc("user", getKeyName(dbUuid).toStdString().c_str(), KEY_SPEC_PROCESS_KEYRING);
|
find_key_by_type_and_desc("user", getKeyName(dbUuid).toStdString().c_str(), KEY_SPEC_PROCESS_KEYRING);
|
||||||
|
|
||||||
if (keySerial == -1) {
|
if (keySerial == -1) {
|
||||||
m_error = QObject::tr("Could not locate key in keyring");
|
m_error = QObject::tr("Could not locate key in Linux Keyring.");
|
||||||
qDebug() << "polkit keyring failed to find: " << errno;
|
qDebug() << "polkit keyring failed to find: " << errno;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -190,7 +190,7 @@ bool Polkit::getKey(const QUuid& dbUuid, QByteArray& key)
|
|||||||
long keychainDataSize = keyctl_read_alloc(keySerial, &keychainBuffer);
|
long keychainDataSize = keyctl_read_alloc(keySerial, &keychainBuffer);
|
||||||
|
|
||||||
if (keychainDataSize == -1) {
|
if (keychainDataSize == -1) {
|
||||||
m_error = QObject::tr("Could not read key in keyring");
|
m_error = QObject::tr("Could not read key in Linux Keyring.");
|
||||||
qDebug() << "polkit keyring failed to read: " << errno;
|
qDebug() << "polkit keyring failed to read: " << errno;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -205,7 +205,7 @@ bool Polkit::getKey(const QUuid& dbUuid, QByteArray& key)
|
|||||||
|
|
||||||
SymmetricCipher aes256Decrypt;
|
SymmetricCipher aes256Decrypt;
|
||||||
if (!aes256Decrypt.init(SymmetricCipher::Aes256_GCM, SymmetricCipher::Decrypt, keychainKey, keychainIv)) {
|
if (!aes256Decrypt.init(SymmetricCipher::Aes256_GCM, SymmetricCipher::Decrypt, keychainKey, keychainIv)) {
|
||||||
m_error = QObject::tr("AES initialization failed");
|
m_error = QObject::tr("Failed to init KeePassXC crypto.");
|
||||||
qDebug() << "polkit aes init failed";
|
qDebug() << "polkit aes init failed";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -213,7 +213,7 @@ bool Polkit::getKey(const QUuid& dbUuid, QByteArray& key)
|
|||||||
key = encryptedMasterKey;
|
key = encryptedMasterKey;
|
||||||
if (!aes256Decrypt.finish(key)) {
|
if (!aes256Decrypt.finish(key)) {
|
||||||
key.clear();
|
key.clear();
|
||||||
m_error = QObject::tr("AES decrypt failed");
|
m_error = QObject::tr("Failed to decrypt key data.");
|
||||||
qDebug() << "polkit aes decrypt failed: " << aes256Decrypt.errorString();
|
qDebug() << "polkit aes decrypt failed: " << aes256Decrypt.errorString();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -229,14 +229,19 @@ bool Polkit::getKey(const QUuid& dbUuid, QByteArray& key)
|
|||||||
|
|
||||||
// Failed to authenticate
|
// Failed to authenticate
|
||||||
if (authResult.is_challenge) {
|
if (authResult.is_challenge) {
|
||||||
m_error = QObject::tr("No Polkit authentication agent was available");
|
m_error = QObject::tr("No Polkit authentication agent was available.");
|
||||||
} else {
|
} else {
|
||||||
m_error = QObject::tr("Polkit authorization failed");
|
m_error = QObject::tr("Polkit authorization failed.");
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool Polkit::canRemember() const
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
bool Polkit::hasKey(const QUuid& dbUuid) const
|
bool Polkit::hasKey(const QUuid& dbUuid) const
|
||||||
{
|
{
|
||||||
if (!m_encryptedMasterKeys.contains(dbUuid)) {
|
if (!m_encryptedMasterKeys.contains(dbUuid)) {
|
||||||
|
@ -36,6 +36,8 @@ public:
|
|||||||
bool getKey(const QUuid& dbUuid, QByteArray& key) override;
|
bool getKey(const QUuid& dbUuid, QByteArray& key) override;
|
||||||
bool hasKey(const QUuid& dbUuid) const override;
|
bool hasKey(const QUuid& dbUuid) const override;
|
||||||
|
|
||||||
|
bool canRemember() const override;
|
||||||
|
|
||||||
void reset(const QUuid& dbUuid) override;
|
void reset(const QUuid& dbUuid) override;
|
||||||
void reset() override;
|
void reset() override;
|
||||||
|
|
||||||
|
@ -75,6 +75,11 @@ bool NoQuickUnlock::hasKey(const QUuid& dbUuid) const
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool NoQuickUnlock::canRemember() const
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
void NoQuickUnlock::reset(const QUuid& dbUuid)
|
void NoQuickUnlock::reset(const QUuid& dbUuid)
|
||||||
{
|
{
|
||||||
Q_UNUSED(dbUuid)
|
Q_UNUSED(dbUuid)
|
||||||
|
@ -35,6 +35,8 @@ public:
|
|||||||
virtual bool getKey(const QUuid& dbUuid, QByteArray& key) = 0;
|
virtual bool getKey(const QUuid& dbUuid, QByteArray& key) = 0;
|
||||||
virtual bool hasKey(const QUuid& dbUuid) const = 0;
|
virtual bool hasKey(const QUuid& dbUuid) const = 0;
|
||||||
|
|
||||||
|
virtual bool canRemember() const = 0;
|
||||||
|
|
||||||
virtual void reset(const QUuid& dbUuid) = 0;
|
virtual void reset(const QUuid& dbUuid) = 0;
|
||||||
virtual void reset() = 0;
|
virtual void reset() = 0;
|
||||||
};
|
};
|
||||||
@ -49,6 +51,8 @@ public:
|
|||||||
bool getKey(const QUuid& dbUuid, QByteArray& key) override;
|
bool getKey(const QUuid& dbUuid, QByteArray& key) override;
|
||||||
bool hasKey(const QUuid& dbUuid) const override;
|
bool hasKey(const QUuid& dbUuid) const override;
|
||||||
|
|
||||||
|
bool canRemember() const override;
|
||||||
|
|
||||||
void reset(const QUuid& dbUuid) override;
|
void reset(const QUuid& dbUuid) override;
|
||||||
void reset() override;
|
void reset() override;
|
||||||
};
|
};
|
||||||
|
@ -31,6 +31,8 @@ public:
|
|||||||
bool getKey(const QUuid& dbUuid, QByteArray& passwordKey) override;
|
bool getKey(const QUuid& dbUuid, QByteArray& passwordKey) override;
|
||||||
bool hasKey(const QUuid& dbUuid) const override;
|
bool hasKey(const QUuid& dbUuid) const override;
|
||||||
|
|
||||||
|
bool canRemember() const override;
|
||||||
|
|
||||||
void reset(const QUuid& dbUuid = "") override;
|
void reset(const QUuid& dbUuid = "") override;
|
||||||
void reset() override;
|
void reset() override;
|
||||||
|
|
||||||
|
@ -68,7 +68,7 @@ void TouchID::deleteKeyEntry(const QString& accountName)
|
|||||||
|
|
||||||
// get data from the KeyChain
|
// get data from the KeyChain
|
||||||
OSStatus status = SecItemDelete(query);
|
OSStatus status = SecItemDelete(query);
|
||||||
LogStatusError("TouchID::deleteKeyEntry - Status deleting existing entry", status);
|
LogStatusError("TouchID::deleteKeyEntry - Error deleting existing entry", status);
|
||||||
}
|
}
|
||||||
|
|
||||||
QString TouchID::databaseKeyName(const QUuid& dbUuid)
|
QString TouchID::databaseKeyName(const QUuid& dbUuid)
|
||||||
@ -88,41 +88,20 @@ void TouchID::reset()
|
|||||||
m_encryptedMasterKeys.clear();
|
m_encryptedMasterKeys.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store the serialized database key into the macOS key store. The OS handles encrypt/decrypt operations.
|
||||||
|
* https://developer.apple.com/documentation/security/keychain_services/keychain_items
|
||||||
bool TouchID::setKey(const QUuid& dbUuid, const QByteArray& passwordKey, const bool ignoreTouchID)
|
*/
|
||||||
|
bool TouchID::setKey(const QUuid& dbUuid, const QByteArray& key, const bool ignoreTouchID)
|
||||||
{
|
{
|
||||||
if (passwordKey.isEmpty()) {
|
if (key.isEmpty()) {
|
||||||
debug("TouchID::setKey - illegal arguments");
|
debug("TouchID::setKey - illegal arguments");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m_encryptedMasterKeys.contains(dbUuid)) {
|
const auto keyName = databaseKeyName(dbUuid);
|
||||||
debug("TouchID::setKey - Already stored key for this database");
|
// Try to delete the existing key entry
|
||||||
return true;
|
deleteKeyEntry(keyName);
|
||||||
}
|
|
||||||
|
|
||||||
// generate random AES 256bit key and IV
|
|
||||||
QByteArray randomKey = randomGen()->randomArray(SymmetricCipher::keySize(SymmetricCipher::Aes256_GCM));
|
|
||||||
QByteArray randomIV = randomGen()->randomArray(SymmetricCipher::defaultIvSize(SymmetricCipher::Aes256_GCM));
|
|
||||||
|
|
||||||
SymmetricCipher aes256Encrypt;
|
|
||||||
if (!aes256Encrypt.init(SymmetricCipher::Aes256_GCM, SymmetricCipher::Encrypt, randomKey, randomIV)) {
|
|
||||||
debug("TouchID::setKey - AES initialisation failed");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// encrypt and keep result in memory
|
|
||||||
QByteArray encryptedMasterKey = passwordKey;
|
|
||||||
if (!aes256Encrypt.finish(encryptedMasterKey)) {
|
|
||||||
debug("TouchID::getKey - AES encrypt failed: %s", aes256Encrypt.errorString().toUtf8().constData());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const QString keyName = databaseKeyName(dbUuid);
|
|
||||||
|
|
||||||
deleteKeyEntry(keyName); // Try to delete the existing key entry
|
|
||||||
|
|
||||||
// prepare adding secure entry to the macOS KeyChain
|
// prepare adding secure entry to the macOS KeyChain
|
||||||
CFErrorRef error = NULL;
|
CFErrorRef error = NULL;
|
||||||
@ -168,30 +147,33 @@ bool TouchID::setKey(const QUuid& dbUuid, const QByteArray& passwordKey, const b
|
|||||||
kCFAllocatorDefault, kSecAttrAccessibleWhenUnlockedThisDeviceOnly, accessControlFlags, &error);
|
kCFAllocatorDefault, kSecAttrAccessibleWhenUnlockedThisDeviceOnly, accessControlFlags, &error);
|
||||||
|
|
||||||
if (sacObject == NULL || error != NULL) {
|
if (sacObject == NULL || error != NULL) {
|
||||||
NSError* e = (__bridge NSError*) error;
|
auto e = (__bridge NSError*) error;
|
||||||
debug("TouchID::setKey - Error creating security flags: %s", e.localizedDescription.UTF8String);
|
debug("TouchID::setKey - Error creating security flags: %s", e.localizedDescription.UTF8String);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
NSString *accountName = keyName.toNSString(); // The NSString is released by Qt
|
auto accountName = keyName.toNSString();
|
||||||
|
auto keyBase64 = key.toBase64();
|
||||||
|
|
||||||
// prepare data (key) to be stored
|
// prepare data (key) to be stored
|
||||||
QByteArray keychainKeyValue = (randomKey + randomIV).toHex();
|
auto keyValueData = CFDataCreateWithBytesNoCopy(
|
||||||
CFDataRef keychainValueData =
|
kCFAllocatorDefault, reinterpret_cast<const UInt8 *>(keyBase64.data()),
|
||||||
CFDataCreateWithBytesNoCopy(kCFAllocatorDefault, reinterpret_cast<UInt8 *>(keychainKeyValue.data()),
|
keyBase64.length(), kCFAllocatorDefault);
|
||||||
keychainKeyValue.length(), kCFAllocatorDefault);
|
|
||||||
|
|
||||||
CFMutableDictionaryRef attributes = makeDictionary();
|
auto attributes = makeDictionary();
|
||||||
CFDictionarySetValue(attributes, kSecClass, kSecClassGenericPassword);
|
CFDictionarySetValue(attributes, kSecClass, kSecClassGenericPassword);
|
||||||
CFDictionarySetValue(attributes, kSecAttrAccount, (__bridge CFStringRef) accountName);
|
CFDictionarySetValue(attributes, kSecAttrAccount, (__bridge CFStringRef) accountName);
|
||||||
CFDictionarySetValue(attributes, kSecValueData, (__bridge CFDataRef) keychainValueData);
|
CFDictionarySetValue(attributes, kSecValueData, (__bridge CFDataRef) keyValueData);
|
||||||
CFDictionarySetValue(attributes, kSecAttrSynchronizable, kCFBooleanFalse);
|
CFDictionarySetValue(attributes, kSecAttrSynchronizable, kCFBooleanFalse);
|
||||||
CFDictionarySetValue(attributes, kSecUseAuthenticationUI, kSecUseAuthenticationUIAllow);
|
CFDictionarySetValue(attributes, kSecUseAuthenticationUI, kSecUseAuthenticationUIAllow);
|
||||||
|
#ifndef QT_DEBUG
|
||||||
|
// Only use TouchID when in release build, also requires application entitlements and signing
|
||||||
CFDictionarySetValue(attributes, kSecAttrAccessControl, sacObject);
|
CFDictionarySetValue(attributes, kSecAttrAccessControl, sacObject);
|
||||||
|
#endif
|
||||||
|
|
||||||
// add to KeyChain
|
// add to KeyChain
|
||||||
OSStatus status = SecItemAdd(attributes, NULL);
|
OSStatus status = SecItemAdd(attributes, NULL);
|
||||||
LogStatusError("TouchID::setKey - Status adding new entry", status);
|
LogStatusError("TouchID::setKey - Error adding new keychain item", status);
|
||||||
|
|
||||||
CFRelease(sacObject);
|
CFRelease(sacObject);
|
||||||
CFRelease(attributes);
|
CFRelease(attributes);
|
||||||
@ -226,12 +208,12 @@ bool TouchID::setKey(const QUuid& dbUuid, const QByteArray& passwordKey)
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if an encrypted PasswordKey is available for the given database, tries to
|
* Retrieve serialized key data from the macOS Keychain after successful authentication
|
||||||
* decrypt it using the KeyChain and if successful, returns it.
|
* with TouchID or Watch interface.
|
||||||
*/
|
*/
|
||||||
bool TouchID::getKey(const QUuid& dbUuid, QByteArray& passwordKey)
|
bool TouchID::getKey(const QUuid& dbUuid, QByteArray& key)
|
||||||
{
|
{
|
||||||
passwordKey.clear();
|
key.clear();
|
||||||
|
|
||||||
if (!hasKey(dbUuid)) {
|
if (!hasKey(dbUuid)) {
|
||||||
debug("TouchID::getKey - No stored key found");
|
debug("TouchID::getKey - No stored key found");
|
||||||
@ -266,39 +248,30 @@ bool TouchID::getKey(const QUuid& dbUuid, QByteArray& passwordKey)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Convert value returned to serialized key
|
||||||
CFDataRef valueData = static_cast<CFDataRef>(dataTypeRef);
|
CFDataRef valueData = static_cast<CFDataRef>(dataTypeRef);
|
||||||
QByteArray dataBytes = QByteArray::fromHex(QByteArray(reinterpret_cast<const char*>(CFDataGetBytePtr(valueData)),
|
key = QByteArray::fromBase64(QByteArray(reinterpret_cast<const char*>(CFDataGetBytePtr(valueData)),
|
||||||
CFDataGetLength(valueData)));
|
CFDataGetLength(valueData)));
|
||||||
CFRelease(dataTypeRef);
|
CFRelease(dataTypeRef);
|
||||||
|
|
||||||
// extract AES key and IV from data bytes
|
|
||||||
QByteArray key = dataBytes.left(SymmetricCipher::keySize(SymmetricCipher::Aes256_GCM));
|
|
||||||
QByteArray iv = dataBytes.right(SymmetricCipher::defaultIvSize(SymmetricCipher::Aes256_GCM));
|
|
||||||
|
|
||||||
SymmetricCipher aes256Decrypt;
|
|
||||||
if (!aes256Decrypt.init(SymmetricCipher::Aes256_GCM, SymmetricCipher::Decrypt, key, iv)) {
|
|
||||||
debug("TouchID::getKey - AES initialization failed");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// decrypt PasswordKey from memory using AES
|
|
||||||
passwordKey = m_encryptedMasterKeys[dbUuid];
|
|
||||||
if (!aes256Decrypt.finish(passwordKey)) {
|
|
||||||
passwordKey.clear();
|
|
||||||
debug("TouchID::getKey - AES decrypt failed: %s", aes256Decrypt.errorString().toUtf8().constData());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cleanse the key information from the memory
|
|
||||||
Botan::secure_scrub_memory(key.data(), key.size());
|
|
||||||
Botan::secure_scrub_memory(iv.data(), iv.size());
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool TouchID::hasKey(const QUuid& dbUuid) const
|
bool TouchID::hasKey(const QUuid& dbUuid) const
|
||||||
{
|
{
|
||||||
return m_encryptedMasterKeys.contains(dbUuid);
|
const QString keyName = databaseKeyName(dbUuid);
|
||||||
|
NSString* accountName = keyName.toNSString(); // The NSString is released by Qt
|
||||||
|
|
||||||
|
CFMutableDictionaryRef query = makeDictionary();
|
||||||
|
CFDictionarySetValue(query, kSecClass, kSecClassGenericPassword);
|
||||||
|
CFDictionarySetValue(query, kSecAttrAccount, (__bridge CFStringRef) accountName);
|
||||||
|
CFDictionarySetValue(query, kSecReturnData, kCFBooleanFalse);
|
||||||
|
|
||||||
|
CFTypeRef item = NULL;
|
||||||
|
OSStatus status = SecItemCopyMatching(query, &item);
|
||||||
|
CFRelease(query);
|
||||||
|
|
||||||
|
return status == errSecSuccess;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Both functions below should probably handle the returned errors to
|
// TODO: Both functions below should probably handle the returned errors to
|
||||||
@ -320,11 +293,7 @@ bool TouchID::isWatchAvailable()
|
|||||||
bool canAuthenticate = [context canEvaluatePolicy:policyCode error:&error];
|
bool canAuthenticate = [context canEvaluatePolicy:policyCode error:&error];
|
||||||
[context release];
|
[context release];
|
||||||
if (error) {
|
if (error) {
|
||||||
debug("Apple Wach available: %d (%ld / %s / %s)", canAuthenticate,
|
debug("Apple Watch is not available: %s", error.localizedDescription.UTF8String);
|
||||||
(long)error.code, error.description.UTF8String,
|
|
||||||
error.localizedDescription.UTF8String);
|
|
||||||
} else {
|
|
||||||
debug("Apple Wach available: %d", canAuthenticate);
|
|
||||||
}
|
}
|
||||||
return canAuthenticate;
|
return canAuthenticate;
|
||||||
} @catch (NSException *) {
|
} @catch (NSException *) {
|
||||||
@ -348,11 +317,7 @@ bool TouchID::isTouchIdAvailable()
|
|||||||
bool canAuthenticate = [context canEvaluatePolicy:policyCode error:&error];
|
bool canAuthenticate = [context canEvaluatePolicy:policyCode error:&error];
|
||||||
[context release];
|
[context release];
|
||||||
if (error) {
|
if (error) {
|
||||||
debug("Touch ID available: %d (%ld / %s / %s)", canAuthenticate,
|
debug("Touch ID is not available: %s", error.localizedDescription.UTF8String);
|
||||||
(long)error.code, error.description.UTF8String,
|
|
||||||
error.localizedDescription.UTF8String);
|
|
||||||
} else {
|
|
||||||
debug("Touch ID available: %d", canAuthenticate);
|
|
||||||
}
|
}
|
||||||
return canAuthenticate;
|
return canAuthenticate;
|
||||||
} @catch (NSException *) {
|
} @catch (NSException *) {
|
||||||
@ -399,10 +364,15 @@ bool TouchID::isAvailable() const
|
|||||||
return isWatchAvailable() || isTouchIdAvailable() || isPasswordFallbackPossible();
|
return isWatchAvailable() || isTouchIdAvailable() || isPasswordFallbackPossible();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool TouchID::canRemember() const
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resets the inner state either for all or for the given database
|
* Resets the inner state either for all or for the given database
|
||||||
*/
|
*/
|
||||||
void TouchID::reset(const QUuid& dbUuid)
|
void TouchID::reset(const QUuid& dbUuid)
|
||||||
{
|
{
|
||||||
m_encryptedMasterKeys.remove(dbUuid);
|
deleteKeyEntry(databaseKeyName(dbUuid));
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
|
|
||||||
#include <Userconsentverifierinterop.h>
|
#include <Userconsentverifierinterop.h>
|
||||||
#include <winrt/base.h>
|
#include <winrt/base.h>
|
||||||
|
#include <winrt/windows.foundation.collections.h>
|
||||||
#include <winrt/windows.foundation.h>
|
#include <winrt/windows.foundation.h>
|
||||||
#include <winrt/windows.security.credentials.h>
|
#include <winrt/windows.security.credentials.h>
|
||||||
#include <winrt/windows.security.cryptography.h>
|
#include <winrt/windows.security.cryptography.h>
|
||||||
@ -34,6 +35,7 @@
|
|||||||
|
|
||||||
using namespace winrt;
|
using namespace winrt;
|
||||||
using namespace Windows::Foundation;
|
using namespace Windows::Foundation;
|
||||||
|
using namespace Windows::Foundation::Collections;
|
||||||
using namespace Windows::Security::Credentials;
|
using namespace Windows::Security::Credentials;
|
||||||
using namespace Windows::Security::Cryptography;
|
using namespace Windows::Security::Cryptography;
|
||||||
using namespace Windows::Storage::Streams;
|
using namespace Windows::Storage::Streams;
|
||||||
@ -97,6 +99,47 @@ namespace
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void storeCredential(const QUuid& uuid, const QByteArray& data)
|
||||||
|
{
|
||||||
|
auto vault = PasswordVault();
|
||||||
|
vault.Add({s_winHelloKeyName,
|
||||||
|
winrt::to_hstring(uuid.toString().toStdString()),
|
||||||
|
winrt::to_hstring(data.toBase64().toStdString())});
|
||||||
|
}
|
||||||
|
|
||||||
|
void removeCredential(const QUuid& uuid)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
auto vault = PasswordVault();
|
||||||
|
vault.Remove({s_winHelloKeyName, winrt::to_hstring(uuid.toString().toStdString()), L"blah"});
|
||||||
|
} catch (winrt::hresult_error const& ex) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void resetCredentials()
|
||||||
|
{
|
||||||
|
auto vault = PasswordVault();
|
||||||
|
auto credentials = vault.FindAllByResource(s_winHelloKeyName);
|
||||||
|
for (const auto& credential : credentials) {
|
||||||
|
try {
|
||||||
|
vault.Remove(credential);
|
||||||
|
} catch (winrt::hresult_error const& ex) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray loadCredential(const QUuid& uuid)
|
||||||
|
{
|
||||||
|
QByteArray data;
|
||||||
|
try {
|
||||||
|
auto vault = PasswordVault();
|
||||||
|
auto credential = vault.Retrieve(s_winHelloKeyName, winrt::to_hstring(uuid.toString().toStdString()));
|
||||||
|
data = QByteArray::fromBase64(QByteArray::fromStdString(winrt::to_string(credential.Password())));
|
||||||
|
} catch (winrt::hresult_error const& ex) {
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
bool WindowsHello::isAvailable() const
|
bool WindowsHello::isAvailable() const
|
||||||
@ -120,6 +163,7 @@ bool WindowsHello::setKey(const QUuid& dbUuid, const QByteArray& data)
|
|||||||
auto challenge = Random::instance()->randomArray(ivSize);
|
auto challenge = Random::instance()->randomArray(ivSize);
|
||||||
QByteArray key;
|
QByteArray key;
|
||||||
if (!deriveEncryptionKey(challenge, key, m_error)) {
|
if (!deriveEncryptionKey(challenge, key, m_error)) {
|
||||||
|
m_error = QObject::tr("Windows Hello setup was canceled or failed. Quick unlock has not been enabled.");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -137,7 +181,7 @@ bool WindowsHello::setKey(const QUuid& dbUuid, const QByteArray& data)
|
|||||||
|
|
||||||
// Prepend the challenge/IV to the encrypted data
|
// Prepend the challenge/IV to the encrypted data
|
||||||
encrypted.prepend(challenge);
|
encrypted.prepend(challenge);
|
||||||
m_encryptedKeys.insert(dbUuid, encrypted);
|
storeCredential(dbUuid, encrypted);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -153,7 +197,7 @@ bool WindowsHello::getKey(const QUuid& dbUuid, QByteArray& data)
|
|||||||
|
|
||||||
// Read the previously used challenge and encrypted data
|
// Read the previously used challenge and encrypted data
|
||||||
auto ivSize = SymmetricCipher::defaultIvSize(SymmetricCipher::Aes256_GCM);
|
auto ivSize = SymmetricCipher::defaultIvSize(SymmetricCipher::Aes256_GCM);
|
||||||
const auto& keydata = m_encryptedKeys.value(dbUuid);
|
const auto& keydata = loadCredential(dbUuid);
|
||||||
auto challenge = keydata.left(ivSize);
|
auto challenge = keydata.left(ivSize);
|
||||||
auto encrypted = keydata.mid(ivSize);
|
auto encrypted = keydata.mid(ivSize);
|
||||||
QByteArray key;
|
QByteArray key;
|
||||||
@ -182,15 +226,20 @@ bool WindowsHello::getKey(const QUuid& dbUuid, QByteArray& data)
|
|||||||
|
|
||||||
void WindowsHello::reset(const QUuid& dbUuid)
|
void WindowsHello::reset(const QUuid& dbUuid)
|
||||||
{
|
{
|
||||||
m_encryptedKeys.remove(dbUuid);
|
removeCredential(dbUuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool WindowsHello::hasKey(const QUuid& dbUuid) const
|
bool WindowsHello::hasKey(const QUuid& dbUuid) const
|
||||||
{
|
{
|
||||||
return m_encryptedKeys.contains(dbUuid);
|
return !loadCredential(dbUuid).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WindowsHello::canRemember() const
|
||||||
|
{
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void WindowsHello::reset()
|
void WindowsHello::reset()
|
||||||
{
|
{
|
||||||
m_encryptedKeys.clear();
|
resetCredentials();
|
||||||
}
|
}
|
||||||
|
@ -27,18 +27,21 @@ class WindowsHello : public QuickUnlockInterface
|
|||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
WindowsHello() = default;
|
WindowsHello() = default;
|
||||||
|
|
||||||
bool isAvailable() const override;
|
bool isAvailable() const override;
|
||||||
QString errorString() const override;
|
QString errorString() const override;
|
||||||
void reset() override;
|
|
||||||
|
|
||||||
bool setKey(const QUuid& dbUuid, const QByteArray& key) override;
|
bool setKey(const QUuid& dbUuid, const QByteArray& key) override;
|
||||||
bool getKey(const QUuid& dbUuid, QByteArray& key) override;
|
bool getKey(const QUuid& dbUuid, QByteArray& key) override;
|
||||||
bool hasKey(const QUuid& dbUuid) const override;
|
bool hasKey(const QUuid& dbUuid) const override;
|
||||||
void reset(const QUuid& dbUuid) override;
|
|
||||||
|
|
||||||
|
bool canRemember() const override;
|
||||||
|
|
||||||
|
void reset(const QUuid& dbUuid) override;
|
||||||
|
void reset() override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QString m_error;
|
QString m_error;
|
||||||
QHash<QUuid, QByteArray> m_encryptedKeys;
|
|
||||||
Q_DISABLE_COPY(WindowsHello);
|
Q_DISABLE_COPY(WindowsHello);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user