mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2026-03-11 08:54:48 +00:00
466 lines
20 KiB
C++
466 lines
20 KiB
C++
|
|
/*
|
||
|
|
* Copyright (C) 2023 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 3 of the License, or
|
||
|
|
* (at your option) any later version.
|
||
|
|
*
|
||
|
|
* 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 "BrowserPasskeys.h"
|
||
|
|
#include "BrowserMessageBuilder.h"
|
||
|
|
#include "BrowserService.h"
|
||
|
|
#include "crypto/Random.h"
|
||
|
|
#include <QJsonArray>
|
||
|
|
#include <QJsonDocument>
|
||
|
|
#include <QJsonObject>
|
||
|
|
#include <QtEndian>
|
||
|
|
|
||
|
|
#include <botan/data_src.h>
|
||
|
|
#include <botan/ec_group.h>
|
||
|
|
#include <botan/ecdsa.h>
|
||
|
|
#include <botan/ed25519.h>
|
||
|
|
#include <botan/pkcs8.h>
|
||
|
|
#include <botan/pubkey.h>
|
||
|
|
#include <botan/rsa.h>
|
||
|
|
#include <botan/sodium.h>
|
||
|
|
|
||
|
|
#include <bitset>
|
||
|
|
|
||
|
|
Q_GLOBAL_STATIC(BrowserPasskeys, s_browserPasskeys);
|
||
|
|
|
||
|
|
const QString BrowserPasskeys::PUBLIC_KEY = QStringLiteral("public-key");
|
||
|
|
const QString BrowserPasskeys::REQUIREMENT_DISCOURAGED = QStringLiteral("discouraged");
|
||
|
|
const QString BrowserPasskeys::REQUIREMENT_PREFERRED = QStringLiteral("preferred");
|
||
|
|
const QString BrowserPasskeys::REQUIREMENT_REQUIRED = QStringLiteral("required");
|
||
|
|
|
||
|
|
const QString BrowserPasskeys::PASSKEYS_ATTESTATION_DIRECT = QStringLiteral("direct");
|
||
|
|
const QString BrowserPasskeys::PASSKEYS_ATTESTATION_NONE = QStringLiteral("none");
|
||
|
|
|
||
|
|
const QString BrowserPasskeys::KPEX_PASSKEY_USERNAME = QStringLiteral("KPEX_PASSKEY_USERNAME");
|
||
|
|
const QString BrowserPasskeys::KPEX_PASSKEY_GENERATED_USER_ID = QStringLiteral("KPEX_PASSKEY_GENERATED_USER_ID");
|
||
|
|
const QString BrowserPasskeys::KPEX_PASSKEY_PRIVATE_KEY_PEM = QStringLiteral("KPEX_PASSKEY_PRIVATE_KEY_PEM");
|
||
|
|
const QString BrowserPasskeys::KPEX_PASSKEY_RELYING_PARTY = QStringLiteral("KPEX_PASSKEY_RELYING_PARTY");
|
||
|
|
const QString BrowserPasskeys::KPEX_PASSKEY_USER_HANDLE = QStringLiteral("KPEX_PASSKEY_USER_HANDLE");
|
||
|
|
|
||
|
|
BrowserPasskeys* BrowserPasskeys::instance()
|
||
|
|
{
|
||
|
|
return s_browserPasskeys;
|
||
|
|
}
|
||
|
|
|
||
|
|
PublicKeyCredential BrowserPasskeys::buildRegisterPublicKeyCredential(const QJsonObject& publicKeyCredentialOptions,
|
||
|
|
const QString& origin,
|
||
|
|
const TestingVariables& testingVariables)
|
||
|
|
{
|
||
|
|
QJsonObject publicKeyCredential;
|
||
|
|
const auto id = testingVariables.credentialId.isEmpty() ? browserMessageBuilder()->getRandomBytesAsBase64(ID_BYTES)
|
||
|
|
: testingVariables.credentialId;
|
||
|
|
|
||
|
|
// Extensions
|
||
|
|
auto extensionObject = publicKeyCredentialOptions["extensions"].toObject();
|
||
|
|
const auto extensionData = buildExtensionData(extensionObject);
|
||
|
|
const auto extensions = browserMessageBuilder()->getBase64FromArray(extensionData);
|
||
|
|
|
||
|
|
// Response
|
||
|
|
QJsonObject responseObject;
|
||
|
|
const auto clientData = buildClientDataJson(publicKeyCredentialOptions, origin, false);
|
||
|
|
const auto attestationObject = buildAttestationObject(publicKeyCredentialOptions, extensions, id, testingVariables);
|
||
|
|
responseObject["clientDataJSON"] = browserMessageBuilder()->getBase64FromJson(clientData);
|
||
|
|
responseObject["attestationObject"] = browserMessageBuilder()->getBase64FromArray(attestationObject.cborEncoded);
|
||
|
|
|
||
|
|
// PublicKeyCredential
|
||
|
|
publicKeyCredential["authenticatorAttachment"] = QString("platform");
|
||
|
|
publicKeyCredential["id"] = id;
|
||
|
|
publicKeyCredential["response"] = responseObject;
|
||
|
|
publicKeyCredential["type"] = PUBLIC_KEY;
|
||
|
|
|
||
|
|
return {id, publicKeyCredential, attestationObject.pem};
|
||
|
|
}
|
||
|
|
|
||
|
|
QJsonObject BrowserPasskeys::buildGetPublicKeyCredential(const QJsonObject& publicKeyCredentialRequestOptions,
|
||
|
|
const QString& origin,
|
||
|
|
const QString& userId,
|
||
|
|
const QString& userHandle,
|
||
|
|
const QString& privateKeyPem)
|
||
|
|
{
|
||
|
|
const auto authenticatorData = buildGetAttestationObject(publicKeyCredentialRequestOptions);
|
||
|
|
const auto clientData = buildClientDataJson(publicKeyCredentialRequestOptions, origin, true);
|
||
|
|
const auto clientDataArray = QJsonDocument(clientData).toJson(QJsonDocument::Compact);
|
||
|
|
const auto signature = buildSignature(authenticatorData, clientDataArray, privateKeyPem);
|
||
|
|
|
||
|
|
QJsonObject responseObject;
|
||
|
|
responseObject["authenticatorData"] = browserMessageBuilder()->getBase64FromArray(authenticatorData);
|
||
|
|
responseObject["clientDataJSON"] = browserMessageBuilder()->getBase64FromArray(clientDataArray);
|
||
|
|
responseObject["signature"] = browserMessageBuilder()->getBase64FromArray(signature);
|
||
|
|
responseObject["userHandle"] = userHandle;
|
||
|
|
|
||
|
|
QJsonObject publicKeyCredential;
|
||
|
|
publicKeyCredential["authenticatorAttachment"] = QString("platform");
|
||
|
|
publicKeyCredential["id"] = userId;
|
||
|
|
publicKeyCredential["response"] = responseObject;
|
||
|
|
publicKeyCredential["type"] = PUBLIC_KEY;
|
||
|
|
|
||
|
|
return publicKeyCredential;
|
||
|
|
}
|
||
|
|
|
||
|
|
bool BrowserPasskeys::isUserVerificationValid(const QString& userVerification) const
|
||
|
|
{
|
||
|
|
return QStringList({REQUIREMENT_PREFERRED, REQUIREMENT_REQUIRED, REQUIREMENT_DISCOURAGED})
|
||
|
|
.contains(userVerification);
|
||
|
|
}
|
||
|
|
|
||
|
|
// See https://w3c.github.io/webauthn/#sctn-createCredential for default timeout values when not set in the request
|
||
|
|
int BrowserPasskeys::getTimeout(const QString& userVerification, int timeout) const
|
||
|
|
{
|
||
|
|
if (timeout == 0) {
|
||
|
|
return userVerification == REQUIREMENT_DISCOURAGED ? DEFAULT_DISCOURAGED_TIMEOUT : DEFAULT_TIMEOUT;
|
||
|
|
}
|
||
|
|
|
||
|
|
return timeout;
|
||
|
|
}
|
||
|
|
|
||
|
|
QStringList BrowserPasskeys::getAllowedCredentialsFromPublicKey(const QJsonObject& publicKey) const
|
||
|
|
{
|
||
|
|
QStringList allowedCredentials;
|
||
|
|
for (const auto& cred : publicKey["allowCredentials"].toArray()) {
|
||
|
|
const auto c = cred.toObject();
|
||
|
|
const auto id = c["id"].toString();
|
||
|
|
|
||
|
|
if (c["type"].toString() == PUBLIC_KEY && !id.isEmpty()) {
|
||
|
|
allowedCredentials << id;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return allowedCredentials;
|
||
|
|
}
|
||
|
|
|
||
|
|
QJsonObject BrowserPasskeys::buildClientDataJson(const QJsonObject& publicKey, const QString& origin, bool get)
|
||
|
|
{
|
||
|
|
QJsonObject clientData;
|
||
|
|
clientData["challenge"] = publicKey["challenge"];
|
||
|
|
clientData["crossOrigin"] = false;
|
||
|
|
clientData["origin"] = origin;
|
||
|
|
clientData["type"] = get ? QString("webauthn.get") : QString("webauthn.create");
|
||
|
|
|
||
|
|
return clientData;
|
||
|
|
}
|
||
|
|
|
||
|
|
// https://w3c.github.io/webauthn/#attestation-object
|
||
|
|
PrivateKey BrowserPasskeys::buildAttestationObject(const QJsonObject& publicKey,
|
||
|
|
const QString& extensions,
|
||
|
|
const QString& id,
|
||
|
|
const TestingVariables& testingVariables)
|
||
|
|
{
|
||
|
|
QByteArray result;
|
||
|
|
|
||
|
|
// Create SHA256 hash from rpId
|
||
|
|
const auto rpIdHash = browserMessageBuilder()->getSha256Hash(publicKey["rp"]["id"].toString());
|
||
|
|
result.append(rpIdHash);
|
||
|
|
|
||
|
|
// Use default flags
|
||
|
|
const auto flags =
|
||
|
|
setFlagsFromJson(QJsonObject({{"ED", !extensions.isEmpty()},
|
||
|
|
{"AT", true},
|
||
|
|
{"BS", false},
|
||
|
|
{"BE", false},
|
||
|
|
{"UV", publicKey["userVerification"].toString() != REQUIREMENT_DISCOURAGED},
|
||
|
|
{"UP", true}}));
|
||
|
|
result.append(flags);
|
||
|
|
|
||
|
|
// Signature counter (not supported, always 0
|
||
|
|
const char counter[4] = {0x00, 0x00, 0x00, 0x00};
|
||
|
|
result.append(QByteArray::fromRawData(counter, 4));
|
||
|
|
|
||
|
|
// AAGUID (use the default/non-set)
|
||
|
|
result.append("\x01\x02\x03\x04\x05\x06\x07\b\x01\x02\x03\x04\x05\x06\x07\b");
|
||
|
|
|
||
|
|
// Credential length
|
||
|
|
const char credentialLength[2] = {0x00, 0x20};
|
||
|
|
result.append(QByteArray::fromRawData(credentialLength, 2));
|
||
|
|
|
||
|
|
// Credential Id
|
||
|
|
result.append(QByteArray::fromBase64(
|
||
|
|
testingVariables.credentialId.isEmpty() ? id.toUtf8() : testingVariables.credentialId.toUtf8(),
|
||
|
|
QByteArray::Base64UrlEncoding));
|
||
|
|
|
||
|
|
// Credential private key
|
||
|
|
const auto alg = getAlgorithmFromPublicKey(publicKey);
|
||
|
|
const auto credentialPublicKey = buildCredentialPrivateKey(alg, testingVariables.first, testingVariables.second);
|
||
|
|
result.append(credentialPublicKey.cborEncoded);
|
||
|
|
|
||
|
|
// Add extension data if available
|
||
|
|
if (!extensions.isEmpty()) {
|
||
|
|
result.append(browserMessageBuilder()->getArrayFromBase64(extensions));
|
||
|
|
}
|
||
|
|
|
||
|
|
// The final result should be CBOR encoded
|
||
|
|
return {m_browserCbor.cborEncodeAttestation(result), credentialPublicKey.pem};
|
||
|
|
}
|
||
|
|
|
||
|
|
// Build a short version of the attestation object for webauthn.get
|
||
|
|
QByteArray BrowserPasskeys::buildGetAttestationObject(const QJsonObject& publicKey)
|
||
|
|
{
|
||
|
|
QByteArray result;
|
||
|
|
|
||
|
|
const auto rpIdHash = browserMessageBuilder()->getSha256Hash(publicKey["rpId"].toString());
|
||
|
|
result.append(rpIdHash);
|
||
|
|
|
||
|
|
const auto flags =
|
||
|
|
setFlagsFromJson(QJsonObject({{"ED", false},
|
||
|
|
{"AT", false},
|
||
|
|
{"BS", false},
|
||
|
|
{"BE", false},
|
||
|
|
{"UV", publicKey["userVerification"].toString() != REQUIREMENT_DISCOURAGED},
|
||
|
|
{"UP", true}}));
|
||
|
|
result.append(flags);
|
||
|
|
|
||
|
|
// Signature counter (not supported, always 0
|
||
|
|
const char counter[4] = {0x00, 0x00, 0x00, 0x00};
|
||
|
|
result.append(QByteArray::fromRawData(counter, 4));
|
||
|
|
|
||
|
|
return result;
|
||
|
|
}
|
||
|
|
|
||
|
|
// See: https://w3c.github.io/webauthn/#sctn-encoded-credPubKey-examples
|
||
|
|
PrivateKey
|
||
|
|
BrowserPasskeys::buildCredentialPrivateKey(int alg, const QString& predefinedFirst, const QString& predefinedSecond)
|
||
|
|
{
|
||
|
|
// Only support -7, P256 (EC), -8 (EdDSA) and -257 (RSA) for now
|
||
|
|
if (alg != WebAuthnAlgorithms::ES256 && alg != WebAuthnAlgorithms::RS256 && alg != WebAuthnAlgorithms::EDDSA) {
|
||
|
|
return {};
|
||
|
|
}
|
||
|
|
|
||
|
|
QByteArray firstPart;
|
||
|
|
QByteArray secondPart;
|
||
|
|
QByteArray pem;
|
||
|
|
|
||
|
|
if (!predefinedFirst.isEmpty() && !predefinedSecond.isEmpty()) {
|
||
|
|
firstPart = browserMessageBuilder()->getArrayFromBase64(predefinedFirst);
|
||
|
|
secondPart = browserMessageBuilder()->getArrayFromBase64(predefinedSecond);
|
||
|
|
} else {
|
||
|
|
if (alg == WebAuthnAlgorithms::ES256) {
|
||
|
|
try {
|
||
|
|
Botan::ECDSA_PrivateKey privateKey(*randomGen()->getRng(), Botan::EC_Group("secp256r1"));
|
||
|
|
const auto& publicPoint = privateKey.public_point();
|
||
|
|
auto x = publicPoint.get_affine_x();
|
||
|
|
auto y = publicPoint.get_affine_y();
|
||
|
|
firstPart = bigIntToQByteArray(x);
|
||
|
|
secondPart = bigIntToQByteArray(y);
|
||
|
|
|
||
|
|
auto privateKeyPem = Botan::PKCS8::PEM_encode(privateKey);
|
||
|
|
pem = QByteArray::fromStdString(privateKeyPem);
|
||
|
|
} catch (std::exception& e) {
|
||
|
|
qWarning("BrowserWebAuthn::buildCredentialPrivateKey: Could not create EC2 private key: %s", e.what());
|
||
|
|
return {};
|
||
|
|
}
|
||
|
|
} else if (alg == WebAuthnAlgorithms::RS256) {
|
||
|
|
try {
|
||
|
|
Botan::RSA_PrivateKey privateKey(*randomGen()->getRng(), RSA_BITS, RSA_EXPONENT);
|
||
|
|
auto modulus = privateKey.get_n();
|
||
|
|
auto exponent = privateKey.get_e();
|
||
|
|
firstPart = bigIntToQByteArray(modulus);
|
||
|
|
secondPart = bigIntToQByteArray(exponent);
|
||
|
|
|
||
|
|
auto privateKeyPem = Botan::PKCS8::PEM_encode(privateKey);
|
||
|
|
pem = QByteArray::fromStdString(privateKeyPem);
|
||
|
|
} catch (std::exception& e) {
|
||
|
|
qWarning("BrowserWebAuthn::buildCredentialPrivateKey: Could not create RSA private key: %s", e.what());
|
||
|
|
return {};
|
||
|
|
}
|
||
|
|
} else if (alg == WebAuthnAlgorithms::EDDSA) {
|
||
|
|
try {
|
||
|
|
Botan::Ed25519_PrivateKey key(*randomGen()->getRng());
|
||
|
|
auto publicKey = key.get_public_key();
|
||
|
|
auto privateKey = key.get_private_key();
|
||
|
|
firstPart = browserMessageBuilder()->getQByteArray(publicKey.data(), publicKey.size());
|
||
|
|
secondPart = browserMessageBuilder()->getQByteArray(privateKey.data(), privateKey.size());
|
||
|
|
|
||
|
|
auto privateKeyPem = Botan::PKCS8::PEM_encode(key);
|
||
|
|
pem = QByteArray::fromStdString(privateKeyPem);
|
||
|
|
} catch (std::exception& e) {
|
||
|
|
qWarning("BrowserWebAuthn::buildCredentialPrivateKey: Could not create EdDSA private key: %s",
|
||
|
|
e.what());
|
||
|
|
return {};
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
auto result = m_browserCbor.cborEncodePublicKey(alg, firstPart, secondPart);
|
||
|
|
return {result, pem};
|
||
|
|
}
|
||
|
|
|
||
|
|
QByteArray BrowserPasskeys::buildSignature(const QByteArray& authenticatorData,
|
||
|
|
const QByteArray& clientData,
|
||
|
|
const QString& privateKeyPem)
|
||
|
|
{
|
||
|
|
const auto clientDataHash = browserMessageBuilder()->getSha256Hash(clientData);
|
||
|
|
const auto attToBeSigned = authenticatorData + clientDataHash;
|
||
|
|
|
||
|
|
try {
|
||
|
|
const auto privateKeyArray = privateKeyPem.toUtf8();
|
||
|
|
Botan::DataSource_Memory dataSource(reinterpret_cast<const uint8_t*>(privateKeyArray.constData()),
|
||
|
|
privateKeyArray.size());
|
||
|
|
|
||
|
|
const auto key = Botan::PKCS8::load_key(dataSource).release();
|
||
|
|
const auto privateKeyBytes = key->private_key_bits();
|
||
|
|
const auto algName = key->algo_name();
|
||
|
|
const auto algId = key->algorithm_identifier();
|
||
|
|
|
||
|
|
std::vector<uint8_t> rawSignature;
|
||
|
|
if (algName == "ECDSA") {
|
||
|
|
Botan::ECDSA_PrivateKey privateKey(algId, privateKeyBytes);
|
||
|
|
#ifdef WITH_XC_BOTAN3
|
||
|
|
Botan::PK_Signer signer(
|
||
|
|
privateKey, *randomGen()->getRng(), "EMSA1(SHA-256)", Botan::Signature_Format::DerSequence);
|
||
|
|
#else
|
||
|
|
Botan::PK_Signer signer(privateKey, *randomGen()->getRng(), "EMSA1(SHA-256)", Botan::DER_SEQUENCE);
|
||
|
|
#endif
|
||
|
|
|
||
|
|
signer.update(reinterpret_cast<const uint8_t*>(attToBeSigned.constData()), attToBeSigned.size());
|
||
|
|
rawSignature = signer.signature(*randomGen()->getRng());
|
||
|
|
} else if (algName == "RSA") {
|
||
|
|
Botan::RSA_PrivateKey privateKey(algId, privateKeyBytes);
|
||
|
|
Botan::PK_Signer signer(privateKey, *randomGen()->getRng(), "EMSA3(SHA-256)");
|
||
|
|
|
||
|
|
signer.update(reinterpret_cast<const uint8_t*>(attToBeSigned.constData()), attToBeSigned.size());
|
||
|
|
rawSignature = signer.signature(*randomGen()->getRng());
|
||
|
|
} else if (algName == "Ed25519") {
|
||
|
|
Botan::Ed25519_PrivateKey privateKey(algId, privateKeyBytes);
|
||
|
|
Botan::PK_Signer signer(privateKey, *randomGen()->getRng(), "SHA-512");
|
||
|
|
|
||
|
|
signer.update(reinterpret_cast<const uint8_t*>(attToBeSigned.constData()), attToBeSigned.size());
|
||
|
|
rawSignature = signer.signature(*randomGen()->getRng());
|
||
|
|
} else {
|
||
|
|
qWarning("BrowserWebAuthn::buildSignature: Algorithm not supported");
|
||
|
|
return {};
|
||
|
|
}
|
||
|
|
|
||
|
|
auto signature = QByteArray(reinterpret_cast<char*>(rawSignature.data()), rawSignature.size());
|
||
|
|
return signature;
|
||
|
|
} catch (std::exception& e) {
|
||
|
|
qWarning("BrowserWebAuthn::buildSignature: Could not sign key: %s", e.what());
|
||
|
|
return {};
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
QByteArray BrowserPasskeys::buildExtensionData(QJsonObject& extensionObject) const
|
||
|
|
{
|
||
|
|
// Only supports "credProps" and "uvm" for now
|
||
|
|
const QStringList allowedKeys = {"credProps", "uvm"};
|
||
|
|
|
||
|
|
// Remove unsupported keys
|
||
|
|
for (const auto& key : extensionObject.keys()) {
|
||
|
|
if (!allowedKeys.contains(key)) {
|
||
|
|
extensionObject.remove(key);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
auto extensionData = m_browserCbor.cborEncodeExtensionData(extensionObject);
|
||
|
|
if (!extensionData.isEmpty()) {
|
||
|
|
return extensionData;
|
||
|
|
}
|
||
|
|
|
||
|
|
return {};
|
||
|
|
}
|
||
|
|
|
||
|
|
// Parse authentication data byte array to JSON
|
||
|
|
// See: https://www.w3.org/TR/webauthn/images/fido-attestation-structures.svg
|
||
|
|
// And: https://w3c.github.io/webauthn/#attested-credential-data
|
||
|
|
QJsonObject BrowserPasskeys::parseAuthData(const QByteArray& authData) const
|
||
|
|
{
|
||
|
|
auto rpIdHash = authData.mid(AuthDataOffsets::RPIDHASH, HASH_BYTES);
|
||
|
|
auto flags = authData.mid(AuthDataOffsets::FLAGS, 1);
|
||
|
|
auto counter = authData.mid(AuthDataOffsets::SIGNATURE_COUNTER, 4);
|
||
|
|
auto aaGuid = authData.mid(AuthDataOffsets::AAGUID, 16);
|
||
|
|
auto credentialLength = authData.mid(AuthDataOffsets::CREDENTIAL_LENGTH, 2);
|
||
|
|
auto credLen = qFromBigEndian<quint16>(credentialLength.data());
|
||
|
|
auto credentialId = authData.mid(AuthDataOffsets::CREDENTIAL_ID, credLen);
|
||
|
|
auto publicKey = authData.mid(AuthDataOffsets::CREDENTIAL_ID + credLen);
|
||
|
|
|
||
|
|
QJsonObject credentialDataJson({{"aaguid", browserMessageBuilder()->getBase64FromArray(aaGuid)},
|
||
|
|
{"credentialId", browserMessageBuilder()->getBase64FromArray(credentialId)},
|
||
|
|
{"publicKey", m_browserCbor.getJsonFromCborData(publicKey)}});
|
||
|
|
|
||
|
|
QJsonObject result({{"credentialData", credentialDataJson},
|
||
|
|
{"flags", parseFlags(flags)},
|
||
|
|
{"rpIdHash", browserMessageBuilder()->getBase64FromArray(rpIdHash)},
|
||
|
|
{"signatureCounter", QJsonValue(qFromBigEndian<int>(counter))}});
|
||
|
|
|
||
|
|
return result;
|
||
|
|
}
|
||
|
|
|
||
|
|
// See: https://w3c.github.io/webauthn/#table-authData
|
||
|
|
QJsonObject BrowserPasskeys::parseFlags(const QByteArray& flags) const
|
||
|
|
{
|
||
|
|
if (flags.isEmpty()) {
|
||
|
|
return {};
|
||
|
|
}
|
||
|
|
|
||
|
|
auto flagsByte = static_cast<uint8_t>(flags[0]);
|
||
|
|
std::bitset<8> flagBits(flagsByte);
|
||
|
|
|
||
|
|
return QJsonObject({{"ED", flagBits.test(AuthenticatorFlags::ED)},
|
||
|
|
{"AT", flagBits.test(AuthenticatorFlags::AT)},
|
||
|
|
{"BS", flagBits.test(AuthenticatorFlags::BS)},
|
||
|
|
{"BE", flagBits.test(AuthenticatorFlags::BE)},
|
||
|
|
{"UV", flagBits.test(AuthenticatorFlags::UV)},
|
||
|
|
{"UP", flagBits.test(AuthenticatorFlags::UP)}});
|
||
|
|
}
|
||
|
|
|
||
|
|
char BrowserPasskeys::setFlagsFromJson(const QJsonObject& flags) const
|
||
|
|
{
|
||
|
|
if (flags.isEmpty()) {
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
char flagBits = 0x00;
|
||
|
|
auto setFlag = [&](const char* key, unsigned char bit) {
|
||
|
|
if (flags[key].toBool()) {
|
||
|
|
flagBits |= 1 << bit;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
setFlag("ED", AuthenticatorFlags::ED);
|
||
|
|
setFlag("AT", AuthenticatorFlags::AT);
|
||
|
|
setFlag("BS", AuthenticatorFlags::BS);
|
||
|
|
setFlag("BE", AuthenticatorFlags::BE);
|
||
|
|
setFlag("UV", AuthenticatorFlags::UV);
|
||
|
|
setFlag("UP", AuthenticatorFlags::UP);
|
||
|
|
|
||
|
|
return flagBits;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Returns the first supported algorithm from the pubKeyCredParams list (only support ES256, RS256 and EdDSA for now)
|
||
|
|
WebAuthnAlgorithms BrowserPasskeys::getAlgorithmFromPublicKey(const QJsonObject& publicKey) const
|
||
|
|
{
|
||
|
|
const auto pubKeyCredParams = publicKey["pubKeyCredParams"].toArray();
|
||
|
|
if (!pubKeyCredParams.isEmpty()) {
|
||
|
|
const auto alg = pubKeyCredParams.first()["alg"].toInt();
|
||
|
|
if (alg == WebAuthnAlgorithms::ES256 || alg == WebAuthnAlgorithms::RS256 || alg == WebAuthnAlgorithms::EDDSA) {
|
||
|
|
return static_cast<WebAuthnAlgorithms>(alg);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return WebAuthnAlgorithms::ES256;
|
||
|
|
}
|
||
|
|
|
||
|
|
QByteArray BrowserPasskeys::bigIntToQByteArray(Botan::BigInt& bigInt) const
|
||
|
|
{
|
||
|
|
auto hexString = QString(bigInt.to_hex_string().c_str());
|
||
|
|
|
||
|
|
// Botan might add a leading "0x" to the hex string depending on the version. Remove it.
|
||
|
|
if (hexString.startsWith(("0x"))) {
|
||
|
|
hexString.remove(0, 2);
|
||
|
|
}
|
||
|
|
|
||
|
|
return browserMessageBuilder()->getArrayFromHexString(hexString);
|
||
|
|
}
|