/* * Copyright (C) 2023 KeePassXC Team * * 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 . */ #include "BrowserPasskeys.h" #include "BrowserMessageBuilder.h" #include "BrowserService.h" #include "crypto/Random.h" #include #include #include #include #include #include #include #include #include #include #include #include #include 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(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 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(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(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(attToBeSigned.constData()), attToBeSigned.size()); rawSignature = signer.signature(*randomGen()->getRng()); } else { qWarning("BrowserWebAuthn::buildSignature: Algorithm not supported"); return {}; } auto signature = QByteArray(reinterpret_cast(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(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(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(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(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); }