Add Noise handshake in QrTransaction 79/305879/12
authorKrzysztof Malysa <k.malysa@samsung.com>
Mon, 12 Feb 2024 13:55:26 +0000 (14:55 +0100)
committerKrzysztof Jackiewicz <k.jackiewicz@samsung.com>
Tue, 5 Mar 2024 12:19:00 +0000 (12:19 +0000)
Change-Id: I6a7ae770c98d6fbf0b9f6e3e330a0419d85b08f9

22 files changed:
srcs/CMakeLists.txt
srcs/bluetooth_advert.cpp
srcs/common.h
srcs/crypto/encryptor.cpp
srcs/crypto/noise/noise.cpp
srcs/crypto/noise/noise.h
srcs/derive_key.h
srcs/encrypted_tunnel.cpp [new file with mode: 0644]
srcs/encrypted_tunnel.h [new file with mode: 0644]
srcs/handshake.cpp [new file with mode: 0644]
srcs/handshake.h [new file with mode: 0644]
srcs/qr_transaction.cpp
srcs/qr_transaction.h
srcs/request_handler.h
tests/CMakeLists.txt
tests/api_tests.cpp
tests/bluetooth_tests.cpp
tests/crypto/encryptor_unittest.cpp
tests/encrypted_tunnel_tests.cpp [new file with mode: 0644]
tests/handshake_tests.cpp [new file with mode: 0644]
tests/qr_transaction_tests.cpp
tests/transaction_tester.h

index 6c2f48422e5e1b74e81d99491141430f9f4aa412..f5c60070508e56141f65f46b8e3571148c4091e2 100644 (file)
@@ -77,6 +77,8 @@ SET(WEBAUTHN_BLE_SOURCES
     ${CMAKE_CURRENT_SOURCE_DIR}/qr_code_shower.cpp
     ${CMAKE_CURRENT_SOURCE_DIR}/tunnel.cpp
     ${CMAKE_CURRENT_SOURCE_DIR}/websockets.cpp
+    ${CMAKE_CURRENT_SOURCE_DIR}/handshake.cpp
+    ${CMAKE_CURRENT_SOURCE_DIR}/encrypted_tunnel.cpp
 )
 SET(WEBAUTHN_BLE_SOURCES ${WEBAUTHN_BLE_SOURCES} PARENT_SCOPE)
 
index aa35db9f0bc64f898242dd4bf0e7909ed5b4efd2..4e5a2d86e30e4d0494f481d83b7612a6e82bbe4c 100644 (file)
@@ -14,6 +14,7 @@
  *  limitations under the License
  */
 #include "bluetooth_advert.h"
+#include "common.h"
 #include "crypto/encryptor.h"
 #include "crypto/hmac.h"
 #include "log/log.h"
