// 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/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/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/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 "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"
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::StrictMock;
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));
void ExpectHasReaderPermission(const std::string& reader_name) {
EXPECT_CALL(*this, HasReaderPermission(_, reader_name))
.WillOnce(Return(true));
}
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<blink::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;
GetFakeSmartCardDelegate().ExpectHasReaderPermission(kFakeReader);
mock_context_factory.ExpectConnectFakeReaderSharedT1(connection_receiver);
mock_connection.ExpectBeginTransaction(transaction_receiver);
mock_transaction.ExpectEndTransaction(SmartCardDisposition::kReset);
}
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};
};
} // 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<blink::ParsedPermissionsPolicy>
SmartCardTestContentBrowserClient::GetPermissionsPolicyForIsolatedWebApp(
WebContents* web_contents,
const url::Origin& app_origin) {
blink::ParsedPermissionsPolicyDeclaration coi_decl(
blink::mojom::PermissionsPolicyFeature::kCrossOriginIsolated,
/*allowed_origins=*/{},
/*self_if_matches=*/std::nullopt, /*matches_all_origins=*/true,
/*matches_opaque_src=*/false);
blink::ParsedPermissionsPolicyDeclaration smart_card_decl(
blink::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`;
})())"));
}
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,
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([&connection_receiver](
const std::string& reader,
device::mojom::SmartCardShareMode share_mode,
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([](uint32_t control_code, 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(
[](uint32_t tag, 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([](uint32_t tag, const std::vector<uint8_t>& data,
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(
[](base::TimeDelta timeout,
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, 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(
[&get_status_callback](
base::TimeDelta timeout,
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([](const std::string& reader,
device::mojom::SmartCardShareMode share_mode,
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", 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;
}
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([](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}));
});
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([](SmartCardDisposition disposition,
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([](SmartCardDisposition disposition,
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([](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}));
});
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([&connection_receiver](
uint32_t control_code, const std::vector<uint8_t>& data,
SmartCardConnection::ControlCallback callback) {
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([&mock_context_factory](
uint32_t control_code, const std::vector<uint8_t>& data,
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<blink::ParsedPermissionsPolicy>
GetPermissionsPolicyForIsolatedWebApp(
WebContents* web_contents,
const url::Origin& app_origin) override {
return {{blink::ParsedPermissionsPolicyDeclaration(
blink::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()").error,
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";
})())"));
}
} // namespace content