Refactor QR transaction to be mockable and testable 76/305876/11
authorKrzysztof Malysa <k.malysa@samsung.com>
Mon, 12 Feb 2024 10:38:51 +0000 (11:38 +0100)
committerKrzysztof Malysa <k.malysa@samsung.com>
Thu, 22 Feb 2024 15:52:07 +0000 (16:52 +0100)
Change-Id: Ib963fc97d2cce876626f609a3efac411457cfd76

27 files changed:
srcs/CMakeLists.txt
srcs/common.h
srcs/lowercase_hex_string_of.h [new file with mode: 0644]
srcs/qr_code_shower.cpp [new file with mode: 0644]
srcs/qr_code_shower.h [new file with mode: 0644]
srcs/qr_transaction.cpp [new file with mode: 0644]
srcs/qr_transaction.h [new file with mode: 0644]
srcs/request.cpp [deleted file]
srcs/request.h [deleted file]
srcs/request_ga.cpp [deleted file]
srcs/request_ga.h [deleted file]
srcs/request_handler.cpp
srcs/request_handler.h
srcs/request_mc.cpp [deleted file]
srcs/request_mc.h [deleted file]
srcs/state_assisted_transaction.cpp [new file with mode: 0644]
srcs/state_assisted_transaction.h [new file with mode: 0644]
srcs/transaction.h [new file with mode: 0644]
srcs/webauthn_ble.cpp
tests/CMakeLists.txt
tests/api_tests.cpp
tests/get_random.h [new file with mode: 0644]
tests/lowercase_hex_string_of_tests.cpp [new file with mode: 0644]
tests/qr_transaction_tests.cpp [new file with mode: 0644]
tests/state_assisted_transaction_tests.cpp [new file with mode: 0644]
tests/test_cancel_from_the_other_thread.h [new file with mode: 0644]
tests/transaction_tester.h [new file with mode: 0644]

index ec80221a48c556fe493c761685910d556cea2781..9979dc72ede8bf2b67f8079ead5530ae8e3e3b4a 100644 (file)
@@ -59,10 +59,7 @@ SET(WEBAUTHN_BLE_SOURCES
     ${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
@@ -75,6 +72,9 @@ SET(WEBAUTHN_BLE_SOURCES
     ${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)
 
index cbe3af24a1a2c2fae153d64ee0df78e2327c1c74..d8b7e67e9b61c4786c9a77811e29bd235740336b 100644 (file)
@@ -17,6 +17,7 @@
 #pragma once
 
 #include <array>
+#include <utility> // for std::move
 
 typedef std::array<char, 2> Hint;
 
diff --git a/srcs/lowercase_hex_string_of.h b/srcs/lowercase_hex_string_of.h
new file mode 100644 (file)
index 0000000..6a6f06f
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+ *  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;
+}
diff --git a/srcs/qr_code_shower.cpp b/srcs/qr_code_shower.cpp
new file mode 100644 (file)
index 0000000..84b6161
--- /dev/null
@@ -0,0 +1,31 @@
+/*
+ *  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);
+}
diff --git a/srcs/qr_code_shower.h b/srcs/qr_code_shower.h
new file mode 100644 (file)
index 0000000..10568dd
--- /dev/null
@@ -0,0 +1,50 @@
+/*
+ *  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;
+};
diff --git a/srcs/qr_transaction.cpp b/srcs/qr_transaction.cpp
new file mode 100644 (file)
index 0000000..64e7a02
--- /dev/null
@@ -0,0 +1,100 @@
+/*
+ *  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;
+}
diff --git a/srcs/qr_transaction.h b/srcs/qr_transaction.h
new file mode 100644 (file)
index 0000000..4555313
--- /dev/null
@@ -0,0 +1,51 @@
+/*
+ *  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;
+};
diff --git a/srcs/request.cpp b/srcs/request.cpp
deleted file mode 100644 (file)
index 819caff..0000000
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- *  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);
-}
diff --git a/srcs/request.h b/srcs/request.h
deleted file mode 100644 (file)
index 9281727..0000000
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- *  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;
-};
diff --git a/srcs/request_ga.cpp b/srcs/request_ga.cpp
deleted file mode 100644 (file)
index ddedcb6..0000000
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- *  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);
-}
diff --git a/srcs/request_ga.h b/srcs/request_ga.h
deleted file mode 100644 (file)
index d86d390..0000000
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- *  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;
-};
index b24db000ee1ab31e2720c6bf674db8ae54e89540..18a6577aebf72515f138bae5ff5b18ae9d4669f5 100644 (file)
@@ -26,12 +26,13 @@ wauthn_error_e RequestHandler::Cancel()
 {
     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 &) {
index 2c1a9dd2a5b4c41877e12b2ea21ff1dee4c4f0d4..aa32b2b4be73d48f24297041a89d89853102205f 100644 (file)
 
 #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:
@@ -34,51 +58,75 @@ 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 (...) {
@@ -90,5 +138,5 @@ private:
     RequestHandler() noexcept = default;
 
     std::mutex m_mutex;
-    std::unique_ptr<Request> m_currentRequest;
+    std::unique_ptr<ITransaction> m_currentTransaction;
 };
diff --git a/srcs/request_mc.cpp b/srcs/request_mc.cpp
deleted file mode 100644 (file)
index 36e4497..0000000
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- *  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);
-}
diff --git a/srcs/request_mc.h b/srcs/request_mc.h
deleted file mode 100644 (file)
index aaf6d01..0000000
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- *  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;
-};
diff --git a/srcs/state_assisted_transaction.cpp b/srcs/state_assisted_transaction.cpp
new file mode 100644 (file)
index 0000000..27aaeb8
--- /dev/null
@@ -0,0 +1,51 @@
+/*
+ *  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;
+}
diff --git a/srcs/state_assisted_transaction.h b/srcs/state_assisted_transaction.h
new file mode 100644 (file)
index 0000000..048fa32
--- /dev/null
@@ -0,0 +1,39 @@
+/*
+ *  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;
+};
diff --git a/srcs/transaction.h b/srcs/transaction.h
new file mode 100644 (file)
index 0000000..229c545
--- /dev/null
@@ -0,0 +1,34 @@
+/*
+ *  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;
+};
index 23e741c7e7ff15235085cd7ee994dc9497076a20..ec5bd5cf75fcb089e5acead0fe07ba2c8f355729 100644 (file)
@@ -14,9 +14,7 @@
  *  limitations under the License
  */
 
-#include "request_ga.h"
 #include "request_handler.h"
-#include "request_mc.h"
 
 #include <webauthn-hal.h>
 
@@ -25,7 +23,7 @@ void 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<RequestMakeCredential>(client_data, options, callbacks);
+    RequestHandler::Instance().Process(client_data, RequestMC{options, callbacks});
 }
 
 WEBAUTHN_HAL_API
@@ -33,7 +31,7 @@ void 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<RequestGetAssertion>(client_data, options, callbacks);
+    RequestHandler::Instance().Process(client_data, RequestGA{options, callbacks});
 }
 
 WEBAUTHN_HAL_API