@@ -307,7 +308,7 @@ int BtAdvertScanner::AwaitAdvert(const CryptoBuffer &eidKey, CryptoBuffer &decry
                        int serviceDataLen,
                        void *userData) noexcept {
         constexpr auto FIDO_CABLE_UUID16 = std::string_view{"FFF9"};
-        constexpr size_t ADVERT_LEN = 16;
+        constexpr size_t ADVERT_LEN = BLUETOOTH_ADVERT_LEN;
         constexpr size_t ENCRYPTED_ADVERT_TAG_LEN = 4;
         constexpr size_t EXPECTED_SERVICE_DATA_LEN = ADVERT_LEN + ENCRYPTED_ADVERT_TAG_LEN;
         if (serviceUUID == FIDO_CABLE_UUID16 && serviceDataLen == EXPECTED_SERVICE_DATA_LEN) {
@@ -377,10 +378,10 @@ int BtAdvertScanner::Cancel()
 
 UnpackedBleAdvert UnpackDecryptedAdvert(const CryptoBuffer &decryptedAdvert)
 {
-    assert(decryptedAdvert.size() == 16);
-    if (decryptedAdvert[0] != 0) {
-        throw std::runtime_error{"First byte of decrypted advert is 0 but it shouldn't be"};
-    }
+    assert(decryptedAdvert.size() == BLUETOOTH_ADVERT_LEN);
+    if (decryptedAdvert[0] != 0)
+        throw std::runtime_error{"First byte of decrypted advert should have value 0"};
+
     UnpackedBleAdvert res;
     std::memcpy(res.nonce.data(), decryptedAdvert.data() + 1, res.nonce.size());
     std::memcpy(res.routingId.data(), decryptedAdvert.data() + 11, res.routingId.size());
index d8b7e67e9b61c4786c9a77811e29bd235740336b..7b8d697d62633fdda500ff181aa1d19d4fe7b0ca 100644 (file)
@@ -19,6 +19,9 @@
 #include <array>
 #include <utility> // for std::move
 
+constexpr size_t QR_SECRET_LEN = 16;
+constexpr size_t BLUETOOTH_ADVERT_LEN = 16;
+
 typedef std::array<char, 2> Hint;
 
 template <typename T>
index 07bceb29845d5ff759141023a9a81a2bfcd09b67..7837a2c65168e5faf46a9998da338fe005178bca 100644 (file)
@@ -147,9 +147,13 @@ CryptoBuffer DecryptAes256GCM(const CryptoBuffer &key,
         LogError("Invalid key length");
         throw OpensslError{};
     }
+    constexpr int TAG_LEN = 16;
+    if (ciphertext.size() < TAG_LEN) {
+        LogError("ciphertext length shorter than tag length");
+        throw OpensslError{};
+    }
 
     EVP_CIPHER_CTX *ctx = nullptr;
-    constexpr int TAG_LEN = 16;
     int len = 0;
     size_t outputLen = 0;
     CryptoBuffer output(ciphertext.size() - TAG_LEN);
index 02d1f6a201907719b58fd0f07e54493492d575a9..d53695009465fd360d19405348b161a01334816d 100644 (file)
@@ -29,8 +29,6 @@
 
 namespace {
 
-constexpr size_t KEY_AND_HASH_LEN = 32;
-
 CryptoBuffer NonceToInitialVector(uint64_t nonce) noexcept
 {
     CryptoBuffer res(12);
index 9114e2619cfdfb5730a88c2c25ef0d40620887f8..0afcfb2acc17b69ce8441d477eaf897737bf5db4 100644 (file)
@@ -23,6 +23,8 @@
 
 namespace Crypto::Noise {
 
+constexpr size_t KEY_AND_HASH_LEN = 32;
+
 class SymmetricState {
 public:
     enum HandshakeKind {
index 3ed07a723773d64935a5b0679aca73041903b437..fb06f43cb5c284205594292acb0b312e1f0605a3 100644 (file)
 
 #include "crypto/common.h"
 
+constexpr size_t EID_KEY_LEN = 64;
+constexpr size_t TUNNEL_ID_LEN = 16;
+constexpr size_t PSK_LEN = 32;
+
 enum class KeyPurpose : uint8_t {
     EIDKey = 1,
     TunnelID = 2,
diff --git a/srcs/encrypted_tunnel.cpp b/srcs/encrypted_tunnel.cpp
new file mode 100644 (file)
index 0000000..3a7ad3a
--- /dev/null
@@ -0,0 +1,58 @@
+/*
+ *  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 "encrypted_tunnel.h"
+#include "exception.h"
+
+#include <utility>
+
+EncryptedTunnel::EncryptedTunnel(std::unique_ptr<ITunnel> tunnel,
+                                 CipherState reader,
+                                 CipherState writer)
+: m_tunnel{std::move(tunnel)}, m_reader{std::move(reader)}, m_writer{std::move(writer)}
+{
+}
+
+CryptoBuffer EncryptedTunnel::ReadBinary()
+{
+    CryptoBuffer ciphertext = m_tunnel->ReadBinary();
+    auto msg = m_reader.DecryptWithAd(ciphertext, {});
+    if (msg.empty())
+        THROW_UNKNOWN("invalid message");
+
+    size_t paddingBytes = msg.back();
+    if (paddingBytes + 1 > msg.size())
+        THROW_UNKNOWN("invalid message");
+
+    msg.resize(msg.size() - paddingBytes - 1);
+    return msg;
+}
+
+void EncryptedTunnel::WriteBinary(CryptoBuffer msg)
+{
+    if (msg.size() > (1 << 20))
+        THROW_UNKNOWN("message too large");
+
+    constexpr size_t paddingGranularity = 32;
+    size_t extraBytes = paddingGranularity - msg.size() % paddingGranularity;
+    msg.resize(msg.size() + extraBytes, 0);
+    msg.back() = static_cast<uint8_t>(extraBytes) - 1;
+
+    auto ciphertext = m_writer.EncryptWithAd(msg, {});
+    m_tunnel->WriteBinary(ciphertext);
+}
+
+void EncryptedTunnel::Cancel() { m_tunnel->Cancel(); }
diff --git a/srcs/encrypted_tunnel.h b/srcs/encrypted_tunnel.h
new file mode 100644 (file)
index 0000000..a46cd34
--- /dev/null
@@ -0,0 +1,53 @@
+/*
+ *  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 "crypto/common.h"
+#include "crypto/noise/noise.h"
+#include "tunnel.h"
+
+#include <memory>
+
+class IEncryptedTunnel {
+public:
+    virtual CryptoBuffer ReadBinary() = 0;
+
+    virtual void WriteBinary(CryptoBuffer msg) = 0;
+
+    // May be called from the other thread.
+    virtual void Cancel() = 0;
+};
+
+class EncryptedTunnel : public IEncryptedTunnel {
+public:
+    using CipherState = Crypto::Noise::SymmetricState::CipherState;
+
+    explicit EncryptedTunnel(std::unique_ptr<ITunnel> tunnel,
+                             CipherState reader,
+                             CipherState writer);
+
+    CryptoBuffer ReadBinary() override;
+
+    void WriteBinary(CryptoBuffer msg) override;
+
+    void Cancel() override;
+
+private:
+    std::unique_ptr<ITunnel> m_tunnel;
+    CipherState m_reader;
+    CipherState m_writer;
+};
diff --git a/srcs/handshake.cpp b/srcs/handshake.cpp
new file mode 100644 (file)
index 0000000..36e8065
--- /dev/null
@@ -0,0 +1,157 @@
+/*
+ *  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 "bluetooth_advert.h"
+#include "crypto/ecdh.h"
+#include "crypto/noise/noise.h"
+#include "derive_key.h"
+#include "exception.h"
+#include "handshake.h"
+#include "log/log.h"
+#include "lowercase_hex_string_of.h"
+#include "tunnel_server_domain.h"
+
+#include <utility>
+
+namespace {
+
+struct InitialHandshakeMessage {
+    CryptoBuffer msg;
+    Crypto::X9_62_P_256_Key ephermalKey;
+    Crypto::Noise::SymmetricState noise;
+};
+
+InitialHandshakeMessage initialHandshakeMessage(const CryptoBuffer &psk,
+                                                const Crypto::X9_62_P_256_Key &privKey)
+{
+    InitialHandshakeMessage res{
+        CryptoBuffer{},
+        Crypto::X9_62_P_256_Key::Create(),
+        Crypto::Noise::SymmetricState{
+            Crypto::Noise::SymmetricState::HandshakeKind::KNpsk0_P256_AESGCM_SHA256},
+    };
+    res.noise.MixHash(CryptoBuffer{1});
+    res.noise.MixHash(privKey.ExportPublicKey(false));
+
+    res.noise.MixKeyAndHash(psk);
+
+    auto exportedEphermalKeyPublic = res.ephermalKey.ExportPublicKey(false);
+    res.noise.MixHash(exportedEphermalKeyPublic);
+    res.noise.MixKey(exportedEphermalKeyPublic);
+
+    res.msg = std::move(exportedEphermalKeyPublic);
+    auto ciphertext = res.noise.EncryptAndHash({});
+    res.msg.insert(res.msg.end(), ciphertext.begin(), ciphertext.end());
+    return res;
+}
+
+struct ProcessedHandshakeResponse {
+    Crypto::Noise::SymmetricState::SplitRes splitRes;
+    CryptoBuffer handshakeHash;
+};
+
+ProcessedHandshakeResponse processHandshakeResponse(const CryptoBuffer &peerHandshakeMessage,
+                                                    const Crypto::X9_62_P_256_Key &ephermalKey,
+                                                    const Crypto::X9_62_P_256_Key &privKey,
+                                                    Crypto::Noise::SymmetricState &&noise)
+{
+    constexpr size_t p256X962Length = 1 + 32 + 32;
+    if (peerHandshakeMessage.size() < p256X962Length) {
+        LogError("invalid handshake response length: " << peerHandshakeMessage.size()
+                                                       << " expected: " << p256X962Length);
+        throw Unknown{};
+    }
+
+    auto peerPointBytes =
+        CryptoBuffer(peerHandshakeMessage.begin(), peerHandshakeMessage.begin() + p256X962Length);
+    auto ciphertext =
+        CryptoBuffer(peerHandshakeMessage.begin() + p256X962Length, peerHandshakeMessage.end());
+
+    noise.MixHash(peerPointBytes);
+    noise.MixKey(peerPointBytes);
+
+    auto peerPublicKey = Crypto::X9_62_P_256_Key::ImportPublicKey(peerPointBytes);
+    noise.MixKey(Crypto::deriveECDHSharedSecret(ephermalKey, peerPublicKey));
+    noise.MixKey(Crypto::deriveECDHSharedSecret(privKey, peerPublicKey));
+
+    auto plaintext = noise.DecryptAndHash(ciphertext);
+    if (!plaintext.empty()) {
+        LogError("non-empty decrypted handshake response");
+        throw Unknown{};
+    }
+
+    return ProcessedHandshakeResponse{
+        noise.Split(),
+        noise.GetHandshakeHash(),
+    };
+}
+
+} // namespace
+
+Handshake::Handshake(std::unique_ptr<ITunnel> tunnel) : m_tunnel{std::move(tunnel)} {}
+
+std::unique_ptr<IEncryptedTunnel>
+Handshake::ConnectAndDoQrHandshake(const CryptoBuffer &qrSecret,
+                                   const CryptoBuffer &decryptedBleAdvert,
+                                   const Crypto::X9_62_P_256_Key &identityKey)
+{
+    auto unpackedAdvert = UnpackDecryptedAdvert(decryptedBleAdvert);
+    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);
+
+    auto tunnelId = DeriveKey(qrSecret, {}, KeyPurpose::TunnelID, TUNNEL_ID_LEN);
+
+    std::string url = "wss://";
+    url += tunnelServerDomain;
+    url += "/cable/connect/";
+    url += LowercaseHexStringOf(unpackedAdvert.routingId);
+    url += '/';
+    url += LowercaseHexStringOf(tunnelId);
+
+    m_tunnel->Connect(url); // Should throw if Cancel is already called.
+
+    // Handshake
+    auto psk = DeriveKey(qrSecret, decryptedBleAdvert, KeyPurpose::PSK, PSK_LEN);
+    auto [msg, ephermalKey, noise] = initialHandshakeMessage(psk, identityKey);
+
+    m_tunnel->WriteBinary(msg); // Should throw if Cancel is already called.
+
+    CryptoBuffer response = m_tunnel->ReadBinary(); // Should throw if Cancel is already called.
+
+    // process response
+    auto [splitRes, handshakeHash] =
+        processHandshakeResponse(response, ephermalKey, identityKey, std::move(noise));
+
+    auto guard = std::unique_lock{m_lock};
+    if (m_cancelled)
+        throw Cancelled{};
+
+    return std::make_unique<EncryptedTunnel>(
+        std::move(m_tunnel), std::move(splitRes.second), std::move(splitRes.first));
+}
+
+void Handshake::Cancel()
+{
+    auto guard = std::lock_guard{m_lock};
+    m_cancelled = true;
+    if (m_tunnel)
+        m_tunnel->Cancel();
+}
diff --git a/srcs/handshake.h b/srcs/handshake.h
new file mode 100644 (file)
index 0000000..1cd9144
--- /dev/null
@@ -0,0 +1,60 @@
+/*
+ *  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 "crypto/ec_key.h"
+#include "encrypted_tunnel.h"
+#include "tunnel.h"
+
+#include <mutex>
+
+class IHandshake {
+public:
+    IHandshake() = default;
+    IHandshake(const IHandshake &) = delete;
+    IHandshake(IHandshake &&) = delete;
+    IHandshake &operator=(const IHandshake &) = delete;
+    IHandshake &operator=(IHandshake &&) = delete;
+    virtual ~IHandshake() = default;
+
+    // May be called only once. Throws Cancelled on cancel.
+    virtual std::unique_ptr<IEncryptedTunnel>
+    ConnectAndDoQrHandshake(const CryptoBuffer &qrSecret,
+                            const CryptoBuffer &decryptedBleAdvert,
+                            const Crypto::X9_62_P_256_Key &identityKey) = 0;
+
+    // May be called from the other threads.
+    virtual void Cancel() = 0;
+};
+
+class Handshake : public IHandshake {
+public:
+    explicit Handshake(std::unique_ptr<ITunnel> tunnel = std::make_unique<Tunnel>());
+
+    std::unique_ptr<IEncryptedTunnel>
+    ConnectAndDoQrHandshake(const CryptoBuffer &qrSecret,
+                            const CryptoBuffer &decryptedBleAdvert,
+                            const Crypto::X9_62_P_256_Key &identityKey) override;
+
+    void Cancel() override;
+
+private:
+    std::unique_ptr<ITunnel> m_tunnel;
+
+    bool m_cancelled = false;
+    std::mutex m_lock;
+};
index 64e7a02554536f9d94881d52a39e0ae294737376..257b6b3ca766a2ed52c2dd741b109a489db74a5c 100644 (file)
@@ -21,7 +21,6 @@
 #include "log/log.h"
 #include "lowercase_hex_string_of.h"
 #include "qr_transaction.h"
-#include "tunnel_server_domain.h"
 
 #include <utility>
 
@@ -29,12 +28,14 @@ QrTransaction::QrTransaction(wauthn_cb_display_qrcode displayQrCodeCallback,
                              void *displayQrCodeCallbackUserData,
                              Hint hint,
                              std::unique_ptr<IQrCodeShower> qrCodeShower,
-                             IBtAdvertScannerUPtr btAdvertScanner)
+                             IBtAdvertScannerUPtr btAdvertScanner,
+                             std::unique_ptr<IHandshake> handshake)
 : m_displayQrCodeCallback{displayQrCodeCallback},
   m_displayQrCodeCallbackUserData{displayQrCodeCallbackUserData},
   m_hint{std::move(hint)},
   m_qrCodeShower{std::move(qrCodeShower)},
-  m_btAdvertScanner{std::move(btAdvertScanner)}
+  m_btAdvertScanner{std::move(btAdvertScanner)},
+  m_handshake{std::move(handshake)}
 {
 }
 
@@ -50,7 +51,7 @@ void QrTransaction::PerformTransaction()
 
     updateStateAndCheckForCancel(State::SHOWING_QR_CODE);
 
-    CryptoBuffer qrSecret = Crypto::RandomBytes(16);
+    CryptoBuffer qrSecret = Crypto::RandomBytes(QR_SECRET_LEN);
     Crypto::X9_62_P_256_Key identityKey = Crypto::X9_62_P_256_Key::Create();
     m_qrCodeShower->ShowQrCode(qrSecret,
                                identityKey.ExportPublicKey(true),
@@ -62,7 +63,7 @@ void QrTransaction::PerformTransaction()
     updateStateAndCheckForCancel(State::AWAITING_BLE_ADVERT);
 
     CryptoBuffer decryptedAdvert;
-    auto eidKey = DeriveKey(qrSecret, {}, KeyPurpose::EIDKey, 64);
+    auto eidKey = DeriveKey(qrSecret, {}, KeyPurpose::EIDKey, EID_KEY_LEN);
     int err = m_btAdvertScanner->AwaitAdvert(eidKey, decryptedAdvert); // may throw
     if (err == BT_ERROR_CANCELLED)
         throw Cancelled{};
@@ -71,14 +72,17 @@ void QrTransaction::PerformTransaction()
         throw Unknown{};
     }
 
-    auto unpackedAdvert = UnpackDecryptedAdvert(decryptedAdvert);
-    LogDebug("unpacked BLE advert: nonce = "
-             << LowercaseHexStringOf(unpackedAdvert.nonce)
-             << ", routingId = " << LowercaseHexStringOf(unpackedAdvert.routingId)
-             << ", encodedTunnelServerDomain = " << unpackedAdvert.encodedTunnelServerDomain);
+    updateStateAndCheckForCancel(State::DOING_HANDSHAKE);
 
-    auto tunnelServerDomain = DecodeTunnelServerDomain(unpackedAdvert.encodedTunnelServerDomain);
-    LogDebug("decoded tunnel server domain: " << tunnelServerDomain);
+    m_encryptedTunnel =
+        m_handshake->ConnectAndDoQrHandshake(qrSecret, decryptedAdvert, identityKey);
+
+    updateStateAndCheckForCancel(State::READING_FROM_ENCRYPTED_TUNNEL);
+
+    {
+        auto msg = m_encryptedTunnel->ReadBinary();
+        LogDebug("Decrypted GetInfo: " << LowercaseHexStringOf(msg));
+    }
 
     updateStateAndCheckForCancel(State::NOT_IN_PROGRESS);
 }
@@ -95,6 +99,12 @@ void QrTransaction::Cancel()
         if (err != BT_ERROR_NONE)
             throw Unknown{};
     } break;
+    case State::DOING_HANDSHAKE: {
+        m_handshake->Cancel();
+    } break;
+    case State::READING_FROM_ENCRYPTED_TUNNEL: {
+        m_encryptedTunnel->Cancel();
+    } break;
     }
     m_state = State::CANCELLED;
 }
index 45553139958f8b98598392cf588f853afdc25db8..e0c8164a9ae9e16056bbc943b01c66c1126b2166 100644 (file)
@@ -17,6 +17,7 @@
 #pragma once
 
 #include "bluetooth_advert.h"
+#include "handshake.h"
 #include "qr_code_shower.h"
 #include "transaction.h"
 
@@ -26,7 +27,8 @@ public:
                   void *displayQrCodeCallbackUserData,
                   Hint hint,
                   std::unique_ptr<IQrCodeShower> qrCodeShower = std::make_unique<QrCodeShower>(),
-                  IBtAdvertScannerUPtr btAdvertScanner = std::make_unique<BtAdvertScanner>());
+                  IBtAdvertScannerUPtr btAdvertScanner = std::make_unique<BtAdvertScanner>(),
+                  std::unique_ptr<IHandshake> handshake = std::make_unique<Handshake>());
 
     void PerformTransaction() override;
 
@@ -38,12 +40,16 @@ private:
     Hint m_hint;
     std::unique_ptr<IQrCodeShower> m_qrCodeShower;
     IBtAdvertScannerUPtr m_btAdvertScanner;
+    std::unique_ptr<IHandshake> m_handshake;
+    std::unique_ptr<IEncryptedTunnel> m_encryptedTunnel;
 
     enum class State {
         CANCELLED,
         NOT_IN_PROGRESS,
         SHOWING_QR_CODE,
         AWAITING_BLE_ADVERT,
+        DOING_HANDSHAKE,
+        READING_FROM_ENCRYPTED_TUNNEL,
     };
 
     std::mutex m_lock;
index 04657a3eec4d584b418e6df4e29c27b7647076e5..ca196b25a2d34ff1ab96bc4a357216f03a6d881a 100644 (file)
@@ -19,6 +19,7 @@
 #include "bluetooth_advert.h"
 #include "common.h"
 #include "exception.h"
+#include "handshake.h"
 #include "log/log.h"
 #include "qr_code_shower.h"
 #include "qr_transaction.h"
@@ -64,7 +65,8 @@ public:
         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)
+        IBtAdvertScannerUPtr btAdvertScanner = nullptr,
+        std::unique_ptr<IHandshake> handshake = nullptr)
     {
         wauthn_error_e result;
         try {
@@ -78,6 +80,8 @@ public:
                 qrCodeShower = std::make_unique<QrCodeShower>();
             if (!btAdvertScanner)
                 btAdvertScanner = std::make_unique<BtAdvertScanner>();
+            if (!handshake)
+                handshake = std::make_unique<Handshake>();
 
             {
                 std::lock_guard<std::mutex> lock(m_mutex);
@@ -97,7 +101,8 @@ public:
                                                         request.callbacks->user_data,
                                                         ToHint(request),
                                                         std::move(qrCodeShower),
-                                                        std::move(btAdvertScanner));
+                                                        std::move(btAdvertScanner),
+                                                        std::move(handshake));
                 }
             }
 
index ccb094725e722c06e375ce8e08f43bab63f493af..97df4b1a5cc9e750aa732b13d67957950510caf8 100644 (file)
@@ -39,6 +39,8 @@ SET(UNIT_TESTS_SOURCES
     ${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}/handshake_tests.cpp
+    ${CMAKE_CURRENT_SOURCE_DIR}/encrypted_tunnel_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 e5081298206b5032863eaa455da5e2628f465a6e..5d3629a7c04fb3f903644d68798e4d31bc0e84bc 100644 (file)
@@ -108,6 +108,19 @@ public:
     int Cancel() override { throw std::runtime_error{"should not be called"}; }
 };
 
+class MHandshake : public IHandshake {
+public:
+    std::unique_ptr<IEncryptedTunnel>
+    ConnectAndDoQrHandshake(const CryptoBuffer & /*qrSecret*/,
+                            const CryptoBuffer & /*decryptedBleAdvert*/,
+                            const Crypto::X9_62_P_256_Key & /*identityKey*/) override
+    {
+        throw std::runtime_error{"should not be called"};
+    }
+
+    void 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)
@@ -115,7 +128,8 @@ void mocked_wah_make_credential(const wauthn_client_data_s *client_data,
     RequestHandler::Instance().Process(client_data,
                                        RequestMC{options, callbacks},
                                        std::make_unique<QrCodeShower>(),
-                                       std::make_unique<MBtAdvertScanner>());
+                                       std::make_unique<MBtAdvertScanner>(),
+                                       std::make_unique<MHandshake>());
 }
 
 void mocked_wah_get_assertion(const wauthn_client_data_s *client_data,
@@ -125,7 +139,8 @@ void mocked_wah_get_assertion(const wauthn_client_data_s *client_data,
     RequestHandler::Instance().Process(client_data,
                                        RequestGA{options, callbacks},
                                        std::make_unique<QrCodeShower>(),
-                                       std::make_unique<MBtAdvertScanner>());
+                                       std::make_unique<MBtAdvertScanner>(),
+                                       std::make_unique<MHandshake>());
 }
 
 void InvokeMc(const wauthn_client_data_s *clientData,
index c2cbdfb444443c0f02fdb692b85460ac83237f4c..ea13ec34cd1d4b1b86e56f29548dcda4fee2fb79 100644 (file)
 
 #include "bluetooth_advert.h"
 #include "bt_error_to_string.h"
+#include "common.h"
 #include "crypto/encryptor.h"
 #include "crypto/hmac.h"
 #include "crypto/random.h"
 #include "derive_key.h"
 #include "get_random.h"
+#include "lowercase_hex_string_of.h"
 #include "test_cancel_from_the_other_thread.h"
 #include "turn_bluetooth.h"
 
 using std::cerr;
 using std::endl;
 
-namespace {
-
-std::string BinToHex(const char *data, int len)
-{
-    std::string res;
-    for (int i = 0; i < len; i++) {
-        char byte[3];
-        sprintf(byte, "%02x", data[i]);
-        res.append(byte);
-    }
-    return res;
-}
-
-} // anonymous namespace
-
 TEST(BluetoothTest, safely_defering_callback_to_be_run_after_g_main_loop_run)
 {
     // This test ensures this logic works in the Bluetooth:StopLEScan() preventing a deadlock
@@ -104,8 +91,9 @@ TEST(BluetoothTest, scan_for_up_to_1_second)
                        const char *serviceData,
                        int serviceDataLen,
                        void * /*userData*/) noexcept {
-        cerr << "Scanned advert: UUID = " << serviceUUID
-             << " data = " << BinToHex(serviceData, serviceDataLen) << endl;
+        cerr << "Scanned advert: UUID = " << serviceUUID << " data = "
+             << LowercaseHexStringOf(CryptoBuffer{serviceData, serviceData + serviceDataLen})
+             << endl;
         return IBluetooth::Scanning::CONTINUE;
     };
     EXPECT_EQ(err = bt.StartLEAdvertScanAndAwaitStop(callback, nullptr), BT_ERROR_CANCELLED)
@@ -116,12 +104,9 @@ TEST(BluetoothTest, scan_for_up_to_1_second)
 
 namespace {
 
-constexpr size_t EID_KEY_LEN = 64;
-constexpr size_t ADVERT_PLAINTEXT_LEN = 16;
-
 CryptoBuffer RandomAdvertPlaintext()
 {
-    auto res = Crypto::RandomBytes(ADVERT_PLAINTEXT_LEN);
+    auto res = Crypto::RandomBytes(BLUETOOTH_ADVERT_LEN);
     res[0] = 0;
     return res;
 }
@@ -132,13 +117,13 @@ struct ServiceData {
 
 CryptoBuffer RandomEidKey()
 {
-    return DeriveKey(Crypto::RandomBytes(16), {}, KeyPurpose::EIDKey, EID_KEY_LEN);
+    return DeriveKey(Crypto::RandomBytes(QR_SECRET_LEN), {}, KeyPurpose::EIDKey, EID_KEY_LEN);
 }
 
 ServiceData GenerateServiceData(const CryptoBuffer &eidKey, const CryptoBuffer &advertPlaintext)
 {
     assert(eidKey.size() == EID_KEY_LEN);
-    assert(advertPlaintext.size() == ADVERT_PLAINTEXT_LEN);
+    assert(advertPlaintext.size() == BLUETOOTH_ADVERT_LEN);
 
     auto aesKey = CryptoBuffer(eidKey.begin(), eidKey.begin() + 32);
     auto encryptedAdvert = Crypto::EncryptAes256ECB(aesKey, advertPlaintext);
@@ -147,10 +132,10 @@ ServiceData GenerateServiceData(const CryptoBuffer &eidKey, const CryptoBuffer &
     auto hmac = Crypto::HmacSha256(hmacKey, encryptedAdvert);
 
     auto res = ServiceData{};
-    assert(encryptedAdvert.size() == 16);
-    res.raw.resize(20);
-    std::memcpy(res.raw.data(), encryptedAdvert.data(), 16);
-    std::memcpy(res.raw.data() + 16, hmac.data(), 4);
+    assert(encryptedAdvert.size() == BLUETOOTH_ADVERT_LEN);
+    res.raw.resize(BLUETOOTH_ADVERT_LEN + 4);
+    std::memcpy(res.raw.data(), encryptedAdvert.data(), BLUETOOTH_ADVERT_LEN);
+    std::memcpy(res.raw.data() + BLUETOOTH_ADVERT_LEN, hmac.data(), 4);
     return res;
 }
 
@@ -505,5 +490,5 @@ TEST(UnpackDecryptedAdvert, first_byte_is_non_0)
         1, 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'x', 'y', 'z', 0xb4, 0x3f};
     EXPECT_THAT([&] { UnpackDecryptedAdvert(decryptedAdvert); },
                 testing::ThrowsMessage<std::runtime_error>(
-                    "First byte of decrypted advert is 0 but it shouldn't be"));
+                    "First byte of decrypted advert should have value 0"));
 }
index 0d1cb321947809b2b149f4489ac7e62ea1eb647a..7caa8d785ce6313120c2eba3201ac8f703a8f806 100644 (file)
@@ -145,6 +145,9 @@ TEST(EncryptAes256GCM, EncryptWrongInputTestCase6)
 
     output[0] ^= 1;
     EXPECT_THROW(Crypto::DecryptAes256GCM(key, iv, aad, output), Crypto::OpensslError);
+
+    // Too short ciphertext
+    EXPECT_THROW(Crypto::DecryptAes256GCM(key, iv, aad, {0x1, 0x2, 0x3}), Crypto::OpensslError);
 }
 
 TEST(EncryptAes256GCM, EncryptEmptyPlaintextTestCase7)
diff --git a/tests/encrypted_tunnel_tests.cpp b/tests/encrypted_tunnel_tests.cpp
new file mode 100644 (file)
index 0000000..765166d
--- /dev/null
@@ -0,0 +1,188 @@
+/*
+ *  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/openssl_error.h"
+#include "crypto/random.h"
+#include "encrypted_tunnel.h"
+#include "test_cancel_from_the_other_thread.h"
+#include "tunnel.h"
+
+#include <gmock/gmock.h>
+#include <stdexcept>
+
+namespace {
+
+class MTunnel : public ITunnel {
+public:
+    void Connect(const std::string & /*url*/) override
+    {
+        throw std::runtime_error{"Connect() should not be called"};
+    }
+
+    void WriteBinary(const std::vector<uint8_t> &msg) override { m_lastWrittenMsg = msg; }
+
+    std::vector<uint8_t> ReadBinary() override
+    {
+        auto msg = std::move(m_nextMsgToRead);
+        m_nextMsgToRead.clear(); // Reset variable.
+        return msg;
+    }
+
+    void Disconnect() override {}
+
+    void Cancel() override { throw std::runtime_error{"Cancel() should not be called"}; }
+
+    CryptoBuffer m_lastWrittenMsg;
+    CryptoBuffer m_nextMsgToRead;
+};
+
+} // namespace
+
+TEST(EncryptedTunnel, works)
+{
+    auto reader = Crypto::Noise::SymmetricState::CipherState{
+        CryptoBuffer(Crypto::Noise::KEY_AND_HASH_LEN, 0x0)};
+    auto otherEndWriter = reader;
+    auto writer = Crypto::Noise::SymmetricState::CipherState{
+        CryptoBuffer(Crypto::Noise::KEY_AND_HASH_LEN, 0xab)};
+    auto otherEndReader = writer;
+
+    auto tunnelUPtr = std::make_unique<MTunnel>();
+    auto *tunnel = tunnelUPtr.get();
+    auto encryptedTunnel =
+        EncryptedTunnel(std::move(tunnelUPtr), std::move(reader), std::move(writer));
+
+    // Test writing
+    for (int i = 0; i < 0x123; ++i) {
+        auto msg = CryptoBuffer{static_cast<uint8_t>(i & 0xff)};
+        auto paddedMsg = msg;
+        paddedMsg.resize(32, 0);
+        paddedMsg.back() = 30;
+        encryptedTunnel.WriteBinary(msg);
+        EXPECT_EQ(otherEndReader.DecryptWithAd(tunnel->m_lastWrittenMsg, {}), paddedMsg);
+    }
+
+    // Values come from this implementation after verifying the Noise handshake and decrypting
+    // GetInfo message works with webauthn.io
+    EXPECT_EQ(
+        tunnel->m_lastWrittenMsg,
+        (CryptoBuffer{0x26, 0x8b, 0x2f, 0xf6, 0x19, 0x0c, 0xbf, 0x73, 0x92, 0x8a, 0xb4, 0x1a,
+                      0x2e, 0xb7, 0x3e, 0xd9, 0x3a, 0xd8, 0x15, 0xcb, 0x92, 0xc7, 0x65, 0x3f,
+                      0x46, 0x18, 0x62, 0xd7, 0xb2, 0x08, 0x8b, 0x9b, 0x9d, 0xd0, 0xf8, 0x5b,
+                      0x09, 0x95, 0x93, 0x04, 0xae, 0x0e, 0x91, 0x75, 0x82, 0xc0, 0xd5, 0x39}));
+
+    // Test reading and reading padding.
+    for (int blocks = 0; blocks < 10; ++blocks) {
+        for (int i = blocks * 32; i < (blocks + 1) * 32; ++i) {
+            auto msg = Crypto::RandomBytes(i);
+            auto paddedMsg = msg;
+            paddedMsg.resize((blocks + 1) * 32);
+            paddedMsg.back() = (blocks + 1) * 32 - 1 - i;
+            tunnel->m_nextMsgToRead = otherEndWriter.EncryptWithAd(paddedMsg, {});
+            EXPECT_EQ(encryptedTunnel.ReadBinary(), msg);
+        }
+    }
+
+    // Test writing and writting padding.
+    for (int blocks = 0; blocks < 10; ++blocks) {
+        for (int i = blocks * 32; i < (blocks + 1) * 32; ++i) {
+            auto msg = Crypto::RandomBytes(i);
+            auto paddedMsg = msg;
+            paddedMsg.resize((blocks + 1) * 32);
+            paddedMsg.back() = (blocks + 1) * 32 - 1 - i;
+            encryptedTunnel.WriteBinary(msg);
+            EXPECT_EQ(otherEndReader.DecryptWithAd(tunnel->m_lastWrittenMsg, {}), paddedMsg);
+        }
+    }
+}
+
+TEST(EncryptedTunnel, invalid_data)
+{
+    auto readerWriter = Crypto::Noise::SymmetricState::CipherState{
+        CryptoBuffer(Crypto::Noise::KEY_AND_HASH_LEN, 0x1)};
+
+    auto tunnelUPtr = std::make_unique<MTunnel>();
+    auto *tunnel = tunnelUPtr.get();
+    auto encryptedTunnel = EncryptedTunnel(std::move(tunnelUPtr), readerWriter, readerWriter);
+    // Reading: decryption error
+    tunnel->m_nextMsgToRead = {};
+    EXPECT_THROW(encryptedTunnel.ReadBinary(), Crypto::OpensslError);
+    tunnel->m_nextMsgToRead = CryptoBuffer(32, 0x0);
+    EXPECT_THROW(encryptedTunnel.ReadBinary(), Crypto::OpensslError);
+    // Reading: empty decrypted message
+    tunnel->m_nextMsgToRead = readerWriter.EncryptWithAd({}, {});
+    EXPECT_THAT([&] { encryptedTunnel.ReadBinary(); },
+                testing::ThrowsMessage<Unknown>(testing::HasSubstr("invalid message")));
+    // Reading: invalid paddingBytes
+    auto msg = CryptoBuffer(32, 0);
+    msg.back() = 32;
+    tunnel->m_nextMsgToRead = readerWriter.EncryptWithAd(msg, {});
+    EXPECT_THAT([&] { encryptedTunnel.ReadBinary(); },
+                testing::ThrowsMessage<Unknown>(testing::HasSubstr("invalid message")));
+
+    // Writing
+    EXPECT_THAT([&] { encryptedTunnel.WriteBinary(CryptoBuffer((1 << 20) + 1, 0)); },
+                testing::ThrowsMessage<Unknown>(testing::HasSubstr("message too large")));
+}
+
+namespace {
+
+class OTMEchoTunnel : public ITunnel {
+public:
+    void Connect(const std::string & /*url*/) override
+    {
+        throw std::runtime_error{"Connect() should not be called"};
+    }
+
+    void WriteBinary(const std::vector<uint8_t> &msg) override
+    {
+        m_cancelFacilitator.WithCancelCheck([&] { m_lastWrittenMsg = msg; });
+    }
+
+    std::vector<uint8_t> ReadBinary() override
+    {
+        std::vector<uint8_t> msg;
+        m_cancelFacilitator.WithCancelCheck([&] { msg = m_lastWrittenMsg; });
+        return msg;
+    }
+
+    void Disconnect() override {}
+
+    void Cancel() override { m_cancelFacilitator.Cancel(); }
+
+private:
+    CryptoBuffer m_lastWrittenMsg;
+    CancelFacilitator m_cancelFacilitator;
+};
+
+} // namespace
+
+TEST(EncryptedTunnel, cancel_from_the_other_thread)
+{
+    auto readerWriter = Crypto::Noise::SymmetricState::CipherState{
+        CryptoBuffer(Crypto::Noise::KEY_AND_HASH_LEN, 0x5f)};
+    auto makeEncryptedTunnel = [&] {
+        return EncryptedTunnel(std::make_unique<OTMEchoTunnel>(), readerWriter, readerWriter);
+    };
+    TestCancelFromTheOtherThread<IEncryptedTunnel>(
+        400, 40, makeEncryptedTunnel, [&](IEncryptedTunnel &encryptedTunnel) {
+            for (int i = 0; i < 2; ++i) {
+                auto msg = Crypto::RandomBytes(get_random(0, 100));
+                encryptedTunnel.WriteBinary(msg);
+                EXPECT_EQ(encryptedTunnel.ReadBinary(), msg);
+            }
+        });
+}
diff --git a/tests/handshake_tests.cpp b/tests/handshake_tests.cpp
new file mode 100644 (file)
index 0000000..f77043a
--- /dev/null
@@ -0,0 +1,239 @@
+/*
+ *  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 "bluetooth_advert.h"
+#include "common.h"
+#include "crypto/ec_key.h"
+#include "crypto/ecdh.h"
+#include "crypto/noise/noise.h"
+#include "crypto/random.h"
+#include "derive_key.h"
+#include "handshake.h"
+#include "lowercase_hex_string_of.h"
+#include "test_cancel_from_the_other_thread.h"
+#include "tunnel_server_domain.h"
+
+#include <stdexcept>
+
+namespace {
+
+CryptoBuffer GenerateDecryptedBluetoothAdvert()
+{
+    auto res = Crypto::RandomBytes(BLUETOOTH_ADVERT_LEN);
+    res[0] = 0;
+    res[res.size() - 2] = 0x01;
+    res.back() = 0;
+    assert(UnpackDecryptedAdvert(res).encodedTunnelServerDomain == 1);
+    return res;
+}
+
+class MTunnel : public ITunnel {
+public:
+    explicit MTunnel(const CryptoBuffer &qrSecret,
+                     const CryptoBuffer &compressedPlatformIdentityPublicKey,
+                     const CryptoBuffer &decryptedBleAdvert,
+                     const CryptoBuffer &getInfoMsg)
+    : m_qrSecret{qrSecret},
+      m_platformIdentityPublicKey{
+          Crypto::X9_62_P_256_Key::ImportPublicKey(compressedPlatformIdentityPublicKey)},
+      m_decryptedBleAdvert{decryptedBleAdvert},
+      m_getInfoMsg{getInfoMsg}
+    {
+    }
+
+    void Connect(const std::string &url) override
+    {
+        auto unpackedBleAdvert = UnpackDecryptedAdvert(m_decryptedBleAdvert);
+        auto tunnelId = DeriveKey(m_qrSecret, {}, KeyPurpose::TunnelID, TUNNEL_ID_LEN);
+
+        std::string expectedUrl = "wss://";
+        expectedUrl += DecodeTunnelServerDomain(unpackedBleAdvert.encodedTunnelServerDomain);
+        expectedUrl += "/cable/connect/";
+        expectedUrl += LowercaseHexStringOf(unpackedBleAdvert.routingId);
+        expectedUrl += '/';
+        expectedUrl += LowercaseHexStringOf(tunnelId);
+
+        EXPECT_EQ(url, expectedUrl);
+    }
+
+    void WriteBinary(const std::vector<uint8_t> &msg) override
+    {
+        // Process msg
+        Crypto::Noise::SymmetricState noise{
+            Crypto::Noise::SymmetricState::HandshakeKind::KNpsk0_P256_AESGCM_SHA256};
+        noise.MixHash(CryptoBuffer{1});
+        noise.MixHash(m_platformIdentityPublicKey.ExportPublicKey(false));
+
+        auto psk = DeriveKey(m_qrSecret, m_decryptedBleAdvert, KeyPurpose::PSK, PSK_LEN);
+        noise.MixKeyAndHash(psk);
+
+        auto exportedPublicKeyLen = m_platformIdentityPublicKey.ExportPublicKey(false).size();
+        ASSERT_GE(msg.size(), exportedPublicKeyLen);
+        auto platformEphermalPublicKeyBytes =
+            CryptoBuffer{msg.data(), msg.data() + exportedPublicKeyLen};
+
+        noise.MixHash(platformEphermalPublicKeyBytes);
+        noise.MixKey(platformEphermalPublicKeyBytes);
+
+        auto ciphertext = CryptoBuffer{msg.begin() + exportedPublicKeyLen, msg.end()};
+        auto plaintext = noise.DecryptAndHash(ciphertext);
+        ASSERT_TRUE(plaintext.empty());
+
+        // Prepare response
+        auto platformEphermalPublicKey =
+            Crypto::X9_62_P_256_Key::ImportPublicKey(platformEphermalPublicKeyBytes);
+
+        auto response = m_authenticatorEphermalKey.ExportPublicKey(false);
+        noise.MixHash(response);
+        noise.MixKey(response);
+
+        noise.MixKey(
+            Crypto::deriveECDHSharedSecret(m_authenticatorEphermalKey, platformEphermalPublicKey));
+        noise.MixKey(Crypto::deriveECDHSharedSecret(m_authenticatorEphermalKey,
+                                                    m_platformIdentityPublicKey));
+
+        ciphertext = noise.EncryptAndHash({});
+        response.insert(response.end(), ciphertext.begin(), ciphertext.end());
+        m_handshakeResponse = std::move(response);
+
+        // Prepare getInfoMsg
+        auto splitRes = noise.Split();
+        auto getInfoMsg = m_getInfoMsg;
+        constexpr size_t paddingGranularity = 32;
+        auto extraBytes = paddingGranularity - getInfoMsg.size() % paddingGranularity;
+        getInfoMsg.resize(getInfoMsg.size() + extraBytes, 0);
+        getInfoMsg.back() = static_cast<uint8_t>(extraBytes) - 1;
+
+        m_encryptedGetInfoMsg = splitRes.second.EncryptWithAd(getInfoMsg, {});
+    }
+
+    std::vector<uint8_t> ReadBinary() override
+    {
+        ++m_readNo;
+        if (m_readNo == 1)
+            return m_handshakeResponse;
+        if (m_readNo == 2)
+            return m_encryptedGetInfoMsg;
+        throw std::runtime_error{"Unexpected ReadBinary() call"};
+    }
+
+    void Disconnect() override
+    {
+        ++m_disconnectNo;
+        if (m_disconnectNo != 1)
+            throw std::runtime_error{"Disconnect() called more than once"};
+    }
+
+    void Cancel() override { throw std::runtime_error{"Unexpected Cancel() call"}; }
+
+private:
+    const CryptoBuffer &m_qrSecret;
+    Crypto::X9_62_P_256_Key m_platformIdentityPublicKey;
+    const CryptoBuffer &m_decryptedBleAdvert;
+    const CryptoBuffer &m_getInfoMsg;
+
+    Crypto::X9_62_P_256_Key m_authenticatorEphermalKey = Crypto::X9_62_P_256_Key::Create();
+    CryptoBuffer m_handshakeResponse;
+    CryptoBuffer m_encryptedGetInfoMsg;
+    int m_readNo = 0;
+    int m_disconnectNo = 0;
+};
+
+class HandshakeTest {
+public:
+    Handshake MakeHandshake()
+    {
+        return Handshake(std::make_unique<MTunnel>(
+            qrSecret, compressedIdentityPublicKey, decryptedBleAdvert, getInfoMsg));
+    }
+
+    void RunAndTestHandshake(IHandshake &handshake)
+    {
+        auto encryptedTunnel =
+            handshake.ConnectAndDoQrHandshake(qrSecret, decryptedBleAdvert, identityKey);
+        EXPECT_EQ(encryptedTunnel->ReadBinary(), getInfoMsg);
+    }
+
+protected:
+    CryptoBuffer qrSecret = Crypto::RandomBytes(QR_SECRET_LEN);
+    Crypto::X9_62_P_256_Key identityKey = Crypto::X9_62_P_256_Key::Create();
+    CryptoBuffer compressedIdentityPublicKey = identityKey.ExportPublicKey(true);
+    CryptoBuffer decryptedBleAdvert = GenerateDecryptedBluetoothAdvert();
+    CryptoBuffer getInfoMsg = Crypto::RandomBytes(get_random(0, 77));
+};
+
+} // namespace
+
+TEST(Handshake, works)
+{
+    HandshakeTest test;
+    auto handshake = test.MakeHandshake();
+    test.RunAndTestHandshake(handshake);
+}
+
+namespace {
+
+class OTMTunnel : public MTunnel {
+public:
+    using MTunnel::MTunnel;
+
+    void Connect(const std::string &url) override
+    {
+        m_cancelFacilitator.WithCancelCheck([&] { MTunnel::Connect(url); });
+    }
+
+    void WriteBinary(const std::vector<uint8_t> &msg) override
+    {
+        m_cancelFacilitator.WithCancelCheck([&] { MTunnel::WriteBinary(msg); });
+    }
+
+    std::vector<uint8_t> ReadBinary() override
+    {
+        std::vector<uint8_t> msg;
+        m_cancelFacilitator.WithCancelCheck([&] { msg = MTunnel::ReadBinary(); });
+        return msg;
+    }
+
+    void Disconnect() override
+    {
+        m_cancelFacilitator.WithCancelCheck([&] { MTunnel::Disconnect(); });
+    }
+
+    void Cancel() override { m_cancelFacilitator.Cancel(); }
+
+private:
+    CancelFacilitator m_cancelFacilitator;
+};
+
+class OTMHandshakeTest : public HandshakeTest {
+public:
+    Handshake MakeHandshake()
+    {
+        return Handshake(std::make_unique<OTMTunnel>(
+            qrSecret, compressedIdentityPublicKey, decryptedBleAdvert, getInfoMsg));
+    }
+};
+
+} // namespace
+
+TEST(Handshake, cancel_from_the_other_thread)
+{
+    OTMHandshakeTest test;
+    auto makeHandshake = [&] { return test.MakeHandshake(); };
+    TestCancelFromTheOtherThread<IHandshake>(200, 20, makeHandshake, [&](IHandshake &handshake) {
+        test.RunAndTestHandshake(handshake);
+    });
+}
index e8148081b6c9d639e99e079974763c6f96067bf8..9b0b8b5b8db614e81ccf2d4967499847dcd1e8f0 100644 (file)
@@ -27,12 +27,18 @@ enum class Event : int {
     QR_CODE_ENDED,
     BLE_ADVERT_STARTED,
     BLE_ADVERT_ENDED,
+    HANDSHAKE_STARTED,
+    HANDSHAKE_ENDED,
+    READING_FROM_ENCRYPTED_TUNNEL_STARTED,
+    READING_FROM_ENCRYPTED_TUNNEL_ENDED,
     FINISHED,
 };
 
 enum class CancelCalledOn : int {
     NONE,
     BLE_ADVERT,
+    HANDSHAKE,
+    ENCRYPTED_TUNNEL,
 };
 
 using TestState = TransactionTestState<Event, CancelCalledOn>;
@@ -58,11 +64,10 @@ class MBtAdvertScanner : public IBtAdvertScanner, public Tester {
 public:
     explicit MBtAdvertScanner(TestState &testState) : Tester{testState} {}
 
-    int AwaitAdvert(const CryptoBuffer & /*eidKey*/, CryptoBuffer &decryptedAdvert) override
+    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;
     }
 
