${CMAKE_CURRENT_SOURCE_DIR}/log/dlog_log_provider.cpp
${CMAKE_CURRENT_SOURCE_DIR}/log/abstract_log_provider.cpp
${CMAKE_CURRENT_SOURCE_DIR}/cbor_encoding.cpp
- ${CMAKE_CURRENT_SOURCE_DIR}/request.cpp
${CMAKE_CURRENT_SOURCE_DIR}/request_handler.cpp
- ${CMAKE_CURRENT_SOURCE_DIR}/request_ga.cpp
- ${CMAKE_CURRENT_SOURCE_DIR}/request_mc.cpp
${CMAKE_CURRENT_SOURCE_DIR}/bluetooth_advert.cpp
${CMAKE_CURRENT_SOURCE_DIR}/derive_key.cpp
${CMAKE_CURRENT_SOURCE_DIR}/tunnel_server_domain.cpp
${CMAKE_CURRENT_SOURCE_DIR}/crypto/ec_key.cpp
${CMAKE_CURRENT_SOURCE_DIR}/crypto/ecdh.cpp
${CMAKE_CURRENT_SOURCE_DIR}/crypto/noise/noise.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/qr_transaction.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/state_assisted_transaction.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/qr_code_shower.cpp
)
SET(WEBAUTHN_BLE_SOURCES ${WEBAUTHN_BLE_SOURCES} PARENT_SCOPE)
#pragma once
#include <array>
+#include <utility> // for std::move
typedef std::array<char, 2> Hint;
--- /dev/null
+/*
+ * Copyright (c) 2024 Samsung Electronics Co., Ltd. All rights reserved
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+#pragma once
+
+#include <cstdint>
+#include <string>
+
+template <class Bytes>
+std::string LowercaseHexStringOf(const Bytes &bytes)
+{
+ static_assert(sizeof(typename Bytes::value_type) == 1);
+ std::string res;
+ for (uint8_t byte : bytes) {
+ constexpr char digits[] = "0123456789abcdef";
+ res += digits[byte >> 4];
+ res += digits[byte & 15];
+ }
+ return res;
+}
--- /dev/null
+/*
+ * Copyright (c) 2024 Samsung Electronics Co., Ltd. All rights reserved
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+#include "cbor_encoding.h"
+#include "qr_code_shower.h"
+
+void QrCodeShower::ShowQrCode(const CryptoBuffer &qrSecret,
+ const CryptoBuffer &identityKeyCompressed,
+ const Hint &hint,
+ bool stateAssisted,
+ wauthn_cb_display_qrcode displayQrCodeCallback,
+ void *displayQrCodeCallbackUserData)
+{
+ std::string fidoUri;
+ CborEncoding::Cbor cbor;
+ cbor.EncodeQRContents(identityKeyCompressed, qrSecret, hint, stateAssisted, fidoUri);
+ displayQrCodeCallback(fidoUri.c_str(), displayQrCodeCallbackUserData);
+}
--- /dev/null
+/*
+ * Copyright (c) 2024 Samsung Electronics Co., Ltd. All rights reserved
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+#pragma once
+
+#include "common.h"
+#include "crypto/common.h"
+
+#include <webauthn-hal.h>
+
+class IQrCodeShower {
+public:
+ IQrCodeShower() = default;
+ IQrCodeShower(const IQrCodeShower &) = delete;
+ IQrCodeShower(IQrCodeShower &&) = delete;
+ IQrCodeShower &operator=(const IQrCodeShower &) = delete;
+ IQrCodeShower &operator=(IQrCodeShower &&) = delete;
+ virtual ~IQrCodeShower() = default;
+
+ // Throws on error.
+ virtual void ShowQrCode(const CryptoBuffer &qrSecret,
+ const CryptoBuffer &identityKeyCompressed,
+ const Hint &hint,
+ bool stateAssisted,
+ wauthn_cb_display_qrcode displayQrCodeCallback,
+ void *displayQrCodeCallbackUserData) = 0;
+};
+
+class QrCodeShower : public IQrCodeShower {
+public:
+ void ShowQrCode(const CryptoBuffer &qrSecret,
+ const CryptoBuffer &identityKeyCompressed,
+ const Hint &hint,
+ bool stateAssisted,
+ wauthn_cb_display_qrcode displayQrCodeCallback,
+ void *displayQrCodeCallbackUserData) override;
+};
--- /dev/null
+/*
+ * Copyright (c) 2024 Samsung Electronics Co., Ltd. All rights reserved
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+#include "crypto/ec_key.h"
+#include "crypto/random.h"
+#include "derive_key.h"
+#include "exception.h"
+#include "log/log.h"
+#include "lowercase_hex_string_of.h"
+#include "qr_transaction.h"
+#include "tunnel_server_domain.h"
+
+#include <utility>
+
+QrTransaction::QrTransaction(wauthn_cb_display_qrcode displayQrCodeCallback,
+ void *displayQrCodeCallbackUserData,
+ Hint hint,
+ std::unique_ptr<IQrCodeShower> qrCodeShower,
+ IBtAdvertScannerUPtr btAdvertScanner)
+: m_displayQrCodeCallback{displayQrCodeCallback},
+ m_displayQrCodeCallbackUserData{displayQrCodeCallbackUserData},
+ m_hint{std::move(hint)},
+ m_qrCodeShower{std::move(qrCodeShower)},
+ m_btAdvertScanner{std::move(btAdvertScanner)}
+{
+}
+
+void QrTransaction::PerformTransaction()
+{
+ auto updateStateAndCheckForCancel = [&](State state) {
+ auto guard = std::lock_guard{m_lock};
+ if (m_state == State::CANCELLED) {
+ throw Cancelled{};
+ }
+ m_state = state;
+ };
+
+ updateStateAndCheckForCancel(State::SHOWING_QR_CODE);
+
+ CryptoBuffer qrSecret = Crypto::RandomBytes(16);
+ Crypto::X9_62_P_256_Key identityKey = Crypto::X9_62_P_256_Key::Create();
+ m_qrCodeShower->ShowQrCode(qrSecret,
+ identityKey.ExportPublicKey(true),
+ m_hint,
+ false,
+ m_displayQrCodeCallback,
+ m_displayQrCodeCallbackUserData);
+
+ updateStateAndCheckForCancel(State::AWAITING_BLE_ADVERT);
+
+ CryptoBuffer decryptedAdvert;
+ auto eidKey = DeriveKey(qrSecret, {}, KeyPurpose::EIDKey, 64);
+ int err = m_btAdvertScanner->AwaitAdvert(eidKey, decryptedAdvert); // may throw
+ if (err == BT_ERROR_CANCELLED)
+ throw Cancelled{};
+ if (err != BT_ERROR_NONE) {
+ LogError("Awating BLE advert failed with code = " << err);
+ throw Unknown{};
+ }
+
+ auto unpackedAdvert = UnpackDecryptedAdvert(decryptedAdvert);
+ LogDebug("unpacked BLE advert: nonce = "
+ << LowercaseHexStringOf(unpackedAdvert.nonce)
+ << ", routingId = " << LowercaseHexStringOf(unpackedAdvert.routingId)
+ << ", encodedTunnelServerDomain = " << unpackedAdvert.encodedTunnelServerDomain);
+
+ auto tunnelServerDomain = DecodeTunnelServerDomain(unpackedAdvert.encodedTunnelServerDomain);
+ LogDebug("decoded tunnel server domain: " << tunnelServerDomain);
+
+ updateStateAndCheckForCancel(State::NOT_IN_PROGRESS);
+}
+
+void QrTransaction::Cancel()
+{
+ auto guard = std::lock_guard{m_lock};
+ switch (m_state) {
+ case State::CANCELLED: break;
+ case State::NOT_IN_PROGRESS: break;
+ case State::SHOWING_QR_CODE: break; // Cannot cancel that :(
+ case State::AWAITING_BLE_ADVERT: {
+ auto err = m_btAdvertScanner->Cancel();
+ if (err != BT_ERROR_NONE)
+ throw Unknown{};
+ } break;
+ }
+ m_state = State::CANCELLED;
+}
--- /dev/null
+/*
+ * Copyright (c) 2024 Samsung Electronics Co., Ltd. All rights reserved
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+#pragma once
+
+#include "bluetooth_advert.h"
+#include "qr_code_shower.h"
+#include "transaction.h"
+
+class QrTransaction : public ITransaction {
+public:
+ QrTransaction(wauthn_cb_display_qrcode displayQrCodeCallback,
+ void *displayQrCodeCallbackUserData,
+ Hint hint,
+ std::unique_ptr<IQrCodeShower> qrCodeShower = std::make_unique<QrCodeShower>(),
+ IBtAdvertScannerUPtr btAdvertScanner = std::make_unique<BtAdvertScanner>());
+
+ void PerformTransaction() override;
+
+ void Cancel() override;
+
+private:
+ wauthn_cb_display_qrcode m_displayQrCodeCallback;
+ void *m_displayQrCodeCallbackUserData;
+ Hint m_hint;
+ std::unique_ptr<IQrCodeShower> m_qrCodeShower;
+ IBtAdvertScannerUPtr m_btAdvertScanner;
+
+ enum class State {
+ CANCELLED,
+ NOT_IN_PROGRESS,
+ SHOWING_QR_CODE,
+ AWAITING_BLE_ADVERT,
+ };
+
+ std::mutex m_lock;
+ State m_state = State::NOT_IN_PROGRESS;
+};
+++ /dev/null
-/*
- * Copyright (c) 2024 Samsung Electronics Co., Ltd All Rights Reserved
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-#include "cbor_encoding.h"
-#include "crypto/ec_key.h"
-#include "crypto/random.h"
-#include "derive_key.h"
-#include "log/log.h"
-#include "request.h"
-#include "tunnel_server_domain.h"
-
-wauthn_error_e Request::Process()
-{
- if (QRInitiated()) {
- auto qrSecret = Crypto::RandomBytes(16);
- auto identityKey = Crypto::X9_62_P_256_Key::Create();
- auto identityKeyCompressed = identityKey.ExportPublicKey(true);
-
- ShowQRCode(qrSecret, identityKeyCompressed);
-
- auto eidKey = DeriveKey(qrSecret, {}, KeyPurpose::EIDKey, 64);
-
- CryptoBuffer decryptedAdvert;
- int err = m_btAdvertScanner->AwaitAdvert(eidKey,
- decryptedAdvert); // may throw
- if (err == BT_ERROR_CANCELLED)
- return WAUTHN_ERROR_CANCELLED;
- if (err)
- return WAUTHN_ERROR_UNKNOWN;
-
- auto unpackedAdvert = UnpackDecryptedAdvert(decryptedAdvert);
- auto hexdump = [](const auto &bytes) {
- std::string res;
- for (uint8_t byte : bytes) {
- constexpr char digits[] = "0123456789abcdef";
- res += digits[byte >> 4];
- res += digits[byte & 15];
- }
- return res;
- };
- LogDebug("unpacked BLE advert: nonce = "
- << hexdump(unpackedAdvert.nonce)
- << ", routingId = " << hexdump(unpackedAdvert.routingId)
- << ", encodedTunnelServerDomain = " << unpackedAdvert.encodedTunnelServerDomain);
-
- auto tunnelServerDomain =
- DecodeTunnelServerDomain(unpackedAdvert.encodedTunnelServerDomain);
- LogDebug("decoded tunnel server domain: " << tunnelServerDomain);
-
- // TODO
- } else {
- // TODO
- }
- // TODO
- return WAUTHN_ERROR_NONE;
-}
-
-wauthn_error_e Request::Cancel()
-{
- int err = m_btAdvertScanner->Cancel(); // may throw
- if (err == BT_ERROR_NOT_IN_PROGRESS)
- return WAUTHN_ERROR_NOT_ALLOWED;
- if (err)
- return WAUTHN_ERROR_UNKNOWN;
-
- // TODO
-
- return WAUTHN_ERROR_NONE;
-}
-
-void Request::ShowQRCode(const CryptoBuffer &qrSecret, const CryptoBuffer &identityKeyCompressed)
-{
-
- std::string fidoUri;
- CborEncoding::Cbor cbor;
- cbor.EncodeQRContents(identityKeyCompressed, qrSecret, GetHint(), StateAssisted(), fidoUri);
-
- QRCallback(fidoUri);
-}
+++ /dev/null
-/*
- * Copyright (c) 2023 Samsung Electronics Co., Ltd All Rights Reserved
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-#pragma once
-
-#include "bluetooth_advert.h"
-#include "common.h"
-#include "crypto/common.h"
-
-#include <webauthn-hal.h>
-
-class Request {
-public:
- Request(const Request &) = delete;
- virtual ~Request() = default;
-
- wauthn_error_e Process();
- wauthn_error_e Cancel();
-
-protected:
- Request(const wauthn_client_data_s *clientData,
- IBtAdvertScannerUPtr btAdvertScanner = std::make_unique<BtAdvertScanner>())
- : m_clientData(clientData), m_btAdvertScanner(std::move(btAdvertScanner))
- {
- }
-
- virtual const wauthn_hybrid_linked_data_s *LinkData() const = 0;
- virtual void QRCallback(const std::string &fidoUri) const = 0;
-
- bool QRInitiated() const { return LinkData() == nullptr; }
-
- bool StateAssisted() const { return LinkData() != nullptr; }
-
- void ShowQRCode(const CryptoBuffer &qrSecret, const CryptoBuffer &identityKeyCompressed);
- virtual Hint GetHint() const = 0;
-
-private:
- const wauthn_client_data_s *m_clientData;
- IBtAdvertScannerUPtr m_btAdvertScanner;
-};
+++ /dev/null
-/*
- * Copyright (c) 2023 Samsung Electronics Co., Ltd All Rights Reserved
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-#include "log/log.h"
-#include "request_ga.h"
-
-const wauthn_hybrid_linked_data_s *RequestGetAssertion::LinkData() const
-{
- return m_options->linked_device;
-}
-
-void RequestGetAssertion::QRCallback(const std::string &fidoUri) const
-{
- if (!m_callbacks || !m_callbacks->qrcode_callback) {
- LogError("Missing QR callback");
- return;
- }
-
- m_callbacks->qrcode_callback(fidoUri.c_str(), m_callbacks->user_data);
-}
+++ /dev/null
-/*
- * Copyright (c) 2023 Samsung Electronics Co., Ltd All Rights Reserved
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-#pragma once
-
-#include "request.h"
-
-class RequestGetAssertion : public Request {
-public:
- typedef wauthn_pubkey_cred_request_options_s Options;
- typedef wauthn_ga_callbacks_s Callbacks;
-
- RequestGetAssertion(const wauthn_client_data_s *clientData,
- const Options *options,
- Callbacks *callbacks,
- IBtAdvertScannerUPtr btAdvertScanner = std::make_unique<BtAdvertScanner>())
- : Request(clientData, std::move(btAdvertScanner)), m_options(options), m_callbacks(callbacks)
- {
- }
-
-protected:
- const wauthn_hybrid_linked_data_s *LinkData() const override;
- void QRCallback(const std::string &fidoUri) const override;
-
- Hint GetHint() const override { return {'g', 'a'}; }
-
-private:
- const Options *m_options;
- Callbacks *m_callbacks;
-};
{
try {
std::lock_guard<std::mutex> lock(m_mutex);
- if (!m_currentRequest) {
+ if (!m_currentTransaction) {
LogError("No request is being processed");
return WAUTHN_ERROR_NOT_ALLOWED;
}
- return m_currentRequest->Cancel();
+ m_currentTransaction->Cancel();
+ return WAUTHN_ERROR_NONE;
} catch (const ExceptionBase &ex) {
return ex.Code();
} catch (const abi::__forced_unwind &) {
#pragma once
+#include "bluetooth_advert.h"
#include "common.h"
#include "exception.h"
#include "log/log.h"
-#include "request.h"
+#include "qr_code_shower.h"
+#include "qr_transaction.h"
+#include "state_assisted_transaction.h"
#include <cxxabi.h>
#include <memory>
#include <mutex>
#include <webauthn-hal.h>
-class Request;
+struct RequestMC {
+ const wauthn_pubkey_cred_creation_options_s *options;
+ wauthn_mc_callbacks_s *callbacks;
+};
+
+struct RequestGA {
+ const wauthn_pubkey_cred_request_options_s *options;
+ wauthn_ga_callbacks_s *callbacks;
+};
+
+[[nodiscard]] constexpr Hint ToHint(const RequestMC &) noexcept { return {'m', 'c'}; }
+
+[[nodiscard]] constexpr Hint ToHint(const RequestGA &) noexcept { return {'g', 'a'}; }
+
+enum class RequestKind {
+ MC,
+ GA,
+};
+
+[[nodiscard]] constexpr RequestKind ToKind(const RequestMC &) noexcept { return RequestKind::MC; }
+
+[[nodiscard]] constexpr RequestKind ToKind(const RequestGA &) noexcept { return RequestKind::GA; }
class RequestHandler {
public:
wauthn_error_e Cancel();
- template <typename T>
- void Process(const wauthn_client_data_s *client_data,
- const typename T::Options *options,
- typename T::Callbacks *callbacks,
- IBtAdvertScannerUPtr btAdvertScanner = std::make_unique<BtAdvertScanner>())
+ template <class Request>
+ void Process(
+ const wauthn_client_data_s *client_data,
+ Request request,
+ // If null, default implementations will be set in the function body to control excepitions.
+ std::unique_ptr<IQrCodeShower> qrCodeShower = nullptr,
+ IBtAdvertScannerUPtr btAdvertScanner = nullptr)
{
wauthn_error_e result;
try {
- if (!callbacks || !callbacks->response_callback)
+ if (!request.callbacks || !request.callbacks->response_callback)
LogError("Missing response callback");
- if (client_data == nullptr || options == nullptr)
- throw InvalidParam();
+ if (client_data == nullptr || request.options == nullptr)
+ throw InvalidParam{};
+
+ if (!qrCodeShower)
+ qrCodeShower = std::make_unique<QrCodeShower>();
+ if (!btAdvertScanner)
+ btAdvertScanner = std::make_unique<BtAdvertScanner>();
{
std::lock_guard<std::mutex> lock(m_mutex);
- if (m_currentRequest != nullptr) {
+ if (m_currentTransaction != nullptr) {
LogError("New request received while processing another one");
- throw NotAllowed(); // Server should not allow it
+ throw NotAllowed{}; // Server should not allow it
+ }
+ if (request.options->linked_device)
+ m_currentTransaction = std::make_unique<StateAssistedTransaction>();
+ else {
+ if (!request.callbacks || !request.callbacks->qrcode_callback) {
+ LogError("Missing QR callback");
+ throw InvalidParam{};
+ }
+ m_currentTransaction =
+ std::make_unique<QrTransaction>(request.callbacks->qrcode_callback,
+ request.callbacks->user_data,
+ ToHint(request),
+ std::move(qrCodeShower),
+ std::move(btAdvertScanner));
}
- m_currentRequest = std::make_unique<T>(
- client_data, options, callbacks, std::move(btAdvertScanner));
}
auto cleanup = OnScopeExit([&] {
try {
std::lock_guard<std::mutex> lock(m_mutex);
- m_currentRequest.reset();
+ m_currentTransaction = nullptr;
+ } catch (const abi::__forced_unwind &) {
+ throw;
} catch (...) {
result = WAUTHN_ERROR_UNKNOWN;
}
});
- result = m_currentRequest->Process();
+ m_currentTransaction->PerformTransaction();
+ result = WAUTHN_ERROR_NONE;
} catch (const ExceptionBase &ex) {
result = ex.Code();
+ } catch (const std::bad_alloc &) {
+ result = WAUTHN_ERROR_MEMORY;
} catch (const abi::__forced_unwind &) {
throw;
} catch (...) {
result = WAUTHN_ERROR_UNKNOWN;
}
- if (callbacks && callbacks->response_callback) {
+ if (request.callbacks && request.callbacks->response_callback) {
try {
- callbacks->response_callback(nullptr, result, callbacks->user_data);
+ request.callbacks->response_callback(nullptr, result, request.callbacks->user_data);
} catch (const abi::__forced_unwind &) {
throw;
} catch (...) {
RequestHandler() noexcept = default;
std::mutex m_mutex;
- std::unique_ptr<Request> m_currentRequest;
+ std::unique_ptr<ITransaction> m_currentTransaction;
};
+++ /dev/null
-/*
- * Copyright (c) 2023 Samsung Electronics Co., Ltd All Rights Reserved
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-#include "log/log.h"
-#include "request_mc.h"
-
-const wauthn_hybrid_linked_data_s *RequestMakeCredential::LinkData() const
-{
- return m_options->linked_device;
-}
-
-void RequestMakeCredential::QRCallback(const std::string &fidoUri) const
-{
- if (!m_callbacks || !m_callbacks->qrcode_callback) {
- LogError("Missing QR callback");
- return;
- }
-
- m_callbacks->qrcode_callback(fidoUri.c_str(), m_callbacks->user_data);
-}
+++ /dev/null
-/*
- * Copyright (c) 2023 Samsung Electronics Co., Ltd All Rights Reserved
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-#pragma once
-
-#include "request.h"
-
-class RequestMakeCredential : public Request {
-public:
- typedef wauthn_pubkey_cred_creation_options_s Options;
- typedef wauthn_mc_callbacks_s Callbacks;
-
- RequestMakeCredential(
- const wauthn_client_data_s *clientData,
- const Options *options,
- Callbacks *callbacks,
- IBtAdvertScannerUPtr btAdvertScanner = std::make_unique<BtAdvertScanner>())
- : Request(clientData, std::move(btAdvertScanner)), m_options(options), m_callbacks(callbacks)
- {
- }
-
-protected:
- const wauthn_hybrid_linked_data_s *LinkData() const override;
- void QRCallback(const std::string &fidoUri) const override;
-
- Hint GetHint() const override { return {'m', 'c'}; }
-
-private:
- const Options *m_options;
- Callbacks *m_callbacks;
-};
--- /dev/null
+/*
+ * Copyright (c) 2024 Samsung Electronics Co., Ltd. All rights reserved
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+#include "exception.h"
+#include "state_assisted_transaction.h"
+
+StateAssistedTransaction::StateAssistedTransaction() {}
+
+void StateAssistedTransaction::PerformTransaction()
+{
+ auto guard = std::unique_lock{m_lock};
+ if (m_state == State::CANCELLED)
+ throw Cancelled{};
+
+ auto unlockedTransaction = [&](State state, auto &&callback) {
+ m_state = state;
+ guard.unlock();
+ callback();
+ guard.lock();
+ if (m_state == State::CANCELLED)
+ throw Cancelled{};
+ };
+
+ // TODO
+ (void)unlockedTransaction;
+
+ m_state = State::NOT_IN_PROGRESS;
+}
+
+void StateAssistedTransaction::Cancel()
+{
+ auto guard = std::lock_guard{m_lock};
+ switch (m_state) {
+ case State::CANCELLED: break;
+ case State::NOT_IN_PROGRESS: break;
+ }
+ m_state = State::CANCELLED;
+}
--- /dev/null
+/*
+ * Copyright (c) 2024 Samsung Electronics Co., Ltd. All rights reserved
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+#pragma once
+
+#include "transaction.h"
+
+#include <mutex>
+
+class StateAssistedTransaction : public ITransaction {
+public:
+ StateAssistedTransaction();
+
+ void PerformTransaction() override;
+
+ void Cancel() override;
+
+private:
+ enum class State {
+ CANCELLED,
+ NOT_IN_PROGRESS,
+ };
+
+ std::mutex m_lock;
+ State m_state = State::NOT_IN_PROGRESS;
+};
--- /dev/null
+/*
+ * Copyright (c) 2024 Samsung Electronics Co., Ltd. All rights reserved
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+#pragma once
+
+class ITransaction {
+public:
+ ITransaction() = default;
+ ITransaction(const ITransaction &) = delete;
+ ITransaction(ITransaction &&) = delete;
+ ITransaction &operator=(const ITransaction &) = delete;
+ ITransaction &operator=(ITransaction &&) = delete;
+
+ virtual ~ITransaction() = default;
+
+ // Throws Cancelled on cancel.
+ virtual void PerformTransaction() = 0;
+
+ // May be called from other threads.
+ virtual void Cancel() = 0;
+};
* limitations under the License
*/
-#include "request_ga.h"
#include "request_handler.h"
-#include "request_mc.h"
#include <webauthn-hal.h>
const wauthn_pubkey_cred_creation_options_s *options,
wauthn_mc_callbacks_s *callbacks)
{
- RequestHandler::Instance().Process<RequestMakeCredential>(client_data, options, callbacks);
+ RequestHandler::Instance().Process(client_data, RequestMC{options, callbacks});
}
WEBAUTHN_HAL_API
const wauthn_pubkey_cred_request_options_s *options,
wauthn_ga_callbacks_s *callbacks)
{
- RequestHandler::Instance().Process<RequestGetAssertion>(client_data, options, callbacks);
+ RequestHandler::Instance().Process(client_data, RequestGA{options, callbacks});
}
WEBAUTHN_HAL_API
${CMAKE_CURRENT_SOURCE_DIR}/turn_bluetooth.cpp
${CMAKE_CURRENT_SOURCE_DIR}/derive_key_tests.cpp
${CMAKE_CURRENT_SOURCE_DIR}/tunnel_server_domain_tests.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/lowercase_hex_string_of_tests.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/qr_transaction_tests.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/state_assisted_transaction_tests.cpp
${CMAKE_CURRENT_SOURCE_DIR}/crypto/hkdf_unittest.cpp
${CMAKE_CURRENT_SOURCE_DIR}/crypto/hmac_unittest.cpp
${CMAKE_CURRENT_SOURCE_DIR}/crypto/sha2_unittest.cpp
*/
#include "bluetooth_advert.h"
+#include "crypto/ec_key.h"
+#include "crypto/random.h"
#include "exception.h"
-#include "request_ga.h"
+#include "qr_code_shower.h"
#include "request_handler.h"
-#include "request_mc.h"
#include <cstring>
#include <functional>
#include <gtest/gtest.h>
+#include <stdexcept>
#include <utility>
#include <webauthn-hal.h>
m_argsCheckOk = m_argsChecker(args...);
}
- void CalledOnce()
+ void CalledOnce(int line)
{
- EXPECT_EQ(m_calls, 1);
+ EXPECT_EQ(m_calls, 1) << "line: " << line;
if (m_argsChecker) {
- EXPECT_TRUE(m_argsCheckOk);
+ EXPECT_TRUE(m_argsCheckOk) << "line: " << line;
}
Reset();
}
- void NotCalled()
+ void NotCalled(int line)
{
- EXPECT_EQ(m_calls, 0);
+ EXPECT_EQ(m_calls, 0) << "line: " << line;
Reset();
}
gaCallback.Call(pubkey_cred, result, user_data);
}
-class BASMock : public IBtAdvertScanner {
+class MBtAdvertScanner : public IBtAdvertScanner {
public:
- [[nodiscard]] int AwaitAdvert(const CryptoBuffer & /*eidKey*/,
- CryptoBuffer &decryptedAdvert) noexcept override
+ int AwaitAdvert(const CryptoBuffer & /*eidKey*/, CryptoBuffer & /*decryptedAdvert*/) override
{
- decryptedAdvert.assign(16, 0);
- return 0;
+ throw std::runtime_error{"should not be called"};
}
- int Cancel() noexcept override { return 0; }
+ int Cancel() override { throw std::runtime_error{"should not be called"}; }
};
+void mocked_wah_make_credential(const wauthn_client_data_s *client_data,
+ const wauthn_pubkey_cred_creation_options_s *options,
+ wauthn_mc_callbacks_s *callbacks)
+{
+ RequestHandler::Instance().Process(client_data,
+ RequestMC{options, callbacks},
+ std::make_unique<QrCodeShower>(),
+ std::make_unique<MBtAdvertScanner>());
+}
+
+void mocked_wah_get_assertion(const wauthn_client_data_s *client_data,
+ const wauthn_pubkey_cred_request_options_s *options,
+ wauthn_ga_callbacks_s *callbacks)
+{
+ RequestHandler::Instance().Process(client_data,
+ RequestGA{options, callbacks},
+ std::make_unique<QrCodeShower>(),
+ std::make_unique<MBtAdvertScanner>());
+}
+
void InvokeMc(const wauthn_client_data_s *clientData,
const wauthn_pubkey_cred_creation_options_s *options,
wauthn_cb_display_qrcode qrcodeCallback,
mcCb.response_callback = responseCallback;
mcCb.user_data = userData;
- RequestHandler::Instance().Process<RequestMakeCredential>(
- clientData, options, &mcCb, std::make_unique<BASMock>());
+ mocked_wah_make_credential(clientData, options, &mcCb);
}
void InvokeGa(const wauthn_client_data_s *clientData,
gaCb.response_callback = responseCallback;
gaCb.user_data = userData;
- RequestHandler::Instance().Process<RequestGetAssertion>(
- clientData, options, &gaCb, std::make_unique<BASMock>());
+ mocked_wah_get_assertion(clientData, options, &gaCb);
}
template <typename T>
auto mcInvalid = invalidParamChecker<wauthn_pubkey_credential_attestaion_s>(&userData);
auto gaInvalid = invalidParamChecker<wauthn_pubkey_credential_assertion_s>(&userData);
- RequestHandler::Instance().Process<RequestMakeCredential>(
- &client, &mcOptions, nullptr, std::make_unique<BASMock>());
+ mocked_wah_make_credential(&client, &mcOptions, nullptr);
InvokeMc(&client, &mcOptions, nullptr, nullptr, nullptr);
- qrCallback.NotCalled();
- mcCallback.NotCalled();
- gaCallback.NotCalled();
+ qrCallback.NotCalled(__LINE__);
+ mcCallback.NotCalled(__LINE__);
+ gaCallback.NotCalled(__LINE__);
mcCallback.SetArgsChecker(mcInvalid);
InvokeMc(nullptr, &mcOptions, QrCallback, McCallback, &userData);
- qrCallback.NotCalled();
- mcCallback.CalledOnce();
- gaCallback.NotCalled();
+ qrCallback.NotCalled(__LINE__);
+ mcCallback.CalledOnce(__LINE__);
+ gaCallback.NotCalled(__LINE__);
InvokeMc(&client, nullptr, QrCallback, McCallback, &userData);
- qrCallback.NotCalled();
- mcCallback.CalledOnce();
- gaCallback.NotCalled();
+ qrCallback.NotCalled(__LINE__);
+ mcCallback.CalledOnce(__LINE__);
+ gaCallback.NotCalled(__LINE__);
- RequestHandler::Instance().Process<RequestGetAssertion>(
- &client, &gaOptions, nullptr, std::make_unique<BASMock>());
+ mocked_wah_get_assertion(&client, &gaOptions, nullptr);
InvokeGa(&client, &gaOptions, nullptr, nullptr, nullptr);
- qrCallback.NotCalled();
- mcCallback.NotCalled();
- gaCallback.NotCalled();
+ qrCallback.NotCalled(__LINE__);
+ mcCallback.NotCalled(__LINE__);
+ gaCallback.NotCalled(__LINE__);
gaCallback.SetArgsChecker(gaInvalid);
InvokeGa(nullptr, &gaOptions, QrCallback, GaCallback, &userData);
- qrCallback.NotCalled();
- mcCallback.NotCalled();
- gaCallback.CalledOnce();
+ qrCallback.NotCalled(__LINE__);
+ mcCallback.NotCalled(__LINE__);
+ gaCallback.CalledOnce(__LINE__);
InvokeGa(&client, nullptr, QrCallback, GaCallback, &userData);
- qrCallback.NotCalled();
- mcCallback.NotCalled();
- gaCallback.CalledOnce();
+ qrCallback.NotCalled(__LINE__);
+ mcCallback.NotCalled(__LINE__);
+ gaCallback.CalledOnce(__LINE__);
EXPECT_EQ(wah_cancel(), WAUTHN_ERROR_NOT_ALLOWED);
}
// Use throwing qr callback to stop further processing
InvokeMc(&client, &mcOptions, QrThrowingCallback, McCallback, &userData);
- qrCallback.CalledOnce();
- mcCallback.CalledOnce();
- gaCallback.NotCalled();
+ qrCallback.CalledOnce(__LINE__);
+ mcCallback.CalledOnce(__LINE__);
+ gaCallback.NotCalled(__LINE__);
InvokeGa(&client, &gaOptions, QrThrowingCallback, GaCallback, &userData);
- qrCallback.CalledOnce();
- mcCallback.NotCalled();
- gaCallback.CalledOnce();
+ qrCallback.CalledOnce(__LINE__);
+ mcCallback.NotCalled(__LINE__);
+ gaCallback.CalledOnce(__LINE__);
}
--- /dev/null
+/*
+ * Copyright (c) 2024 Samsung Electronics Co., Ltd. All rights reserved
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+#pragma once
+
+#include <cassert>
+#include <random>
+#include <type_traits>
+
+template <class A, class B>
+inline auto get_random(A min, B max)
+{
+ static std::random_device rd;
+ assert(min <= max);
+ return std::uniform_int_distribution<std::common_type_t<A, B>>(min, max)(rd);
+}
--- /dev/null
+/*
+ * Copyright (c) 2024 Samsung Electronics Co., Ltd. All rights reserved
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+#include "lowercase_hex_string_of.h"
+
+#include <gtest/gtest.h>
+#include <vector>
+
+TEST(LowercaseHexStringOf, simple)
+{
+ EXPECT_EQ(LowercaseHexStringOf(std::string{}), "");
+ EXPECT_EQ(LowercaseHexStringOf(std::string{"abc"}), "616263");
+ EXPECT_EQ(
+ LowercaseHexStringOf(std::vector<uint8_t>{0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef}),
+ "0123456789abcdef");
+}
--- /dev/null
+/*
+ * Copyright (c) 2024 Samsung Electronics Co., Ltd. All rights reserved
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+#include "get_random.h"
+#include "qr_transaction.h"
+#include "test_cancel_from_the_other_thread.h"
+#include "transaction_tester.h"
+
+namespace {
+
+enum class Event : int {
+ RESET = 0,
+ QR_CODE_STARTED,
+ QR_CODE_ENDED,
+ BLE_ADVERT_STARTED,
+ BLE_ADVERT_ENDED,
+ FINISHED,
+};
+
+enum class CancelCalledOn : int {
+ NONE,
+ BLE_ADVERT,
+};
+
+using TestState = TransactionTestState<Event, CancelCalledOn>;
+using Tester = TransactionTester<Event, CancelCalledOn>;
+
+class MQrCodeShower : public IQrCodeShower, public Tester {
+public:
+ explicit MQrCodeShower(TestState &testState) : Tester{testState} {}
+
+ void ShowQrCode(const CryptoBuffer & /*qrSecret*/,
+ const CryptoBuffer & /*identityKeyCompressed*/,
+ const Hint & /*hint*/,
+ bool /*stateAssisted*/,
+ wauthn_cb_display_qrcode /*displayQrCodeCallback*/,
+ void * /*displayQrCodeCallbackUserData*/) override
+ {
+ SCOPED_TRACE("");
+ UpdateAndCheckState(Event::QR_CODE_STARTED, Event::QR_CODE_ENDED, false);
+ }
+};
+
+class MBtAdvertScanner : public IBtAdvertScanner, public Tester {
+public:
+ explicit MBtAdvertScanner(TestState &testState) : Tester{testState} {}
+
+ int AwaitAdvert(const CryptoBuffer & /*eidKey*/, CryptoBuffer &decryptedAdvert) override
+ {
+ SCOPED_TRACE("");
+ UpdateAndCheckState(Event::BLE_ADVERT_STARTED, Event::BLE_ADVERT_ENDED, true);
+ decryptedAdvert.assign(16, 0);
+ return BT_ERROR_NONE;
+ }
+
+ int Cancel() override
+ {
+ SCOPED_TRACE("");
+ CheckCancel(CancelCalledOn::BLE_ADVERT);
+ return BT_ERROR_NONE;
+ }
+};
+
+} // namespace
+
+TEST(QrTransaction, Cancel)
+{
+ TestState testState;
+ auto makeTransaction = [&] {
+ return QrTransaction(nullptr,
+ nullptr,
+ {},
+ std::make_unique<MQrCodeShower>(testState),
+ std::make_unique<MBtAdvertScanner>(testState));
+ };
+ // Before PerformTransaction()
+ {
+ auto transaction = makeTransaction();
+ testState.Reset(Event::RESET, transaction);
+ EXPECT_THROW(transaction.PerformTransaction(), Cancelled);
+ EXPECT_EQ(testState.m_lastEvent, Event::RESET);
+ EXPECT_EQ(testState.m_cancelCalledOn, CancelCalledOn::NONE);
+ }
+ // In MQrCodeShower
+ {
+ auto transaction = makeTransaction();
+ testState.Reset(Event::QR_CODE_STARTED, transaction);
+ EXPECT_THROW(transaction.PerformTransaction(), Cancelled);
+ EXPECT_EQ(testState.m_lastEvent, Event::QR_CODE_STARTED);
+ // QrCodeShower has no Cancel()
+ EXPECT_EQ(testState.m_cancelCalledOn, CancelCalledOn::NONE);
+ }
+ // After MQrCodeShower
+ {
+ auto transaction = makeTransaction();
+ testState.Reset(Event::QR_CODE_ENDED, transaction);
+ EXPECT_THROW(transaction.PerformTransaction(), Cancelled);
+ EXPECT_EQ(testState.m_lastEvent, Event::QR_CODE_ENDED);
+ // QrCodeShower has no Cancel()
+ EXPECT_EQ(testState.m_cancelCalledOn, CancelCalledOn::NONE);
+ }
+ // In MBleAdvertScanner
+ {
+ auto transaction = makeTransaction();
+ testState.Reset(Event::BLE_ADVERT_STARTED, transaction);
+ EXPECT_THROW(transaction.PerformTransaction(), Cancelled);
+ EXPECT_EQ(testState.m_lastEvent, Event::BLE_ADVERT_STARTED);
+ EXPECT_EQ(testState.m_cancelCalledOn, CancelCalledOn::BLE_ADVERT);
+ }
+ // After MBleAdvertScanner
+ {
+ auto transaction = makeTransaction();
+ testState.Reset(Event::BLE_ADVERT_ENDED, transaction);
+ EXPECT_THROW(transaction.PerformTransaction(), Cancelled);
+ EXPECT_EQ(testState.m_lastEvent, Event::BLE_ADVERT_ENDED);
+ EXPECT_EQ(testState.m_cancelCalledOn, CancelCalledOn::BLE_ADVERT);
+ }
+ // No Cancel
+ {
+ auto transaction = makeTransaction();
+ testState.Reset(Event::FINISHED, transaction);
+ EXPECT_NO_THROW(transaction.PerformTransaction());
+ EXPECT_EQ(testState.m_lastEvent, Event::BLE_ADVERT_ENDED);
+ EXPECT_EQ(testState.m_cancelCalledOn, CancelCalledOn::NONE);
+ }
+}
+
+namespace {
+
+class OTMQrCodeShower : public IQrCodeShower {
+public:
+ void ShowQrCode(const CryptoBuffer & /*qrSecret*/,
+ const CryptoBuffer & /*identityKeyCompressed*/,
+ const Hint & /*hint*/,
+ bool /*stateAssisted*/,
+ wauthn_cb_display_qrcode /*displayQrCodeCallback*/,
+ void * /*displayQrCodeCallbackUserData*/) override
+ {
+ std::this_thread::sleep_for(std::chrono::nanoseconds{get_random(0, 500)});
+ }
+};
+
+class OTMBtAdvertScanner : public IBtAdvertScanner {
+public:
+ int AwaitAdvert(const CryptoBuffer & /*eidKey*/, CryptoBuffer &decryptedAdvert) override
+ {
+ auto res = BT_ERROR_NONE;
+ m_cancelFacilitator.WithCancelCheck([&] { decryptedAdvert.assign(16, 0); },
+ [&] { res = BT_ERROR_CANCELLED; });
+ return res;
+ }
+
+ int Cancel() override
+ {
+ m_cancelFacilitator.Cancel();
+ return BT_ERROR_NONE;
+ }
+
+private:
+ CancelFacilitator m_cancelFacilitator;
+};
+
+} // namespace
+
+TEST(QrTransaction, cancel_from_the_other_thread)
+{
+ auto makeTransaction = [&] {
+ return QrTransaction(nullptr,
+ nullptr,
+ {},
+ std::make_unique<OTMQrCodeShower>(),
+ std::make_unique<OTMBtAdvertScanner>());
+ };
+ TestCancelFromTheOtherThread<ITransaction>(
+ 400, 40, makeTransaction, [](ITransaction &transaction) {
+ transaction.PerformTransaction();
+ });
+}
--- /dev/null
+/*
+ * Copyright (c) 2024 Samsung Electronics Co., Ltd. All rights reserved
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+#include "get_random.h"
+#include "state_assisted_transaction.h"
+#include "test_cancel_from_the_other_thread.h"
+#include "transaction_tester.h"
+
+namespace {
+
+enum class Event : int {
+ RESET = 0,
+ FINISHED,
+};
+
+enum class CancelCalledOn : int {
+ NONE,
+};
+
+using TestState = TransactionTestState<Event, CancelCalledOn>;
+using Tester = TransactionTester<Event, CancelCalledOn>;
+
+} // namespace
+
+TEST(StateAssistedTransaction, Cancel)
+{
+ TestState testState;
+ auto makeTransaction = [&] { return StateAssistedTransaction(); };
+ // Before PerformTransaction()
+ {
+ auto transaction = makeTransaction();
+ testState.Reset(Event::RESET, transaction);
+ EXPECT_THROW(transaction.PerformTransaction(), Cancelled);
+ EXPECT_EQ(testState.m_lastEvent, Event::RESET);
+ EXPECT_EQ(testState.m_cancelCalledOn, CancelCalledOn::NONE);
+ }
+ // No Cancel
+ {
+ auto transaction = makeTransaction();
+ testState.Reset(Event::FINISHED, transaction);
+ EXPECT_NO_THROW(transaction.PerformTransaction());
+ EXPECT_EQ(testState.m_lastEvent, Event::RESET);
+ EXPECT_EQ(testState.m_cancelCalledOn, CancelCalledOn::NONE);
+ }
+}
+
+#if 0 // TODO: enable it in the future. For now, when the implementation does not use mocked classes
+ // this test will fail
+
+TEST(StateAssistedTransaction, cancel_from_the_other_thread)
+{
+ auto makeTransaction = [&] { return StateAssistedTransaction(); };
+ TestCancelFromTheOtherThread<ITransaction>(
+ 1000, 100, makeTransaction, [](ITransaction &transaction) {
+ transaction.PerformTransaction();
+ });
+}
+
+#endif
--- /dev/null
+/*
+ * Copyright (c) 2024 Samsung Electronics Co., Ltd. All rights reserved
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+#pragma once
+
+#include "exception.h"
+#include "get_random.h"
+
+#include <cassert>
+#include <chrono>
+#include <cstddef>
+#include <cstdint>
+#include <exception>
+#include <future>
+#include <gtest/gtest.h>
+#include <iostream>
+#include <mutex>
+#include <thread>
+
+class CancelFacilitator {
+public:
+ template <class Func, class CallbackOnCancel>
+ void WithCancelCheck(Func &&func, CallbackOnCancel &&callbackOnCancel)
+ {
+ // If Cancel happens before operation.
+ if (m_cancellationFuture.wait_for(std::chrono::nanoseconds{get_random(0, 100'000)}) !=
+ std::future_status::timeout) {
+ std::forward<CallbackOnCancel>(callbackOnCancel)();
+ return;
+ }
+ // Run operation.
+ std::forward<Func>(func)();
+ // If Cancel happens after operation but not too late.
+ if (m_cancellationFuture.wait_for(std::chrono::nanoseconds{get_random(0, 100'000)}) !=
+ std::future_status::timeout) {
+ std::forward<CallbackOnCancel>(callbackOnCancel)();
+ return;
+ }
+ // If Cancel happens too late, the upper layer should check for it.
+ std::this_thread::sleep_for(std::chrono::nanoseconds{get_random(0, 100'000)});
+ }
+
+ template <class Func>
+ void WithCancelCheck(Func &&func)
+ {
+ WithCancelCheck(std::forward<Func>(func), [] { throw Cancelled{}; });
+ }
+
+ void CancelCheck()
+ {
+ WithCancelCheck([] {});
+ }
+
+ // May be called only once. May be called from the other thread.
+ void Cancel() { m_cancellationPromise.set_value(); }
+
+private:
+ std::promise<void> m_cancellationPromise;
+ std::future<void> m_cancellationFuture = m_cancellationPromise.get_future();
+};
+
+template <class Interface, class Func, class TransactionInvoker>
+void TestCancelFromTheOtherThread(size_t reps,
+ size_t minExpectedCancellationsNum,
+ Func maker,
+ TransactionInvoker transactionInvoker)
+{
+ auto tp = std::chrono::steady_clock::now();
+ constexpr size_t REPS = 32;
+ for (size_t i = 0; i < REPS; ++i) {
+ auto transaction = maker();
+ transactionInvoker(static_cast<Interface &>(transaction));
+ }
+ const std::chrono::nanoseconds avgTimeOfFullTransaction =
+ (std::chrono::steady_clock::now() - tp) / REPS;
+
+ // Test cancelling before starting the transaction.
+ {
+ auto transactionObj = maker();
+ Interface *transaction = &transactionObj;
+ transaction->Cancel();
+ EXPECT_THROW(transactionInvoker(*transaction), Cancelled);
+ }
+
+ std::mutex mutex;
+ std::condition_variable cv;
+ enum class State {
+ START,
+ NEW_TRANSACTION_READY,
+ CANCELLER_READY_TO_CANCEL,
+ TRANSACTION_FINISHED_FIRST,
+ CANCELLER_FINISHED,
+ FINISHED,
+ } state = State::START;
+ Interface *transaction = nullptr;
+
+ auto canceller = std::thread([&] {
+ auto lock = std::unique_lock{mutex};
+ for (;;) {
+ cv.wait(lock, [&] {
+ return state == State::FINISHED || state == State::NEW_TRANSACTION_READY;
+ });
+ if (state == State::FINISHED)
+ break;
+
+ state = State::CANCELLER_READY_TO_CANCEL;
+ lock.unlock();
+ cv.notify_all();
+
+ std::this_thread::sleep_for(
+ std::chrono::nanoseconds{get_random(0, avgTimeOfFullTransaction.count() * 3)});
+ transaction->Cancel();
+
+ lock.lock();
+ assert(state == State::CANCELLER_READY_TO_CANCEL ||
+ state == State::TRANSACTION_FINISHED_FIRST);
+ state = State::CANCELLER_FINISHED;
+ cv.notify_all();
+ }
+ });
+
+ size_t sucessfullCancellations = 0;
+ auto lock = std::unique_lock{mutex};
+ for (size_t i = 0; i < reps; ++i) {
+ auto transactionObj = maker();
+ transaction = &transactionObj;
+ // Wake up canceller and wait for it to become ready to cancel.
+ state = State::NEW_TRANSACTION_READY;
+ cv.notify_all();
+ // Wait for the canceller.
+ cv.wait(lock, [&] {
+ return state == State::CANCELLER_READY_TO_CANCEL || state == State::CANCELLER_FINISHED;
+ });
+ lock.unlock(); // Allow canceller to finish first.
+
+ try {
+ transactionInvoker(*transaction);
+ } catch (const Cancelled &) {
+ // This may happen if cancellation happens before or during the transaction and is
+ // expected.
+ ++sucessfullCancellations;
+ } catch (const std::exception &e) {
+ ADD_FAILURE() << "Unexpected exception: " << e.what();
+ } catch (...) {
+ ADD_FAILURE() << "Unexpected unknown exception";
+ }
+
+ lock.lock();
+ assert(state == State::CANCELLER_READY_TO_CANCEL || state == State::CANCELLER_FINISHED);
+ if (state != State::CANCELLER_FINISHED) {
+ state = State::TRANSACTION_FINISHED_FIRST;
+ // Wait for canceller before reinitializing transaction.
+ cv.wait(lock, [&] { return state == State::CANCELLER_FINISHED; });
+ }
+ }
+ // Signal canceller to quit and await it
+ state = State::FINISHED;
+ lock.unlock();
+ cv.notify_all();
+ canceller.join();
+
+ std::cerr << "sucessfullCancellations: " << sucessfullCancellations << " of " << reps
+ << std::endl;
+ // Reasonable number of cancellations has to happen, otherwise this code is wrong.
+ EXPECT_GE(sucessfullCancellations, minExpectedCancellationsNum);
+}
--- /dev/null
+/*
+ * Copyright (c) 2024 Samsung Electronics Co., Ltd. All rights reserved
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+#pragma once
+
+#include <cassert>
+#include <gtest/gtest.h>
+#include <stdexcept>
+
+template <class Event, class CancelCalledOn>
+struct TransactionTestState {
+ Event m_lastEvent;
+ Event m_eventToCancelAfter;
+ CancelCalledOn m_cancelCalledOn;
+ ITransaction *m_transaction;
+
+ void Reset(Event eventToCancelAfter, ITransaction &transaction) noexcept
+ {
+ m_lastEvent = Event::RESET;
+ m_eventToCancelAfter = eventToCancelAfter;
+ m_cancelCalledOn = CancelCalledOn::NONE;
+ m_transaction = &transaction;
+ if (m_lastEvent == eventToCancelAfter)
+ transaction.Cancel();
+ }
+};
+
+template <class Event, class CancelCalledOn>
+class TransactionTester {
+public:
+ explicit TransactionTester(TransactionTestState<Event, CancelCalledOn> &testState)
+ : m_testState{testState}
+ {
+ }
+
+ // Returs whether the cancellation occurred.
+ void UpdateAndCheckState(Event startEvent, Event endEvent, bool hasCancel)
+ {
+ // Returns whether the cancellation occurred.
+ auto advanceEvent = [&]() -> bool {
+ m_testState.m_lastEvent = Event{static_cast<int>(m_testState.m_lastEvent) + 1};
+ if (m_testState.m_lastEvent == m_testState.m_eventToCancelAfter) {
+ // Cancel should be called only once.
+ assert(m_testState.m_cancelCalledOn == CancelCalledOn::NONE);
+ m_testState.m_transaction->Cancel();
+ if (hasCancel) {
+ // ITransaction::Cancel() should call apropriate Cancel() method immediately.
+ assert(m_testState.m_cancelCalledOn != CancelCalledOn::NONE);
+ }
+ return true;
+ }
+ return false;
+ };
+ if (advanceEvent())
+ return;
+ if (m_testState.m_lastEvent != startEvent) {
+ ADD_FAILURE() << ": unexpected m_lastEvent: "
+ << static_cast<int>(m_testState.m_lastEvent);
+ throw std::runtime_error{""};
+ }
+ if (advanceEvent())
+ return;
+ if (m_testState.m_lastEvent != endEvent) {
+ ADD_FAILURE() << ": unexpected m_lastEvent: "
+ << static_cast<int>(m_testState.m_lastEvent);
+ throw std::runtime_error{""};
+ }
+ }
+
+ void CheckCancel(CancelCalledOn cancelCalledOn)
+ {
+ if (m_testState.m_lastEvent != m_testState.m_eventToCancelAfter) {
+ ADD_FAILURE() << ": unexpected m_lastEvent: "
+ << static_cast<int>(m_testState.m_lastEvent);
+ throw std::runtime_error{""};
+ }
+ if (m_testState.m_cancelCalledOn != CancelCalledOn::NONE) {
+ ADD_FAILURE() << ": cancel is being called for the second time.";
+ throw std::runtime_error{""};
+ }
+ m_testState.m_cancelCalledOn = cancelCalledOn;
+ }
+
+private:
+ TransactionTestState<Event, CancelCalledOn> &m_testState;
+};