index 86d469a3ae0583dcc571cf4c6f17c5b297896524..65c94c42175ffaabe59e7cf84fbbc90b3a5a418a 100644 (file)
@@ -35,6 +35,9 @@ SET(UNIT_TESTS_SOURCES
     ${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
index bb617b11d4c5edce1c4c71a2c7b69d42a8607369..e5081298206b5032863eaa455da5e2628f465a6e 100644 (file)
  */
 
 #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>
 
@@ -44,18 +46,18 @@ public:
             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();
     }
 
@@ -96,18 +98,36 @@ void GaCallback(const wauthn_pubkey_credential_assertion_s *pubkey_cred,
     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,
@@ -119,8 +139,7 @@ void InvokeMc(const wauthn_client_data_s *clientData,
     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,
@@ -134,8 +153,7 @@ 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>
@@ -163,43 +181,41 @@ TEST(ApiTest, testInvalidArguments)
     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);
 }
@@ -234,12 +250,12 @@ TEST(ApiTest, testQrCallback)
     // 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__);
 }
diff --git a/tests/get_random.h b/tests/get_random.h
new file mode 100644 (file)
index 0000000..ce58ad1
--- /dev/null
@@ -0,0 +1,29 @@
+/*
+ *  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);
+}
diff --git a/tests/lowercase_hex_string_of_tests.cpp b/tests/lowercase_hex_string_of_tests.cpp
new file mode 100644 (file)
index 0000000..8a08c7e
--- /dev/null
@@ -0,0 +1,29 @@
+/*
+ *  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");
+}
diff --git a/tests/qr_transaction_tests.cpp b/tests/qr_transaction_tests.cpp
new file mode 100644 (file)
index 0000000..e814808
--- /dev/null
@@ -0,0 +1,191 @@
+/*
+ *  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();
+        });
+}
diff --git a/tests/state_assisted_transaction_tests.cpp b/tests/state_assisted_transaction_tests.cpp
new file mode 100644 (file)
index 0000000..3014d94
--- /dev/null
@@ -0,0 +1,72 @@
+/*
+ *  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
diff --git a/tests/test_cancel_from_the_other_thread.h b/tests/test_cancel_from_the_other_thread.h
new file mode 100644 (file)
index 0000000..aecf03a
--- /dev/null
@@ -0,0 +1,179 @@
+/*
+ *  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);
+}
diff --git a/tests/transaction_tester.h b/tests/transaction_tester.h
new file mode 100644 (file)
index 0000000..bce41cb
--- /dev/null
@@ -0,0 +1,99 @@
+/*
+ *  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;
+};