@@ -74,6 +79,52 @@ public:
     }
 };
 
+class MEncryptedTunnel : public IEncryptedTunnel, public Tester {
+public:
+    explicit MEncryptedTunnel(TestState &testState) : Tester{testState} {}
+
+    CryptoBuffer ReadBinary() override
+    {
+        SCOPED_TRACE("");
+        UpdateAndCheckState(Event::READING_FROM_ENCRYPTED_TUNNEL_STARTED,
+                            Event::READING_FROM_ENCRYPTED_TUNNEL_ENDED,
+                            true);
+        return {};
+    }
+
+    void WriteBinary(CryptoBuffer /*msg*/) override
+    {
+        throw std::runtime_error{"Should not be called"};
+    }
+
+    void Cancel() override
+    {
+        SCOPED_TRACE("");
+        CheckCancel(CancelCalledOn::ENCRYPTED_TUNNEL);
+    }
+};
+
+class MHandshake : public IHandshake, public Tester {
+public:
+    explicit MHandshake(TestState &testState) : Tester{testState} {}
+
+    std::unique_ptr<IEncryptedTunnel>
+    ConnectAndDoQrHandshake(const CryptoBuffer & /*qrSecret*/,
+                            const CryptoBuffer & /*decryptedBleAdvert*/,
+                            const Crypto::X9_62_P_256_Key & /*identityKey*/) override
+    {
+        SCOPED_TRACE("");
+        UpdateAndCheckState(Event::HANDSHAKE_STARTED, Event::HANDSHAKE_ENDED, true);
+        return std::make_unique<MEncryptedTunnel>(m_testState);
+    }
+
+    void Cancel() override
+    {
+        SCOPED_TRACE("");
+        CheckCancel(CancelCalledOn::HANDSHAKE);
+    }
+};
+
 } // namespace
 
 TEST(QrTransaction, Cancel)
