| // Copyright 2022 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include <vector> |
| |
| #include "base/base_switches.h" |
| #include "base/check_deref.h" |
| #include "base/functional/bind.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/test/gmock_callback_support.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "base/test/test_future.h" |
| #include "content/browser/smart_card/mock_smart_card_context_factory.h" |
| #include "content/browser/smart_card/smart_card_histograms.h" |
| #include "content/public/browser/browser_context.h" |
| #include "content/public/browser/content_browser_client.h" |
| #include "content/public/browser/smart_card_delegate.h" |
| #include "content/public/common/content_switches.h" |
| #include "content/public/test/browser_test.h" |
| #include "content/public/test/browser_test_utils.h" |
| #include "content/public/test/content_browser_test.h" |
| #include "content/public/test/content_browser_test_content_browser_client.h" |
| #include "content/public/test/content_browser_test_utils.h" |
| #include "content/public/test/content_mock_cert_verifier.h" |
| #include "content/public/test/test_utils.h" |
| #include "content/shell/browser/shell.h" |
| #include "mojo/public/cpp/bindings/remote.h" |
| #include "mojo/public/cpp/bindings/self_owned_receiver.h" |
| #include "net/dns/mock_host_resolver.h" |
| #include "net/test/embedded_test_server/default_handlers.h" |
| #include "net/test/embedded_test_server/embedded_test_server.h" |
| #include "services/device/public/mojom/smart_card.mojom.h" |
| #include "services/network/public/cpp/permissions_policy/permissions_policy_declaration.h" |
| #include "services/network/public/mojom/permissions_policy/permissions_policy_feature.mojom-shared.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/blink/public/common/features_generated.h" |
| #include "third_party/blink/public/mojom/smart_card/smart_card.mojom.h" |
| #include "url/gurl.h" |
| #include "url/origin.h" |
| |
| using base::test::RunOnceCallback; |
| using base::test::TestFuture; |
| using device::mojom::SmartCardConnection; |
| using device::mojom::SmartCardConnectionState; |
| using device::mojom::SmartCardContext; |
| using device::mojom::SmartCardDisposition; |
| using device::mojom::SmartCardError; |
| using device::mojom::SmartCardListReadersResult; |
| using device::mojom::SmartCardProtocol; |
| using device::mojom::SmartCardReaderStateFlags; |
| using device::mojom::SmartCardReaderStateOut; |
| using device::mojom::SmartCardReaderStateOutPtr; |
| using device::mojom::SmartCardResult; |
| using device::mojom::SmartCardShareMode; |
| using device::mojom::SmartCardStatus; |
| using device::mojom::SmartCardSuccess; |
| using device::mojom::SmartCardTransaction; |
| using ::testing::_; |
| using testing::Exactly; |
| using testing::HasSubstr; |
| using testing::InSequence; |
| using testing::MatchesRegex; |
| using testing::Return; |
| using testing::SaveArg; |
| using testing::StrictMock; |
| using testing::WithArg; |
| using testing::WithArgs; |
| using testing::WithoutArgs; |
| |
| namespace content { |
| |
| namespace { |
| |
| constexpr char kFakeReader[] = "Fake reader"; |
| |
| class MockSmartCardConnection : public device::mojom::SmartCardConnection { |
| public: |
| MOCK_METHOD(void, |
| Disconnect, |
| (SmartCardDisposition disposition, DisconnectCallback callback), |
| (override)); |
| |
| MOCK_METHOD(void, |
| Transmit, |
| (device::mojom::SmartCardProtocol protocol, |
| const std::vector<uint8_t>& data, |
| TransmitCallback callback), |
| (override)); |
| |
| MOCK_METHOD(void, |
| Control, |
| (uint32_t control_code, |
| const std::vector<uint8_t>& data, |
| ControlCallback callback), |
| (override)); |
| |
| MOCK_METHOD(void, |
| GetAttrib, |
| (uint32_t id, GetAttribCallback callback), |
| (override)); |
| |
| MOCK_METHOD(void, |
| SetAttrib, |
| (uint32_t id, |
| const std::vector<uint8_t>& data, |
| SetAttribCallback callback), |
| (override)); |
| |
| MOCK_METHOD(void, Status, (StatusCallback callback), (override)); |
| |
| MOCK_METHOD(void, |
| BeginTransaction, |
| (BeginTransactionCallback callback), |
| (override)); |
| |
| void ExpectBeginTransaction( |
| mojo::AssociatedReceiver<SmartCardTransaction>& transaction_receiver) { |
| EXPECT_CALL(*this, BeginTransaction(_)) |
| .WillOnce([&transaction_receiver]( |
| SmartCardConnection::BeginTransactionCallback callback) { |
| std::move(callback).Run( |
| device::mojom::SmartCardTransactionResult::NewTransaction( |
| transaction_receiver.BindNewEndpointAndPassRemote())); |
| }); |
| } |
| }; |
| |
| class MockSmartCardTransaction : public SmartCardTransaction { |
| public: |
| MOCK_METHOD(void, |
| EndTransaction, |
| (SmartCardDisposition disposition, |
| EndTransactionCallback callback), |
| (override)); |
| |
| void ExpectEndTransaction(SmartCardDisposition disposition) { |
| EXPECT_CALL(*this, EndTransaction(disposition, _)) |
| .WillOnce([](SmartCardDisposition disposition, |
| SmartCardTransaction::EndTransactionCallback callback) { |
| std::move(callback).Run( |
| SmartCardResult::NewSuccess(SmartCardSuccess::kOk)); |
| }); |
| } |
| }; |
| |
| class FakeSmartCardDelegate : public SmartCardDelegate { |
| public: |
| FakeSmartCardDelegate() = default; |
| // SmartCardDelegate overrides: |
| mojo::PendingRemote<device::mojom::SmartCardContextFactory> |
| GetSmartCardContextFactory(BrowserContext& browser_context) override; |
| |
| MOCK_METHOD(bool, |
| IsPermissionBlocked, |
| (RenderFrameHost & render_frame_host), |
| (override)); |
| |
| MOCK_METHOD(bool, |
| HasReaderPermission, |
| (content::RenderFrameHost & render_frame_host, |
| const std::string& reader_name), |
| (override)); |
| |
| MOCK_METHOD(void, |
| RequestReaderPermission, |
| (content::RenderFrameHost & render_frame_host, |
| const std::string& reader_name, |
| RequestReaderPermissionCallback callback), |
| (override)); |
| |
| MOCK_METHOD(void, |
| NotifyConnectionUsed, |
| (content::RenderFrameHost & render_frame_host), |
| (override)); |
| |
| MOCK_METHOD(void, |
| NotifyLastConnectionLost, |
| (content::RenderFrameHost & render_frame_host), |
| (override)); |
| |
| MOCK_METHOD(void, |
| AddObserver, |
| (content::RenderFrameHost & render_frame_host, |
| PermissionObserver* observer), |
| (override)); |
| |
| MOCK_METHOD(void, |
| RemoveObserver, |
| (content::RenderFrameHost & render_frame_host, |
| PermissionObserver* observer), |
| (override)); |
| |
| void ExpectHasReaderPermission(const std::string& reader_name) { |
| EXPECT_CALL(*this, HasReaderPermission(_, reader_name)) |
| .WillOnce(Return(true)); |
| } |
| |
| void ExpectAddObserver() { EXPECT_CALL(*this, AddObserver); } |
| |
| void ExpectRemoveObserver() { EXPECT_CALL(*this, RemoveObserver); } |
| |
| MockSmartCardContextFactory mock_context_factory; |
| }; |
| |
| class SmartCardTestContentBrowserClient |
| : public ContentBrowserTestContentBrowserClient { |
| public: |
| SmartCardTestContentBrowserClient(); |
| SmartCardTestContentBrowserClient(SmartCardTestContentBrowserClient&) = |
| delete; |
| SmartCardTestContentBrowserClient& operator=( |
| SmartCardTestContentBrowserClient&) = delete; |
| ~SmartCardTestContentBrowserClient() override; |
| |
| void SetSmartCardDelegate(std::unique_ptr<SmartCardDelegate>); |
| |
| // ContentBrowserClient: |
| SmartCardDelegate* GetSmartCardDelegate() override; |
| bool ShouldUrlUseApplicationIsolationLevel(BrowserContext* browser_context, |
| const GURL& url) override; |
| std::optional<network::ParsedPermissionsPolicy> |
| GetPermissionsPolicyForIsolatedWebApp(WebContents* web_contents, |
| const url::Origin& app_origin) override; |
| |
| private: |
| std::unique_ptr<SmartCardDelegate> delegate_; |
| }; |
| |
| class SmartCardTest : public ContentBrowserTest { |
| public: |
| GURL GetIsolatedContextUrl() { |
| return embedded_https_test_server().GetURL( |
| "a.com", |
| "/set-header?Cross-Origin-Opener-Policy: same-origin&" |
| "Cross-Origin-Embedder-Policy: require-corp&" |
| "Permissions-Policy: smart-card%3D(self)"); |
| } |
| |
| FakeSmartCardDelegate& GetFakeSmartCardDelegate() { |
| return *static_cast<FakeSmartCardDelegate*>( |
| test_client_->GetSmartCardDelegate()); |
| } |
| |
| void TestEmptyTransaction(std::string expected_result, |
| std::string transaction_callback) { |
| ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl())); |
| |
| MockSmartCardContextFactory& mock_context_factory = |
| GetFakeSmartCardDelegate().mock_context_factory; |
| MockSmartCardConnection mock_connection; |
| mojo::Receiver<SmartCardConnection> connection_receiver(&mock_connection); |
| |
| MockSmartCardTransaction mock_transaction; |
| mojo::AssociatedReceiver<SmartCardTransaction> transaction_receiver( |
| &mock_transaction); |
| |
| { |
| InSequence s; |
| |
| auto& delegate = GetFakeSmartCardDelegate(); |
| delegate.ExpectAddObserver(); |
| delegate.ExpectHasReaderPermission(kFakeReader); |
| mock_context_factory.ExpectConnectFakeReaderSharedT1(connection_receiver); |
| mock_connection.ExpectBeginTransaction(transaction_receiver); |
| mock_transaction.ExpectEndTransaction(SmartCardDisposition::kReset); |
| delegate.ExpectRemoveObserver(); |
| } |
| |
| std::string js_snippet = base::StringPrintf(R"( |
| (async () => { |
| let context = await navigator.smartCard.establishContext(); |
| |
| let connection = |
| (await context.connect("Fake reader", "shared", |
| {preferredProtocols: ["t1"]})).connection; |
| |
| let transaction = %s; |
| |
| let transactionPromise = connection.startTransaction(transaction); |
| try { |
| await transactionPromise; |
| } catch (e) { |
| return `startTransaction: ${e.name}, ${e.message}`; |
| } |
| |
| return "ok"; |
| })())", |
| transaction_callback.c_str()); |
| |
| EXPECT_EQ(expected_result, EvalJs(shell(), js_snippet)); |
| } |
| |
| private: |
| void SetUpOnMainThread() override { |
| ContentBrowserTest::SetUpOnMainThread(); |
| |
| test_client_ = std::make_unique<SmartCardTestContentBrowserClient>(); |
| test_client_->SetSmartCardDelegate( |
| std::make_unique<FakeSmartCardDelegate>()); |
| |
| // Serve a.com (and any other domain). |
| host_resolver()->AddRule("*", "127.0.0.1"); |
| |
| // Add a handler for the "/set-header" page (among others) |
| embedded_https_test_server().AddDefaultHandlers(GetTestDataFilePath()); |
| |
| ASSERT_TRUE(embedded_https_test_server().Start()); |
| } |
| |
| void SetUpInProcessBrowserTestFixture() override { |
| ContentBrowserTest::SetUpInProcessBrowserTestFixture(); |
| } |
| |
| void TearDownInProcessBrowserTestFixture() override { |
| ContentBrowserTest::TearDownInProcessBrowserTestFixture(); |
| } |
| |
| void TearDown() override { |
| ASSERT_TRUE(embedded_https_test_server().ShutdownAndWaitUntilComplete()); |
| ContentBrowserTest::TearDown(); |
| } |
| |
| std::unique_ptr<SmartCardTestContentBrowserClient> test_client_; |
| |
| base::test::ScopedFeatureList scoped_feature_list_{ |
| blink::features::kSmartCard}; |
| |
| protected: |
| base::HistogramTester histogram_tester_; |
| }; |
| } // namespace |
| |
| SmartCardTestContentBrowserClient::SmartCardTestContentBrowserClient() = |
| default; |
| |
| SmartCardTestContentBrowserClient::~SmartCardTestContentBrowserClient() = |
| default; |
| |
| SmartCardDelegate* SmartCardTestContentBrowserClient::GetSmartCardDelegate() { |
| return delegate_.get(); |
| } |
| |
| void SmartCardTestContentBrowserClient::SetSmartCardDelegate( |
| std::unique_ptr<SmartCardDelegate> delegate) { |
| delegate_ = std::move(delegate); |
| } |
| |
| bool SmartCardTestContentBrowserClient::ShouldUrlUseApplicationIsolationLevel( |
| BrowserContext* browser_context, |
| const GURL& url) { |
| return true; |
| } |
| |
| std::optional<network::ParsedPermissionsPolicy> |
| SmartCardTestContentBrowserClient::GetPermissionsPolicyForIsolatedWebApp( |
| WebContents* web_contents, |
| const url::Origin& app_origin) { |
| network::ParsedPermissionsPolicyDeclaration coi_decl( |
| network::mojom::PermissionsPolicyFeature::kCrossOriginIsolated, |
| /*allowed_origins=*/{}, |
| /*self_if_matches=*/std::nullopt, /*matches_all_origins=*/true, |
| /*matches_opaque_src=*/false); |
| network::ParsedPermissionsPolicyDeclaration smart_card_decl( |
| network::mojom::PermissionsPolicyFeature::kSmartCard, |
| /*allowed_origins=*/{}, |
| /*self_if_matches=*/app_origin, /*matches_all_origins=*/false, |
| /*matches_opaque_src=*/false); |
| return {{coi_decl, smart_card_decl}}; |
| } |
| |
| mojo::PendingRemote<device::mojom::SmartCardContextFactory> |
| FakeSmartCardDelegate::GetSmartCardContextFactory( |
| BrowserContext& browser_context) { |
| return mock_context_factory.GetRemote(); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(SmartCardTest, Disconnect) { |
| ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl())); |
| |
| MockSmartCardContextFactory& mock_context_factory = |
| GetFakeSmartCardDelegate().mock_context_factory; |
| MockSmartCardConnection mock_connection; |
| mojo::Receiver<SmartCardConnection> connection_receiver(&mock_connection); |
| |
| { |
| InSequence s; |
| |
| GetFakeSmartCardDelegate().ExpectHasReaderPermission(kFakeReader); |
| |
| mock_context_factory.ExpectConnectFakeReaderSharedT1(connection_receiver); |
| |
| EXPECT_CALL(mock_connection, Disconnect(SmartCardDisposition::kEject, _)) |
| .WillOnce([](SmartCardDisposition disposition, |
| SmartCardConnection::DisconnectCallback callback) { |
| std::move(callback).Run( |
| SmartCardResult::NewSuccess(SmartCardSuccess::kOk)); |
| }); |
| } |
| |
| EXPECT_EQ( |
| "second disconnect: InvalidStateError, Failed to execute 'disconnect' on " |
| "'SmartCardConnection': Is disconnected.", |
| EvalJs(shell(), R"( |
| (async () => { |
| let context = await navigator.smartCard.establishContext(); |
| |
| let connection = |
| (await context.connect("Fake reader", "shared", |
| {preferredProtocols: ["t1"]})).connection; |
| |
| await connection.disconnect("eject"); |
| |
| // A second attempt should fail. |
| try { |
| await connection.disconnect("unpower"); |
| } catch (e) { |
| return `second disconnect: ${e.name}, ${e.message}`; |
| } |
| |
| return `second disconnect did not throw`; |
| })())")); |
| |
| content::FetchHistogramsFromChildProcesses(); |
| histogram_tester_.ExpectUniqueSample( |
| "SmartCard.ConnectionClosedReason", |
| SmartCardConnectionClosedReason::kSmartCardConnectionClosedDisconnect, 1); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(SmartCardTest, LastConnectionClosed) { |
| ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl())); |
| |
| MockSmartCardContextFactory& mock_context_factory = |
| GetFakeSmartCardDelegate().mock_context_factory; |
| MockSmartCardConnection mock_connection1; |
| MockSmartCardConnection mock_connection2; |
| mojo::Receiver<SmartCardConnection> connection_receiver1(&mock_connection1); |
| mojo::Receiver<SmartCardConnection> connection_receiver2(&mock_connection2); |
| mojo::Remote<device::mojom::SmartCardConnectionWatcher> watcher1; |
| mojo::Remote<device::mojom::SmartCardConnectionWatcher> watcher2; |
| |
| { |
| InSequence s; |
| |
| auto& delegate = GetFakeSmartCardDelegate(); |
| |
| delegate.ExpectHasReaderPermission(kFakeReader); |
| EXPECT_CALL(mock_context_factory, |
| Connect("Fake reader", SmartCardShareMode::kShared, _, _, _)) |
| .WillOnce(WithArgs<3, 4>( |
| [&connection_receiver1, &watcher1]( |
| mojo::PendingRemote<device::mojom::SmartCardConnectionWatcher> |
| connection_watcher, |
| SmartCardContext::ConnectCallback callback) { |
| watcher1.Bind(std::move(connection_watcher)); |
| |
| auto success = device::mojom::SmartCardConnectSuccess::New( |
| connection_receiver1.BindNewPipeAndPassRemote(), |
| SmartCardProtocol::kT1); |
| |
| std::move(callback).Run( |
| device::mojom::SmartCardConnectResult::NewSuccess( |
| std::move(success))); |
| })); |
| |
| delegate.ExpectHasReaderPermission(kFakeReader); |
| EXPECT_CALL(mock_context_factory, |
| Connect("Fake reader", SmartCardShareMode::kShared, _, _, _)) |
| .WillOnce(WithArgs<3, 4>( |
| [&connection_receiver2, &watcher2]( |
| mojo::PendingRemote<device::mojom::SmartCardConnectionWatcher> |
| connection_watcher, |
| SmartCardContext::ConnectCallback callback) { |
| watcher2.Bind(std::move(connection_watcher)); |
| |
| auto success = device::mojom::SmartCardConnectSuccess::New( |
| connection_receiver2.BindNewPipeAndPassRemote(), |
| SmartCardProtocol::kT1); |
| |
| std::move(callback).Run( |
| device::mojom::SmartCardConnectResult::NewSuccess( |
| std::move(success))); |
| })); |
| |
| EXPECT_CALL(mock_connection1, Disconnect(SmartCardDisposition::kEject, _)) |
| .WillOnce(WithArg<1>( |
| [&watcher1](SmartCardConnection::DisconnectCallback callback) { |
| watcher1.reset(); |
| std::move(callback).Run( |
| SmartCardResult::NewSuccess(SmartCardSuccess::kOk)); |
| })); |
| |
| EXPECT_CALL(mock_connection2, Disconnect(SmartCardDisposition::kEject, _)) |
| .WillOnce(WithArg<1>( |
| [&watcher2](SmartCardConnection::DisconnectCallback callback) { |
| watcher2.reset(); |
| std::move(callback).Run( |
| SmartCardResult::NewSuccess(SmartCardSuccess::kOk)); |
| })); |
| |
| EXPECT_CALL(delegate, NotifyLastConnectionLost).Times(1); |
| } |
| |
| EXPECT_TRUE(ExecJs(shell(), R"( |
| (async () => { |
| let context = await navigator.smartCard.establishContext(); |
| |
| let connection1 = |
| (await context.connect("Fake reader", "shared", |
| {preferredProtocols: ["t1"]})).connection; |
| |
| let connection2 = |
| (await context.connect("Fake reader", "shared", |
| {preferredProtocols: ["t1"]})).connection; |
| |
| await connection1.disconnect("eject"); |
| await connection2.disconnect("eject"); |
| })())")); |
| content::FetchHistogramsFromChildProcesses(); |
| histogram_tester_.ExpectUniqueSample( |
| "SmartCard.ConnectionClosedReason", |
| SmartCardConnectionClosedReason::kSmartCardConnectionClosedDisconnect, 2); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(SmartCardTest, NotifyConnectionUsed) { |
| ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl())); |
| |
| MockSmartCardContextFactory& mock_context_factory = |
| GetFakeSmartCardDelegate().mock_context_factory; |
| MockSmartCardConnection mock_connection; |
| mojo::Receiver<SmartCardConnection> connection_receiver(&mock_connection); |
| mojo::Remote<device::mojom::SmartCardConnectionWatcher> watcher; |
| |
| { |
| InSequence s; |
| |
| auto& delegate = GetFakeSmartCardDelegate(); |
| |
| delegate.ExpectHasReaderPermission(kFakeReader); |
| EXPECT_CALL(mock_context_factory, |
| Connect("Fake reader", SmartCardShareMode::kShared, _, _, _)) |
| .WillOnce(WithArgs<3, 4>( |
| [&connection_receiver, &watcher]( |
| mojo::PendingRemote<device::mojom::SmartCardConnectionWatcher> |
| connection_watcher, |
| SmartCardContext::ConnectCallback callback) { |
| watcher.Bind(std::move(connection_watcher)); |
| |
| auto success = device::mojom::SmartCardConnectSuccess::New( |
| connection_receiver.BindNewPipeAndPassRemote(), |
| SmartCardProtocol::kT1); |
| |
| std::move(callback).Run( |
| device::mojom::SmartCardConnectResult::NewSuccess( |
| std::move(success))); |
| })); |
| |
| EXPECT_CALL(mock_connection, Transmit) |
| .WillOnce([&watcher](SmartCardProtocol protocol, |
| const std::vector<uint8_t>& data, |
| SmartCardConnection::TransmitCallback callback) { |
| ASSERT_TRUE(watcher.is_bound()); |
| watcher->NotifyConnectionUsed(); |
| std::move(callback).Run( |
| device::mojom::SmartCardDataResult::NewData({12u, 34u})); |
| }); |
| |
| EXPECT_CALL(delegate, NotifyConnectionUsed).Times(1); |
| |
| EXPECT_CALL(mock_connection, Disconnect(SmartCardDisposition::kEject, _)) |
| .WillOnce(WithArg<1>( |
| [&watcher](SmartCardConnection::DisconnectCallback callback) { |
| watcher.reset(); |
| std::move(callback).Run( |
| SmartCardResult::NewSuccess(SmartCardSuccess::kOk)); |
| })); |
| |
| EXPECT_CALL(delegate, NotifyLastConnectionLost).Times(1); |
| } |
| |
| EXPECT_TRUE(ExecJs(shell(), R"( |
| (async () => { |
| let context = await navigator.smartCard.establishContext(); |
| |
| let connection = |
| (await context.connect("Fake reader", "shared", |
| {preferredProtocols: ["t1"]})).connection; |
| |
| let apdu = new Uint8Array([0x03, 0x02, 0x01]); |
| await connection.transmit(apdu); |
| |
| await connection.disconnect("eject"); |
| })())")); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(SmartCardTest, ConcurrentDisconnect) { |
| ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl())); |
| |
| MockSmartCardContextFactory& mock_context_factory = |
| GetFakeSmartCardDelegate().mock_context_factory; |
| MockSmartCardConnection mock_connection; |
| mojo::Receiver<SmartCardConnection> connection_receiver(&mock_connection); |
| |
| TestFuture<SmartCardConnection::DisconnectCallback> disconnect_future; |
| |
| { |
| InSequence s; |
| |
| GetFakeSmartCardDelegate().ExpectHasReaderPermission(kFakeReader); |
| |
| mock_context_factory.ExpectConnectFakeReaderSharedT1(connection_receiver); |
| |
| EXPECT_CALL(mock_connection, Disconnect(SmartCardDisposition::kEject, _)) |
| .WillOnce([&disconnect_future]( |
| SmartCardDisposition disposition, |
| SmartCardConnection::DisconnectCallback callback) { |
| // Ensure this disconnect() call doesn't finish before the second |
| // one is issued. |
| disconnect_future.SetValue(std::move(callback)); |
| }); |
| } |
| |
| EXPECT_EQ( |
| "second disconnect: InvalidStateError, Failed to execute 'disconnect' on " |
| "'SmartCardConnection': An operation is already in progress in this " |
| "smart card context.", |
| EvalJs(shell(), R"( |
| (async () => { |
| let context = await navigator.smartCard.establishContext(); |
| |
| let connection = |
| (await context.connect("Fake reader", "shared", |
| {preferredProtocols: ["t1"]})).connection; |
| |
| // This first disconnect() call will go through but won't be finished |
| // before the end of this script. |
| connection.disconnect("eject"); |
| |
| // A second attempt should fail since the first one is still ongoing. |
| try { |
| await connection.disconnect("unpower"); |
| } catch (e) { |
| return `second disconnect: ${e.name}, ${e.message}`; |
| } |
| |
| return `second disconnect did not throw`; |
| })())")); |
| |
| // Let the first disconnect() finish. |
| disconnect_future.Take().Run( |
| SmartCardResult::NewSuccess(SmartCardSuccess::kOk)); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(SmartCardTest, Transmit) { |
| ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl())); |
| |
| MockSmartCardContextFactory& mock_context_factory = |
| GetFakeSmartCardDelegate().mock_context_factory; |
| MockSmartCardConnection mock_connection; |
| mojo::Receiver<SmartCardConnection> connection_receiver(&mock_connection); |
| |
| { |
| InSequence s; |
| |
| GetFakeSmartCardDelegate().ExpectHasReaderPermission(kFakeReader); |
| |
| mock_context_factory.ExpectConnectFakeReaderSharedT1(connection_receiver); |
| |
| EXPECT_CALL(mock_connection, Transmit(SmartCardProtocol::kT1, _, _)) |
| .WillOnce([](SmartCardProtocol protocol, |
| const std::vector<uint8_t>& data, |
| SmartCardConnection::TransmitCallback callback) { |
| EXPECT_EQ(data, std::vector<uint8_t>({3u, 2u, 1u})); |
| std::move(callback).Run( |
| device::mojom::SmartCardDataResult::NewData({12u, 34u})); |
| }); |
| } |
| |
| EXPECT_EQ("response: 12,34", EvalJs(shell(), R"( |
| (async () => { |
| let context = await navigator.smartCard.establishContext(); |
| |
| let connection = |
| (await context.connect("Fake reader", "shared", |
| {preferredProtocols: ["t1"]})).connection; |
| |
| let apdu = new Uint8Array([0x03, 0x02, 0x01]); |
| let response = await connection.transmit(apdu); |
| |
| let responseString = new Uint8Array(response).toString(); |
| return `response: ${responseString}`; |
| })())")); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(SmartCardTest, TransmitWithOptions) { |
| ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl())); |
| |
| MockSmartCardContextFactory& mock_context_factory = |
| GetFakeSmartCardDelegate().mock_context_factory; |
| MockSmartCardConnection mock_connection; |
| mojo::Receiver<SmartCardConnection> connection_receiver(&mock_connection); |
| |
| { |
| InSequence s; |
| |
| GetFakeSmartCardDelegate().ExpectHasReaderPermission(kFakeReader); |
| |
| EXPECT_CALL(mock_context_factory, |
| Connect(kFakeReader, SmartCardShareMode::kDirect, _, _, _)) |
| .WillOnce( |
| [&connection_receiver]( |
| const std::string& reader, |
| device::mojom::SmartCardShareMode share_mode, |
| device::mojom::SmartCardProtocolsPtr preferred_protocols, |
| mojo::PendingRemote<device::mojom::SmartCardConnectionWatcher> |
| connection_watcher, |
| SmartCardContext::ConnectCallback callback) { |
| EXPECT_FALSE(preferred_protocols->t0); |
| EXPECT_FALSE(preferred_protocols->t1); |
| EXPECT_FALSE(preferred_protocols->raw); |
| |
| auto success = device::mojom::SmartCardConnectSuccess::New( |
| connection_receiver.BindNewPipeAndPassRemote(), |
| SmartCardProtocol::kUndefined); |
| |
| std::move(callback).Run( |
| device::mojom::SmartCardConnectResult::NewSuccess( |
| std::move(success))); |
| }); |
| |
| EXPECT_CALL(mock_connection, Transmit(SmartCardProtocol::kT0, _, _)) |
| .WillOnce([](SmartCardProtocol protocol, |
| const std::vector<uint8_t>& data, |
| SmartCardConnection::TransmitCallback callback) { |
| EXPECT_EQ(data, std::vector<uint8_t>({3u, 2u, 1u})); |
| std::move(callback).Run( |
| device::mojom::SmartCardDataResult::NewData({12u, 34u})); |
| }); |
| } |
| |
| EXPECT_EQ("response: 12,34", EvalJs(shell(), R"( |
| (async () => { |
| let context = await navigator.smartCard.establishContext(); |
| |
| let connection = |
| (await context.connect("Fake reader", "direct")).connection; |
| |
| // In real usage you would have in between: |
| // let IOCTL_SMARTCARD_SET_PROTOCOL = ...; |
| // connection.control(IOCTL_SMARTCARD_SET_PROTOCOL, ...); |
| |
| let apdu = new Uint8Array([0x03, 0x02, 0x01]); |
| let response = await connection.transmit(apdu, {protocol: "t0"}); |
| |
| let responseString = new Uint8Array(response).toString(); |
| return `response: ${responseString}`; |
| })())")); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(SmartCardTest, TransmitNoProtocol) { |
| ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl())); |
| |
| MockSmartCardContextFactory& mock_context_factory = |
| GetFakeSmartCardDelegate().mock_context_factory; |
| StrictMock<MockSmartCardConnection> mock_connection; |
| mojo::Receiver<SmartCardConnection> connection_receiver(&mock_connection); |
| |
| { |
| InSequence s; |
| |
| GetFakeSmartCardDelegate().ExpectHasReaderPermission(kFakeReader); |
| |
| EXPECT_CALL(mock_context_factory, |
| Connect(kFakeReader, SmartCardShareMode::kDirect, _, _, _)) |
| .WillOnce(WithArgs<2, 4>( |
| [&connection_receiver]( |
| device::mojom::SmartCardProtocolsPtr preferred_protocols, |
| SmartCardContext::ConnectCallback callback) { |
| EXPECT_FALSE(preferred_protocols->t0); |
| EXPECT_FALSE(preferred_protocols->t1); |
| EXPECT_FALSE(preferred_protocols->raw); |
| |
| auto success = device::mojom::SmartCardConnectSuccess::New( |
| connection_receiver.BindNewPipeAndPassRemote(), |
| SmartCardProtocol::kUndefined); |
| |
| std::move(callback).Run( |
| device::mojom::SmartCardConnectResult::NewSuccess( |
| std::move(success))); |
| })); |
| } |
| |
| EXPECT_THAT( |
| EvalJs(shell(), R"( |
| (async () => { |
| let context = await navigator.smartCard.establishContext(); |
| |
| let connection = |
| (await context.connect("Fake reader", "direct")).connection; |
| |
| let apdu = new Uint8Array([0x03, 0x02, 0x01]); |
| try { |
| await connection.transmit(apdu); |
| } catch(e) { |
| return `transmit: ${e.name}, ${e.message}`; |
| } |
| |
| return "ok"; |
| })())") |
| .ExtractString(), |
| MatchesRegex("transmit: InvalidStateError, .*No active protocol\\.")); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(SmartCardTest, Control) { |
| ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl())); |
| |
| MockSmartCardContextFactory& mock_context_factory = |
| GetFakeSmartCardDelegate().mock_context_factory; |
| MockSmartCardConnection mock_connection; |
| mojo::Receiver<SmartCardConnection> connection_receiver(&mock_connection); |
| |
| { |
| InSequence s; |
| |
| GetFakeSmartCardDelegate().ExpectHasReaderPermission(kFakeReader); |
| |
| mock_context_factory.ExpectConnectFakeReaderSharedT1(connection_receiver); |
| |
| EXPECT_CALL(mock_connection, Control(42, _, _)) |
| .WillOnce( |
| WithArgs<1, 2>([](const std::vector<uint8_t>& data, |
| SmartCardConnection::ControlCallback callback) { |
| EXPECT_EQ(data, std::vector<uint8_t>({3u, 2u, 1u})); |
| std::move(callback).Run( |
| device::mojom::SmartCardDataResult::NewData({12u, 34u})); |
| })); |
| } |
| |
| EXPECT_EQ("response: 12,34", EvalJs(shell(), R"( |
| (async () => { |
| let context = await navigator.smartCard.establishContext(); |
| |
| let connection = |
| (await context.connect("Fake reader", "shared", |
| {preferredProtocols: ["t1"]})).connection; |
| |
| let data = new Uint8Array([0x03, 0x02, 0x01]); |
| let response = await connection.control(42, data); |
| |
| let responseString = new Uint8Array(response).toString(); |
| return `response: ${responseString}`; |
| })())")); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(SmartCardTest, GetAttribute) { |
| ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl())); |
| |
| MockSmartCardContextFactory& mock_context_factory = |
| GetFakeSmartCardDelegate().mock_context_factory; |
| MockSmartCardConnection mock_connection; |
| mojo::Receiver<SmartCardConnection> connection_receiver(&mock_connection); |
| |
| { |
| InSequence s; |
| |
| GetFakeSmartCardDelegate().ExpectHasReaderPermission(kFakeReader); |
| |
| mock_context_factory.ExpectConnectFakeReaderSharedT1(connection_receiver); |
| |
| EXPECT_CALL(mock_connection, GetAttrib(42, _)) |
| .WillOnce( |
| WithArg<1>([](SmartCardConnection::GetAttribCallback callback) { |
| std::move(callback).Run( |
| device::mojom::SmartCardDataResult::NewData({12u, 34u})); |
| })); |
| } |
| |
| EXPECT_EQ("response: 12,34", EvalJs(shell(), R"( |
| (async () => { |
| let context = await navigator.smartCard.establishContext(); |
| |
| let connection = |
| (await context.connect("Fake reader", "shared", |
| {preferredProtocols: ["t1"]})).connection; |
| |
| let response = await connection.getAttribute(42); |
| |
| let responseString = new Uint8Array(response).toString(); |
| return `response: ${responseString}`; |
| })())")); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(SmartCardTest, SetAttribute) { |
| ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl())); |
| |
| MockSmartCardContextFactory& mock_context_factory = |
| GetFakeSmartCardDelegate().mock_context_factory; |
| MockSmartCardConnection mock_connection; |
| mojo::Receiver<SmartCardConnection> connection_receiver(&mock_connection); |
| |
| { |
| InSequence s; |
| |
| GetFakeSmartCardDelegate().ExpectHasReaderPermission(kFakeReader); |
| |
| mock_context_factory.ExpectConnectFakeReaderSharedT1(connection_receiver); |
| |
| EXPECT_CALL(mock_connection, |
| SetAttrib(42, std::vector<uint8_t>({3u, 2u, 1u}), _)) |
| .WillOnce( |
| WithArg<2>([](SmartCardConnection::SetAttribCallback callback) { |
| std::move(callback).Run( |
| device::mojom::SmartCardResult::NewSuccess( |
| SmartCardSuccess::kOk)); |
| })); |
| } |
| |
| EXPECT_EQ("success", EvalJs(shell(), R"( |
| (async () => { |
| let context = await navigator.smartCard.establishContext(); |
| |
| let connection = |
| (await context.connect("Fake reader", "shared", |
| {preferredProtocols: ["t1"]})).connection; |
| |
| let data = new Uint8Array([0x03, 0x02, 0x01]); |
| await connection.setAttribute(42, data); |
| |
| return 'success'; |
| })())")); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(SmartCardTest, Status) { |
| ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl())); |
| |
| MockSmartCardContextFactory& mock_context_factory = |
| GetFakeSmartCardDelegate().mock_context_factory; |
| MockSmartCardConnection mock_connection; |
| mojo::Receiver<SmartCardConnection> connection_receiver(&mock_connection); |
| |
| { |
| InSequence s; |
| |
| GetFakeSmartCardDelegate().ExpectHasReaderPermission(kFakeReader); |
| |
| mock_context_factory.ExpectConnectFakeReaderSharedT1(connection_receiver); |
| |
| EXPECT_CALL(mock_connection, Status) |
| .WillOnce([](SmartCardConnection::StatusCallback callback) { |
| auto result = device::mojom::SmartCardStatusResult::NewStatus( |
| SmartCardStatus::New( |
| kFakeReader, SmartCardConnectionState::kSpecific, |
| SmartCardProtocol::kT1, std::vector<uint8_t>({3u, 2u, 1u}))); |
| std::move(callback).Run(std::move(result)); |
| }); |
| } |
| |
| EXPECT_EQ("Fake reader, t1, {3,2,1}", EvalJs(shell(), R"( |
| (async () => { |
| let context = await navigator.smartCard.establishContext(); |
| |
| let connection = |
| (await context.connect("Fake reader", "shared", |
| {preferredProtocols: ["t1"]})).connection; |
| |
| let status = await connection.status(); |
| |
| let atr = new Uint8Array(status.answerToReset).toString(); |
| return `${status.readerName}, ${status.state}, {${atr}}`; |
| })())")); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(SmartCardTest, ListReaders) { |
| MockSmartCardContextFactory& mock_context_factory = |
| GetFakeSmartCardDelegate().mock_context_factory; |
| |
| mock_context_factory.ExpectListReaders({"Foo", "Bar"}); |
| |
| ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl())); |
| |
| auto expected_reader_names = |
| base::Value(base::Value::List().Append("Foo").Append("Bar")); |
| |
| EXPECT_EQ(expected_reader_names, EvalJs(shell(), R"((async () => { |
| let context = await navigator.smartCard.establishContext(); |
| return await context.listReaders(); |
| })())")); |
| } |
| |
| /* |
| This test checks that in case there are no readers available, listReaders() call |
| will return an empty list of readers with no errors. |
| |
| Note that internally we will receive a kNoReadersAvailable error from |
| SmartCardDelegate. However, we should not forward this error to Javascript. |
| */ |
| IN_PROC_BROWSER_TEST_F(SmartCardTest, ListReadersEmpty) { |
| MockSmartCardContextFactory& mock_context_factory = |
| GetFakeSmartCardDelegate().mock_context_factory; |
| |
| EXPECT_CALL(mock_context_factory, ListReaders(_)) |
| .WillOnce(RunOnceCallback<0>(SmartCardListReadersResult::NewError( |
| SmartCardError::kNoReadersAvailable))); |
| |
| ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl())); |
| |
| auto expected_reader_names = base::Value(base::Value::List()); |
| |
| EXPECT_EQ(expected_reader_names, EvalJs(shell(), R"((async () => { |
| let context = await navigator.smartCard.establishContext(); |
| return await context.listReaders(); |
| })())")); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(SmartCardTest, GetStatusChange) { |
| MockSmartCardContextFactory& mock_context_factory = |
| GetFakeSmartCardDelegate().mock_context_factory; |
| |
| EXPECT_CALL(mock_context_factory, |
| GetStatusChange(base::Milliseconds(4321), _, _)) |
| .WillOnce(WithArgs<1, 2>( |
| [](std::vector<device::mojom::SmartCardReaderStateInPtr> states_in, |
| SmartCardContext::GetStatusChangeCallback callback) { |
| ASSERT_EQ(states_in.size(), 1u); |
| ASSERT_EQ(states_in[0]->reader, "Fake Reader"); |
| EXPECT_FALSE(states_in[0]->current_state->unaware); |
| EXPECT_FALSE(states_in[0]->current_state->ignore); |
| EXPECT_FALSE(states_in[0]->current_state->changed); |
| EXPECT_FALSE(states_in[0]->current_state->unknown); |
| EXPECT_FALSE(states_in[0]->current_state->unavailable); |
| EXPECT_TRUE(states_in[0]->current_state->empty); |
| EXPECT_FALSE(states_in[0]->current_state->present); |
| EXPECT_FALSE(states_in[0]->current_state->exclusive); |
| EXPECT_FALSE(states_in[0]->current_state->inuse); |
| EXPECT_FALSE(states_in[0]->current_state->mute); |
| EXPECT_FALSE(states_in[0]->current_state->unpowered); |
| EXPECT_EQ(states_in[0]->current_count, 6u); |
| |
| auto state_flags = SmartCardReaderStateFlags::New(); |
| state_flags->unaware = false; |
| state_flags->ignore = false; |
| state_flags->changed = false; |
| state_flags->unknown = false; |
| state_flags->unavailable = false; |
| state_flags->empty = false; |
| state_flags->present = true; |
| state_flags->exclusive = false; |
| state_flags->inuse = true; |
| state_flags->mute = false; |
| state_flags->unpowered = false; |
| |
| std::vector<SmartCardReaderStateOutPtr> states_out; |
| states_out.push_back(SmartCardReaderStateOut::New( |
| "Fake Reader", std::move(state_flags), 7, |
| std::vector<uint8_t>({1u, 2u, 3u, 4u}))); |
| auto result = |
| device::mojom::SmartCardStatusChangeResult::NewReaderStates( |
| std::move(states_out)); |
| std::move(callback).Run(std::move(result)); |
| })); |
| |
| ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl())); |
| |
| EXPECT_EQ( |
| "Fake Reader, {ignore=false, changed=false, " |
| "unknown=false, unavailable=false, empty=false, present=true, " |
| "exclusive=false, inuse=true, mute=false, unpowered=false}, 7, {1,2,3,4}", |
| EvalJs(shell(), R"((async () => { |
| let context = await navigator.smartCard.establishContext(); |
| |
| let readerStates = [{readerName: "Fake Reader", |
| currentState: {empty: true}, |
| currentCount: 6 }]; |
| let statesOut = await context.getStatusChange( |
| readerStates, |
| {timeout: 4321}); |
| |
| if (statesOut.length !== 1) { |
| return `states array has size ${statesOut.length}`; |
| } |
| let atrString = new Uint8Array(statesOut[0].answerToReset).toString(); |
| |
| let flags = statesOut[0].eventState; |
| let eventStateString = `ignore=${flags.ignore}` |
| + `, changed=${flags.changed}` |
| + `, unknown=${flags.unknown}` |
| + `, unavailable=${flags.unavailable}` |
| + `, empty=${flags.empty}` |
| + `, present=${flags.present}` |
| + `, exclusive=${flags.exclusive}` |
| + `, inuse=${flags.inuse}` |
| + `, mute=${flags.mute}` |
| + `, unpowered=${flags.unpowered}`; |
| |
| return `${statesOut[0].readerName}, {${eventStateString}}` + |
| `, ${statesOut[0].eventCount}, {${atrString}}`; |
| })())")); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(SmartCardTest, GetStatusChangeInterruptedByClose) { |
| std::optional<SmartCardContext::GetStatusChangeCallback> callback_holder; |
| MockSmartCardContextFactory& mock_context_factory = |
| GetFakeSmartCardDelegate().mock_context_factory; |
| |
| EXPECT_CALL(mock_context_factory, GetStatusChange) |
| .WillOnce( |
| WithArg<2>([&callback_holder]( |
| SmartCardContext::GetStatusChangeCallback callback) { |
| callback_holder.emplace(std::move(callback)); |
| })); |
| |
| ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl())); |
| |
| ASSERT_TRUE(ExecJs(shell(), R"((async () => { |
| let context = await navigator.smartCard.establishContext(); |
| |
| let readerStates = [{readerName: "Fake Reader", |
| currentState: {empty: true}, |
| currentCount: 6 }]; |
| let promise = context.getStatusChange( |
| readerStates, |
| {timeout: 4321}); |
| })())")); |
| ASSERT_NO_FATAL_FAILURE(shell()->Reload()); |
| content::WaitForLoadStop(shell()->web_contents()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(SmartCardTest, GetStatusChangeAborted) { |
| MockSmartCardContextFactory& mock_context_factory = |
| GetFakeSmartCardDelegate().mock_context_factory; |
| |
| base::test::TestFuture<SmartCardContext::GetStatusChangeCallback> |
| get_status_callback; |
| |
| { |
| InSequence s; |
| |
| EXPECT_CALL(mock_context_factory, |
| GetStatusChange(base::TimeDelta::Max(), _, _)) |
| .WillOnce(WithArgs<1, 2>( |
| [&get_status_callback]( |
| std::vector<device::mojom::SmartCardReaderStateInPtr> states_in, |
| SmartCardContext::GetStatusChangeCallback callback) { |
| ASSERT_EQ(states_in.size(), size_t(1)); |
| ASSERT_EQ(states_in[0]->reader, "Fake Reader"); |
| EXPECT_FALSE(states_in[0]->current_state->unaware); |
| EXPECT_FALSE(states_in[0]->current_state->ignore); |
| EXPECT_FALSE(states_in[0]->current_state->changed); |
| EXPECT_FALSE(states_in[0]->current_state->unknown); |
| EXPECT_FALSE(states_in[0]->current_state->unavailable); |
| EXPECT_TRUE(states_in[0]->current_state->empty); |
| EXPECT_FALSE(states_in[0]->current_state->present); |
| EXPECT_FALSE(states_in[0]->current_state->exclusive); |
| EXPECT_FALSE(states_in[0]->current_state->inuse); |
| EXPECT_FALSE(states_in[0]->current_state->mute); |
| EXPECT_FALSE(states_in[0]->current_state->unpowered); |
| |
| // Don't respond immediately. |
| get_status_callback.SetValue(std::move(callback)); |
| })); |
| |
| // Aborting a blink context.getStatusChange() call means sending a Cancel() |
| // request down to device.mojom. |
| EXPECT_CALL(mock_context_factory, Cancel(_)) |
| .WillOnce( |
| [&get_status_callback](SmartCardContext::CancelCallback callback) { |
| std::move(get_status_callback) |
| .Take() |
| .Run(device::mojom::SmartCardStatusChangeResult::NewError( |
| SmartCardError::kCancelled)); |
| |
| std::move(callback).Run( |
| SmartCardResult::NewSuccess(SmartCardSuccess::kOk)); |
| }); |
| } |
| |
| ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl())); |
| |
| EXPECT_EQ("Exception: Error, Something", EvalJs(shell(), R"((async () => { |
| let context = await navigator.smartCard.establishContext(); |
| |
| let abortController = new AbortController(); |
| |
| let getStatusPromise = context.getStatusChange( |
| [{readerName: "Fake Reader", currentState: {empty: true}}], |
| {signal: abortController.signal}); |
| |
| abortController.abort(Error("Something")); |
| |
| try { |
| let result = await getStatusPromise; |
| return "Success"; |
| } catch (e) { |
| return `Exception: ${e.name}, ${e.message}`; |
| } |
| })())")); |
| } |
| |
| // Tests passing an AbortSignal to getStatusChange() that is already aborted. |
| IN_PROC_BROWSER_TEST_F(SmartCardTest, GetStatusChangeAlreadyAborted) { |
| ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl())); |
| |
| EXPECT_EQ("Exception: Error, Something", EvalJs(shell(), R"((async () => { |
| let context = await navigator.smartCard.establishContext(); |
| |
| let getStatusPromise = context.getStatusChange( |
| [{readerName: "Fake Reader", currentState: {empty: true}}], |
| {signal: AbortSignal.abort(Error("Something"))}); |
| |
| try { |
| let result = await getStatusPromise; |
| return "Success"; |
| } catch (e) { |
| return `Exception: ${e.name}, ${e.message}`; |
| } |
| })())")); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(SmartCardTest, Connect) { |
| MockSmartCardContextFactory& mock_context_factory = |
| GetFakeSmartCardDelegate().mock_context_factory; |
| |
| EXPECT_CALL(mock_context_factory, |
| Connect(kFakeReader, SmartCardShareMode::kShared, _, _, _)) |
| .WillOnce(WithArgs<2, 4>([](device::mojom::SmartCardProtocolsPtr |
| preferred_protocols, |
| SmartCardContext::ConnectCallback callback) { |
| mojo::PendingRemote<device::mojom::SmartCardConnection> pending_remote; |
| |
| EXPECT_TRUE(preferred_protocols->t0); |
| EXPECT_TRUE(preferred_protocols->t1); |
| EXPECT_FALSE(preferred_protocols->raw); |
| |
| mojo::MakeSelfOwnedReceiver( |
| std::make_unique<MockSmartCardConnection>(), |
| pending_remote.InitWithNewPipeAndPassReceiver()); |
| |
| auto success = device::mojom::SmartCardConnectSuccess::New( |
| std::move(pending_remote), SmartCardProtocol::kT1); |
| |
| std::move(callback).Run( |
| device::mojom::SmartCardConnectResult::NewSuccess( |
| std::move(success))); |
| })); |
| |
| GetFakeSmartCardDelegate().ExpectHasReaderPermission(kFakeReader); |
| |
| ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl())); |
| |
| auto expected_reader_names = |
| base::Value(base::Value::List().Append("Foo").Append("Bar")); |
| |
| EXPECT_EQ("[object SmartCardConnection], t1", EvalJs(shell(), R"( |
| (async () => { |
| let context = await navigator.smartCard.establishContext(); |
| let result = await context.connect("Fake reader", "shared", |
| {preferredProtocols: ["t0", "t1"]}); |
| return `${result.connection}, ${result.activeProtocol}`; |
| })())")); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(SmartCardTest, ConnectDenied) { |
| MockSmartCardContextFactory& mock_context_factory = |
| GetFakeSmartCardDelegate().mock_context_factory; |
| |
| EXPECT_CALL(mock_context_factory, Connect).Times(0); |
| |
| { |
| InSequence s; |
| |
| mock_context_factory.ExpectListReaders({kFakeReader}); |
| |
| // No permission yet. So renderer will have to request it. |
| EXPECT_CALL(GetFakeSmartCardDelegate(), HasReaderPermission(_, kFakeReader)) |
| .WillOnce(Return(false)); |
| |
| // Permission was requested and it got denied. |
| EXPECT_CALL(GetFakeSmartCardDelegate(), |
| RequestReaderPermission(_, kFakeReader, _)) |
| .WillOnce(RunOnceCallback<2>(false)); |
| } |
| |
| ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl())); |
| |
| EXPECT_EQ( |
| "NotAllowedError, Failed to execute 'connect' on 'SmartCardContext': The " |
| "user has denied permission or the user attention requirement was not " |
| "fulfilled.", |
| EvalJs(shell(), R"( |
| (async () => { |
| let context = await navigator.smartCard.establishContext(); |
| let readers = await context.listReaders(); |
| try { |
| let result = await context.connect(readers[0], "shared", |
| {preferredProtocols: ["t0", "t1"]}); |
| } catch (e) { |
| return `${e.name}, ${e.message}`; |
| } |
| return "ok"; |
| })())")); |
| } |
| |
| // Tests that a connection request is immediately denied if the application |
| // passes a reader name string that is not known to have come from the smart |
| // card API. |
| // This is to avoid presenting unfiltered strings to the user in a permission |
| // prompt. |
| IN_PROC_BROWSER_TEST_F(SmartCardTest, ConnectDeniedUnknownString) { |
| constexpr char kMyDisturbingString[] = "my disturbing string"; |
| |
| MockSmartCardContextFactory& mock_context_factory = |
| GetFakeSmartCardDelegate().mock_context_factory; |
| |
| // The connection request shall not go through. |
| EXPECT_CALL(mock_context_factory, Connect(_, _, _, _, _)).Times(0); |
| |
| // We tell that there's no permission yet. |
| EXPECT_CALL(GetFakeSmartCardDelegate(), |
| HasReaderPermission(_, kMyDisturbingString)) |
| .WillOnce(Return(false)); |
| |
| // But the permission should not be requested as the reader name string is |
| // unknown. |
| EXPECT_CALL(GetFakeSmartCardDelegate(), |
| RequestReaderPermission(_, kMyDisturbingString, _)) |
| .Times(0); |
| |
| ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl())); |
| |
| EXPECT_EQ("NotAllowedError", EvalJs(shell(), JsReplace(R"( |
| (async () => { |
| let context = await navigator.smartCard.establishContext(); |
| try { |
| let result = await context.connect($1, "shared", |
| {preferredProtocols: ["t0", "t1"]}); |
| } catch (e) { |
| return e.name; |
| } |
| return "ok"; |
| })())", |
| kMyDisturbingString))); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(SmartCardTest, ConnectPermissionGranted) { |
| MockSmartCardContextFactory& mock_context_factory = |
| GetFakeSmartCardDelegate().mock_context_factory; |
| MockSmartCardConnection mock_connection; |
| mojo::Receiver<SmartCardConnection> connection_receiver(&mock_connection); |
| |
| { |
| InSequence s; |
| |
| mock_context_factory.ExpectListReaders({kFakeReader}); |
| |
| // No permission yet. So renderer will have to request it. |
| EXPECT_CALL(GetFakeSmartCardDelegate(), HasReaderPermission(_, kFakeReader)) |
| .WillOnce(Return(false)); |
| |
| // Permission was requested and granted. |
| EXPECT_CALL(GetFakeSmartCardDelegate(), |
| RequestReaderPermission(_, kFakeReader, _)) |
| .WillOnce(RunOnceCallback<2>(true)); |
| |
| // The Connect request will then finally go through. |
| mock_context_factory.ExpectConnectFakeReaderSharedT1(connection_receiver); |
| } |
| |
| ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl())); |
| |
| EXPECT_EQ("ok", EvalJs(shell(), R"( |
| (async () => { |
| let context = await navigator.smartCard.establishContext(); |
| let readers = await context.listReaders(); |
| try { |
| let result = await context.connect(readers[0], "shared", |
| {preferredProtocols: ["t1"]}); |
| } catch (e) { |
| return `${e.name}, ${e.message}`; |
| } |
| return "ok"; |
| })())")); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(SmartCardTest, StartTransaction) { |
| ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl())); |
| |
| MockSmartCardContextFactory& mock_context_factory = |
| GetFakeSmartCardDelegate().mock_context_factory; |
| MockSmartCardConnection mock_connection; |
| mojo::Receiver<SmartCardConnection> connection_receiver(&mock_connection); |
| |
| MockSmartCardTransaction mock_transaction; |
| mojo::AssociatedReceiver<SmartCardTransaction> transaction_receiver( |
| &mock_transaction); |
| |
| { |
| InSequence s; |
| |
| GetFakeSmartCardDelegate().ExpectHasReaderPermission(kFakeReader); |
| |
| mock_context_factory.ExpectConnectFakeReaderSharedT1(connection_receiver); |
| |
| mock_connection.ExpectBeginTransaction(transaction_receiver); |
| |
| EXPECT_CALL(mock_connection, Transmit(SmartCardProtocol::kT1, _, _)) |
| .WillOnce( |
| WithArgs<1, 2>([](const std::vector<uint8_t>& data, |
| SmartCardConnection::TransmitCallback callback) { |
| EXPECT_EQ(data, std::vector<uint8_t>({3u, 2u, 1u})); |
| std::move(callback).Run( |
| device::mojom::SmartCardDataResult::NewData({12u, 34u})); |
| })); |
| |
| mock_transaction.ExpectEndTransaction(SmartCardDisposition::kReset); |
| } |
| |
| EXPECT_EQ("ok", EvalJs(shell(), R"( |
| (async () => { |
| let context = await navigator.smartCard.establishContext(); |
| |
| let connection = |
| (await context.connect("Fake reader", "shared", |
| {preferredProtocols: ["t1"]})).connection; |
| |
| let transaction = async () => { |
| let apdu = new Uint8Array([0x03, 0x02, 0x01]); |
| await connection.transmit(apdu); |
| return "reset"; |
| } |
| |
| await connection.startTransaction(transaction); |
| |
| return "ok"; |
| })())")); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(SmartCardTest, StartTransactionAborted) { |
| ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl())); |
| |
| MockSmartCardContextFactory& mock_context_factory = |
| GetFakeSmartCardDelegate().mock_context_factory; |
| StrictMock<MockSmartCardConnection> mock_connection; |
| mojo::Receiver<SmartCardConnection> connection_receiver(&mock_connection); |
| |
| base::test::TestFuture<SmartCardConnection::BeginTransactionCallback> |
| begin_transaction_callback; |
| |
| { |
| InSequence s; |
| |
| GetFakeSmartCardDelegate().ExpectHasReaderPermission(kFakeReader); |
| |
| mock_context_factory.ExpectConnectFakeReaderSharedT1(connection_receiver); |
| |
| EXPECT_CALL(mock_connection, BeginTransaction(_)) |
| .WillOnce([&begin_transaction_callback]( |
| SmartCardConnection::BeginTransactionCallback callback) { |
| // Don't respond immediately. |
| begin_transaction_callback.SetValue(std::move(callback)); |
| }); |
| |
| // Aborting a blink connection.startTransaction() call means sending a |
| // Cancel() request down to device.mojom.SmartCardContext |
| EXPECT_CALL(mock_context_factory, Cancel(_)) |
| .WillOnce([&begin_transaction_callback]( |
| SmartCardContext::CancelCallback callback) { |
| begin_transaction_callback.Take().Run( |
| device::mojom::SmartCardTransactionResult::NewError( |
| SmartCardError::kCancelled)); |
| |
| std::move(callback).Run( |
| SmartCardResult::NewSuccess(SmartCardSuccess::kOk)); |
| }); |
| } |
| |
| EXPECT_EQ("Exception: Error, Something", EvalJs(shell(), R"( |
| (async () => { |
| let context = await navigator.smartCard.establishContext(); |
| |
| let connection = |
| (await context.connect("Fake reader", "shared", |
| {preferredProtocols: ["t1"]})).connection; |
| |
| let transaction = async () => { |
| let apdu = new Uint8Array([0x03, 0x02, 0x01]); |
| await connection.transmit(apdu); |
| return "reset"; |
| } |
| |
| let abortController = new AbortController(); |
| |
| let promise = |
| connection.startTransaction(transaction, |
| {signal: abortController.signal}); |
| |
| abortController.abort(Error("Something")); |
| |
| try { |
| await promise; |
| return "Success"; |
| } catch (e) { |
| return `Exception: ${e.name}, ${e.message}`; |
| } |
| })())")); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(SmartCardTest, TransactionCallbackRejects) { |
| TestEmptyTransaction("startTransaction: Error, Oops!", R"( |
| async () => { throw new Error('Oops!'); } |
| )"); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(SmartCardTest, TransactionCallbackThrows) { |
| TestEmptyTransaction("startTransaction: Error, Oops!", R"( |
| () => { throw new Error('Oops!'); } |
| )"); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(SmartCardTest, TransactionCallbackReturnsEmptyPromise) { |
| TestEmptyTransaction("ok", R"( async () => {} )"); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(SmartCardTest, TransactionCallbackReturnsNothing) { |
| TestEmptyTransaction("ok", R"( () => {} )"); |
| } |
| |
| // A transaction callback must return a SmartCardDisposition. |
| // Check that if the callback returns something else, startTransaction() returns |
| // an appropriate error. |
| IN_PROC_BROWSER_TEST_F(SmartCardTest, TransactionCallbackReturnsInvalidValue) { |
| TestEmptyTransaction( |
| "startTransaction: TypeError, Failed to execute 'startTransaction' on " |
| "'SmartCardConnection': The provided value '[object Object]' is not a " |
| "valid enum value of type SmartCardDisposition.", |
| R"( |
| async () => { |
| // Return some random object instead of a SmartCardDisposition |
| return {foo: 'bar', hello: 42}; |
| } |
| )"); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(SmartCardTest, EndTransactionFails) { |
| ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl())); |
| |
| MockSmartCardContextFactory& mock_context_factory = |
| GetFakeSmartCardDelegate().mock_context_factory; |
| MockSmartCardConnection mock_connection; |
| mojo::Receiver<SmartCardConnection> connection_receiver(&mock_connection); |
| |
| MockSmartCardTransaction mock_transaction; |
| mojo::AssociatedReceiver<SmartCardTransaction> transaction_receiver( |
| &mock_transaction); |
| |
| { |
| InSequence s; |
| |
| GetFakeSmartCardDelegate().ExpectHasReaderPermission(kFakeReader); |
| mock_context_factory.ExpectConnectFakeReaderSharedT1(connection_receiver); |
| mock_connection.ExpectBeginTransaction(transaction_receiver); |
| |
| EXPECT_CALL(mock_transaction, |
| EndTransaction(SmartCardDisposition::kEject, _)) |
| .WillOnce(WithArg<1>( |
| [](SmartCardTransaction::EndTransactionCallback callback) { |
| std::move(callback).Run( |
| SmartCardResult::NewError(SmartCardError::kResetCard)); |
| })); |
| } |
| |
| EXPECT_EQ( |
| "startTransaction: SmartCardError, The smart card has been reset, so any " |
| "shared state information is invalid.", |
| EvalJs(shell(), R"( |
| (async () => { |
| let context = await navigator.smartCard.establishContext(); |
| |
| let connection = |
| (await context.connect("Fake reader", "shared", |
| {preferredProtocols: ["t1"]})).connection; |
| |
| let transaction = async () => { |
| return "eject"; |
| } |
| |
| transactionPromise = connection.startTransaction(transaction); |
| try { |
| await transactionPromise; |
| } catch (e) { |
| return `startTransaction: ${e.name}, ${e.message}`; |
| } |
| |
| return "startTransaction did not throw"; |
| })())")); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(SmartCardTest, DisconnectedOnTransactionReturn) { |
| ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl())); |
| |
| MockSmartCardContextFactory& mock_context_factory = |
| GetFakeSmartCardDelegate().mock_context_factory; |
| MockSmartCardConnection mock_connection; |
| mojo::Receiver<SmartCardConnection> connection_receiver(&mock_connection); |
| |
| StrictMock<MockSmartCardTransaction> mock_transaction; |
| mojo::AssociatedReceiver<SmartCardTransaction> transaction_receiver( |
| &mock_transaction); |
| |
| { |
| InSequence s; |
| |
| GetFakeSmartCardDelegate().ExpectHasReaderPermission(kFakeReader); |
| mock_context_factory.ExpectConnectFakeReaderSharedT1(connection_receiver); |
| mock_connection.ExpectBeginTransaction(transaction_receiver); |
| |
| EXPECT_CALL(mock_connection, Disconnect(SmartCardDisposition::kLeave, _)) |
| .WillOnce( |
| WithArg<1>([](SmartCardConnection::DisconnectCallback callback) { |
| std::move(callback).Run( |
| SmartCardResult::NewSuccess(SmartCardSuccess::kOk)); |
| })); |
| } |
| |
| EXPECT_EQ( |
| "startTransaction: InvalidStateError, Failed to execute " |
| "'startTransaction' on 'SmartCardConnection': Cannot end transaction " |
| "with an invalid connection.", |
| EvalJs(shell(), R"( |
| (async () => { |
| let context = await navigator.smartCard.establishContext(); |
| |
| let connection = |
| (await context.connect("Fake reader", "shared", |
| {preferredProtocols: ["t1"]})).connection; |
| |
| let transaction = async () => { |
| await connection.disconnect(); |
| return "eject"; |
| } |
| |
| transactionPromise = connection.startTransaction(transaction); |
| try { |
| await transactionPromise; |
| } catch (e) { |
| return `startTransaction: ${e.name}, ${e.message}`; |
| } |
| |
| return "startTransaction did not throw"; |
| })())")); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(SmartCardTest, OngoingTransmitOnTransactionReturn) { |
| ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl())); |
| |
| MockSmartCardContextFactory& mock_context_factory = |
| GetFakeSmartCardDelegate().mock_context_factory; |
| MockSmartCardConnection mock_connection; |
| mojo::Receiver<SmartCardConnection> connection_receiver(&mock_connection); |
| |
| StrictMock<MockSmartCardTransaction> mock_transaction; |
| mojo::AssociatedReceiver<SmartCardTransaction> transaction_receiver( |
| &mock_transaction); |
| |
| { |
| InSequence s; |
| |
| GetFakeSmartCardDelegate().ExpectHasReaderPermission(kFakeReader); |
| mock_context_factory.ExpectConnectFakeReaderSharedT1(connection_receiver); |
| mock_connection.ExpectBeginTransaction(transaction_receiver); |
| |
| EXPECT_CALL(mock_connection, Transmit(SmartCardProtocol::kT1, _, _)) |
| .WillOnce( |
| WithArgs<1, 2>([](const std::vector<uint8_t>& data, |
| SmartCardConnection::TransmitCallback callback) { |
| EXPECT_EQ(data, std::vector<uint8_t>({3u, 2u, 1u})); |
| std::move(callback).Run( |
| device::mojom::SmartCardDataResult::NewData({12u, 34u})); |
| })); |
| |
| mock_transaction.ExpectEndTransaction(SmartCardDisposition::kEject); |
| } |
| |
| EXPECT_EQ( |
| "startTransaction: InvalidStateError, Transaction callback returned " |
| "while an operation was still in progress.", |
| EvalJs(shell(), R"( |
| (async () => { |
| let context = await navigator.smartCard.establishContext(); |
| |
| let connection = |
| (await context.connect("Fake reader", "shared", |
| {preferredProtocols: ["t1"]})).connection; |
| |
| let transaction = async () => { |
| // Return before the transmit() completes. |
| let apdu = new Uint8Array([0x03, 0x02, 0x01]); |
| connection.transmit(apdu); |
| return "eject"; |
| } |
| |
| transactionPromise = connection.startTransaction(transaction); |
| try { |
| await transactionPromise; |
| } catch (e) { |
| return `startTransaction: ${e.name}, ${e.message}`; |
| } |
| return "ok"; |
| })())")); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(SmartCardTest, |
| ContextOperationBlocksConnectionOperation) { |
| ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl())); |
| |
| MockSmartCardContextFactory& mock_context_factory = |
| GetFakeSmartCardDelegate().mock_context_factory; |
| StrictMock<MockSmartCardConnection> mock_connection; |
| mojo::Receiver<SmartCardConnection> connection_receiver(&mock_connection); |
| |
| TestFuture<SmartCardContext::ListReadersCallback> list_readers_callback; |
| |
| { |
| InSequence s; |
| |
| GetFakeSmartCardDelegate().ExpectHasReaderPermission(kFakeReader); |
| |
| mock_context_factory.ExpectConnectFakeReaderSharedT1(connection_receiver); |
| |
| EXPECT_CALL(mock_context_factory, ListReaders(_)) |
| .WillOnce([&list_readers_callback]( |
| SmartCardContext::ListReadersCallback callback) { |
| // Don't respond immediately. |
| list_readers_callback.SetValue(std::move(callback)); |
| }); |
| } |
| |
| EXPECT_EQ( |
| "control: InvalidStateError, Failed to execute 'control' on " |
| "'SmartCardConnection': An operation is already in progress in this " |
| "smart card context.", |
| EvalJs(shell(), R"( |
| (async () => { |
| let context = await navigator.smartCard.establishContext(); |
| |
| let connection = |
| (await context.connect("Fake reader", "shared", |
| {preferredProtocols: ["t1"]})).connection; |
| |
| let listReadersPromise = context.listReaders(); |
| |
| try { |
| let data = new Uint8Array([0x03, 0x02, 0x01]); |
| await connection.control(42, data); |
| } catch (e) { |
| return `control: ${e.name}, ${e.message}`; |
| } |
| |
| await listReadersPromise; |
| |
| return `ok`; |
| })())")); |
| |
| // Let context.listReaders() conclude |
| list_readers_callback.Take().Run( |
| SmartCardListReadersResult::NewReaders({kFakeReader})); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(SmartCardTest, ConnectionDiesWithOperationInProgress) { |
| ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl())); |
| |
| MockSmartCardContextFactory& mock_context_factory = |
| GetFakeSmartCardDelegate().mock_context_factory; |
| StrictMock<MockSmartCardConnection> mock_connection; |
| mojo::Receiver<SmartCardConnection> connection_receiver(&mock_connection); |
| |
| { |
| InSequence s; |
| |
| GetFakeSmartCardDelegate().ExpectHasReaderPermission(kFakeReader); |
| |
| mock_context_factory.ExpectConnectFakeReaderSharedT1(connection_receiver); |
| |
| EXPECT_CALL(mock_connection, Control(42, _, _)) |
| .WillOnce(WithoutArgs( |
| [&connection_receiver]() { connection_receiver.reset(); })); |
| } |
| |
| EXPECT_EQ( |
| "control: InvalidStateError, Failed to execute 'control' on " |
| "'SmartCardConnection': Is disconnected.", |
| EvalJs(shell(), R"( |
| (async () => { |
| let context = await navigator.smartCard.establishContext(); |
| |
| let connection = |
| (await context.connect("Fake reader", "shared", |
| {preferredProtocols: ["t1"]})).connection; |
| |
| try { |
| let data = new Uint8Array([0x03, 0x02, 0x01]); |
| await connection.control(42, data); |
| } catch (e) { |
| return `control: ${e.name}, ${e.message}`; |
| } |
| })())")); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(SmartCardTest, ContextDiesConnectionStays) { |
| ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl())); |
| |
| MockSmartCardContextFactory& mock_context_factory = |
| GetFakeSmartCardDelegate().mock_context_factory; |
| StrictMock<MockSmartCardConnection> mock_connection; |
| mojo::Receiver<SmartCardConnection> connection_receiver(&mock_connection); |
| |
| { |
| InSequence s; |
| |
| GetFakeSmartCardDelegate().ExpectHasReaderPermission(kFakeReader); |
| |
| mock_context_factory.ExpectConnectFakeReaderSharedT1(connection_receiver); |
| |
| EXPECT_CALL(mock_connection, Control(42, _, _)) |
| .WillOnce( |
| WithArg<2>([&mock_context_factory]( |
| SmartCardConnection::ControlCallback callback) { |
| mock_context_factory.ClearContextReceivers(); |
| std::move(callback).Run( |
| device::mojom::SmartCardDataResult::NewData({12u, 34u})); |
| })); |
| } |
| |
| EXPECT_EQ("response: 12,34", EvalJs(shell(), R"( |
| (async () => { |
| let context = await navigator.smartCard.establishContext(); |
| |
| let connection = |
| (await context.connect("Fake reader", "shared", |
| {preferredProtocols: ["t1"]})).connection; |
| |
| let data = new Uint8Array([0x03, 0x02, 0x01]); |
| let response = await connection.control(42, data); |
| |
| let responseString = new Uint8Array(response).toString(); |
| return `response: ${responseString}`; |
| })())")); |
| } |
| |
| // A ContentBrowserClient that grants Isolated Web Apps the "smart-card" |
| // permission, but not "cross-origin-isolated", which should result in Smart |
| // Cards being disabled. |
| class NoCoiPermissionSmartCardTestContentBrowserClient |
| : public SmartCardTestContentBrowserClient { |
| public: |
| std::optional<network::ParsedPermissionsPolicy> |
| GetPermissionsPolicyForIsolatedWebApp( |
| WebContents* web_contents, |
| const url::Origin& app_origin) override { |
| return {{network::ParsedPermissionsPolicyDeclaration( |
| network::mojom::PermissionsPolicyFeature::kSmartCard, |
| /*allowed_origins=*/{}, |
| /*self_if_matches=*/app_origin, |
| /*matches_all_origins=*/false, /*matches_opaque_src=*/false)}}; |
| } |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(SmartCardTest, NoCoiPermission) { |
| NoCoiPermissionSmartCardTestContentBrowserClient client; |
| client.SetSmartCardDelegate(std::make_unique<FakeSmartCardDelegate>()); |
| |
| ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl())); |
| |
| EXPECT_EQ(false, EvalJs(shell(), "self.crossOriginIsolated")); |
| EXPECT_THAT(EvalJs(shell(), "navigator.smartCard.establishContext()"), |
| EvalJsResult::ErrorIs(HasSubstr( |
| "Frame is not sufficiently isolated to use smart cards."))); |
| } |
| |
| /* Tests the situation where a transaction callback erroneously returns while an |
| * operation in this connection is ongoing. If that operation fails at PC/SC |
| * level the Web API implementation should still cleanup after itself by ending |
| * the PC/SC transaction once that operation completes. |
| */ |
| IN_PROC_BROWSER_TEST_F(SmartCardTest, EndTransactionAfterFailedOperation) { |
| ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl())); |
| |
| MockSmartCardContextFactory& mock_context_factory = |
| GetFakeSmartCardDelegate().mock_context_factory; |
| MockSmartCardConnection mock_connection; |
| mojo::Receiver<SmartCardConnection> connection_receiver(&mock_connection); |
| |
| MockSmartCardTransaction mock_transaction; |
| mojo::AssociatedReceiver<SmartCardTransaction> transaction_receiver( |
| &mock_transaction); |
| |
| { |
| InSequence s; |
| |
| GetFakeSmartCardDelegate().ExpectHasReaderPermission(kFakeReader); |
| |
| mock_context_factory.ExpectConnectFakeReaderSharedT1(connection_receiver); |
| |
| mock_connection.ExpectBeginTransaction(transaction_receiver); |
| |
| EXPECT_CALL(mock_connection, Status(_)) |
| .WillOnce([](SmartCardConnection::StatusCallback callback) { |
| // Simulate a PC/SC failure. |
| auto result = device::mojom::SmartCardStatusResult::NewError( |
| SmartCardError::kReaderUnavailable); |
| std::move(callback).Run(std::move(result)); |
| }); |
| |
| mock_transaction.ExpectEndTransaction(SmartCardDisposition::kReset); |
| } |
| |
| EXPECT_EQ( |
| "startTransaction: InvalidStateError, Transaction callback returned " |
| "while an operation was still in progress.", |
| EvalJs(shell(), R"( |
| (async () => { |
| let context = await navigator.smartCard.establishContext(); |
| |
| let connection = |
| (await context.connect("Fake reader", "shared", |
| {preferredProtocols: ["t1"]})).connection; |
| |
| let transaction = () => { |
| connection.status(); |
| }; |
| |
| try { |
| await connection.startTransaction(transaction); |
| } catch (e) { |
| return `startTransaction: ${e.name}, ${e.message}`; |
| } |
| |
| return "ok"; |
| })())")); |
| } |
| |
| /* Tests the situation where a transaction callback erroneously returns while a |
| * SmartCardContext operation is ongoing. The Web API implementation should |
| * cleanup after itself by ending the PC/SC transaction once that operation |
| * completes. |
| */ |
| IN_PROC_BROWSER_TEST_F(SmartCardTest, EndTransactionAfterContextOperation) { |
| ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl())); |
| |
| MockSmartCardContextFactory& mock_context_factory = |
| GetFakeSmartCardDelegate().mock_context_factory; |
| MockSmartCardConnection mock_connection; |
| mojo::Receiver<SmartCardConnection> connection_receiver(&mock_connection); |
| |
| MockSmartCardTransaction mock_transaction; |
| mojo::AssociatedReceiver<SmartCardTransaction> transaction_receiver( |
| &mock_transaction); |
| |
| { |
| InSequence s; |
| |
| GetFakeSmartCardDelegate().ExpectHasReaderPermission(kFakeReader); |
| |
| mock_context_factory.ExpectConnectFakeReaderSharedT1(connection_receiver); |
| |
| mock_connection.ExpectBeginTransaction(transaction_receiver); |
| |
| mock_context_factory.ExpectListReaders({"Foo", "Bar"}); |
| |
| mock_transaction.ExpectEndTransaction(SmartCardDisposition::kEject); |
| } |
| |
| EXPECT_EQ( |
| "startTransaction: InvalidStateError, Transaction callback returned " |
| "while an operation was still in progress.", |
| EvalJs(shell(), R"( |
| (async () => { |
| let context = await navigator.smartCard.establishContext(); |
| |
| let connection = |
| (await context.connect("Fake reader", "shared", |
| {preferredProtocols: ["t1"]})).connection; |
| |
| let transaction = () => { |
| context.listReaders(); |
| return "eject"; |
| }; |
| |
| try { |
| await connection.startTransaction(transaction); |
| } catch (e) { |
| return `startTransaction: ${e.name}, ${e.message}`; |
| } |
| |
| return "ok"; |
| })())")); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(SmartCardTest, EstablishContextDenied) { |
| ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl())); |
| |
| EXPECT_CALL(GetFakeSmartCardDelegate(), IsPermissionBlocked(_)) |
| .WillOnce(Return(true)); |
| |
| EXPECT_EQ("NotAllowedError", EvalJs(shell(), R"((async () => { |
| try { |
| let context = await navigator.smartCard.establishContext(); |
| } catch (e) { |
| return e.name; |
| } |
| return "ok"; |
| })())")); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(SmartCardTest, WatcherClosedWhenPermissionExpired) { |
| ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl())); |
| |
| MockSmartCardContextFactory& mock_context_factory = |
| GetFakeSmartCardDelegate().mock_context_factory; |
| MockSmartCardConnection mock_connection; |
| mojo::Receiver<SmartCardConnection> connection_receiver(&mock_connection); |
| mojo::Remote<device::mojom::SmartCardConnectionWatcher> watcher; |
| raw_ptr<content::SmartCardDelegate::PermissionObserver> observer_store = |
| nullptr; |
| |
| { |
| InSequence s; |
| |
| auto& delegate = GetFakeSmartCardDelegate(); |
| |
| EXPECT_CALL(delegate, AddObserver).WillOnce(SaveArg<1>(&observer_store)); |
| delegate.ExpectHasReaderPermission(kFakeReader); |
| EXPECT_CALL(mock_context_factory, |
| Connect("Fake reader", SmartCardShareMode::kShared, _, _, _)) |
| .WillOnce(WithArgs<3, 4>( |
| [&connection_receiver, &watcher]( |
| mojo::PendingRemote<device::mojom::SmartCardConnectionWatcher> |
| connection_watcher, |
| SmartCardContext::ConnectCallback callback) { |
| watcher.Bind(std::move(connection_watcher)); |
| |
| auto success = device::mojom::SmartCardConnectSuccess::New( |
| connection_receiver.BindNewPipeAndPassRemote(), |
| SmartCardProtocol::kT1); |
| |
| std::move(callback).Run( |
| device::mojom::SmartCardConnectResult::NewSuccess( |
| std::move(success))); |
| })); |
| EXPECT_CALL(delegate, HasReaderPermission(_, "Fake reader")) |
| .WillOnce(Return(false)); |
| } |
| |
| EXPECT_TRUE(ExecJs(shell(), R"( |
| (async () => { |
| let context = await navigator.smartCard.establishContext(); |
| |
| let connection = |
| (await context.connect("Fake reader", "shared", |
| {preferredProtocols: ["t1"]})).connection; |
| })())")); |
| |
| TestFuture<void> watcher_closed; |
| ASSERT_TRUE(watcher.is_connected()); |
| watcher.set_disconnect_handler(base::BindOnce( |
| [](TestFuture<void>* watcher_closed) { watcher_closed->SetValue(); }, |
| &watcher_closed)); |
| ASSERT_TRUE(observer_store); |
| auto& observer = CHECK_DEREF(observer_store.get()); |
| |
| observer.OnPermissionRevoked(url::Origin::Create(GetIsolatedContextUrl())); |
| EXPECT_TRUE(watcher_closed.Wait()); |
| ASSERT_FALSE(watcher.is_connected()); |
| |
| content::FetchHistogramsFromChildProcesses(); |
| histogram_tester_.ExpectUniqueSample( |
| "SmartCard.ConnectionClosedReason", |
| SmartCardConnectionClosedReason:: |
| kSmartCardConnectionClosedPermissionRevoked, |
| 1); |
| } |
| |
| } // namespace content |