@@ -84,7 +135,8 @@ TEST(QrTransaction, Cancel)
                              nullptr,
                              {},
                              std::make_unique<MQrCodeShower>(testState),
-                             std::make_unique<MBtAdvertScanner>(testState));
+                             std::make_unique<MBtAdvertScanner>(testState),
+                             std::make_unique<MHandshake>(testState));
     };
     // Before PerformTransaction()
     {
@@ -128,12 +180,44 @@ TEST(QrTransaction, Cancel)
         EXPECT_EQ(testState.m_lastEvent, Event::BLE_ADVERT_ENDED);
         EXPECT_EQ(testState.m_cancelCalledOn, CancelCalledOn::BLE_ADVERT);
     }
+    // In MHandshake
+    {
+        auto transaction = makeTransaction();
+        testState.Reset(Event::HANDSHAKE_STARTED, transaction);
+        EXPECT_THROW(transaction.PerformTransaction(), Cancelled);
+        EXPECT_EQ(testState.m_lastEvent, Event::HANDSHAKE_STARTED);
+        EXPECT_EQ(testState.m_cancelCalledOn, CancelCalledOn::HANDSHAKE);
+    }
+    // After MHandshake
+    {
+        auto transaction = makeTransaction();
+        testState.Reset(Event::HANDSHAKE_ENDED, transaction);
+        EXPECT_THROW(transaction.PerformTransaction(), Cancelled);
+        EXPECT_EQ(testState.m_lastEvent, Event::HANDSHAKE_ENDED);
+        EXPECT_EQ(testState.m_cancelCalledOn, CancelCalledOn::HANDSHAKE);
+    }
+    // In MEncryptedTunnel
+    {
+        auto transaction = makeTransaction();
+        testState.Reset(Event::READING_FROM_ENCRYPTED_TUNNEL_STARTED, transaction);
+        EXPECT_THROW(transaction.PerformTransaction(), Cancelled);
+        EXPECT_EQ(testState.m_lastEvent, Event::READING_FROM_ENCRYPTED_TUNNEL_STARTED);
+        EXPECT_EQ(testState.m_cancelCalledOn, CancelCalledOn::ENCRYPTED_TUNNEL);
+    }
+    // After MEncryptedTunnel
+    {
+        auto transaction = makeTransaction();
+        testState.Reset(Event::READING_FROM_ENCRYPTED_TUNNEL_ENDED, transaction);
+        EXPECT_THROW(transaction.PerformTransaction(), Cancelled);
+        EXPECT_EQ(testState.m_lastEvent, Event::READING_FROM_ENCRYPTED_TUNNEL_ENDED);
+        EXPECT_EQ(testState.m_cancelCalledOn, CancelCalledOn::ENCRYPTED_TUNNEL);
+    }
     // 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_lastEvent, Event::READING_FROM_ENCRYPTED_TUNNEL_ENDED);
         EXPECT_EQ(testState.m_cancelCalledOn, CancelCalledOn::NONE);
     }
 }
@@ -155,11 +239,10 @@ public:
 
 class OTMBtAdvertScanner : public IBtAdvertScanner {
 public:
-    int AwaitAdvert(const CryptoBuffer & /*eidKey*/, CryptoBuffer &decryptedAdvert) override
+    int AwaitAdvert(const CryptoBuffer & /*eidKey*/, CryptoBuffer & /*decryptedAdvert*/) override
     {
         auto res = BT_ERROR_NONE;
-        m_cancelFacilitator.WithCancelCheck([&] { decryptedAdvert.assign(16, 0); },
-                                            [&] { res = BT_ERROR_CANCELLED; });
+        m_cancelFacilitator.WithCancelCheck([] {}, [&] { res = BT_ERROR_CANCELLED; });
         return res;
     }
 
@@ -173,6 +256,38 @@ private:
     CancelFacilitator m_cancelFacilitator;
 };
 
+class OTMEncryptedTunnel : public IEncryptedTunnel {
+    CryptoBuffer ReadBinary() override
+    {
+        m_cancelFacilitator.CancelCheck();
+        return {};
+    }
+
+    void WriteBinary(CryptoBuffer /*msg*/) override { m_cancelFacilitator.CancelCheck(); }
+
+    void Cancel() override { m_cancelFacilitator.Cancel(); }
+
+private:
+    CancelFacilitator m_cancelFacilitator;
+};
+
+class OTMHandshake : public IHandshake {
+public:
+    std::unique_ptr<IEncryptedTunnel>
+    ConnectAndDoQrHandshake(const CryptoBuffer & /*qrSecret*/,
+                            const CryptoBuffer & /*decryptedBleAdvert*/,
+                            const Crypto::X9_62_P_256_Key & /*identityKey*/) override
+    {
+        m_cancelFacilitator.CancelCheck();
+        return std::make_unique<OTMEncryptedTunnel>();
+    }
+
+    void Cancel() override { m_cancelFacilitator.Cancel(); }
+
+private:
+    CancelFacilitator m_cancelFacilitator;
+};
+
 } // namespace
 
 TEST(QrTransaction, cancel_from_the_other_thread)
@@ -182,7 +297,8 @@ TEST(QrTransaction, cancel_from_the_other_thread)
                              nullptr,
                              {},
                              std::make_unique<OTMQrCodeShower>(),
-                             std::make_unique<OTMBtAdvertScanner>());
+                             std::make_unique<OTMBtAdvertScanner>(),
+                             std::make_unique<OTMHandshake>());
     };
     TestCancelFromTheOtherThread<ITransaction>(
         400, 40, makeTransaction, [](ITransaction &transaction) {
index bce41cbbe7c6a638f4c8f1d3f199b5ec245dca74..7582a260c9a40a36a78d56dbf60aaeed51cbd18a 100644 (file)
@@ -94,6 +94,6 @@ public:
         m_testState.m_cancelCalledOn = cancelCalledOn;
     }
 
-private:
+protected:
     TransactionTestState<Event, CancelCalledOn> &m_testState;
 };