aboutsummaryrefslogtreecommitdiff
path: root/ssh-agent-filter.C
diff options
context:
space:
mode:
authorTimo Weingärtner <timo@tiwe.de>2026-03-07 22:36:10 +0100
committerTimo Weingärtner <timo@tiwe.de>2026-03-07 22:36:10 +0100
commit960aa108a9cd4ff31932653fdc847d492901a37b (patch)
tree905386c7eb0a7d470ff90b8428f2a70f3327743e /ssh-agent-filter.C
parentf47546025f34ff4294b7f6b3e672c521c8d2d3f5 (diff)
parent8bd8f61f2ab2978a0a1bf7aa6f19a6352b9769c9 (diff)
downloadssh-agent-filter-960aa108a9cd4ff31932653fdc847d492901a37b.tar.gz
Merge tag '0.5.3' into debian
0.5.3
Diffstat (limited to 'ssh-agent-filter.C')
-rw-r--r--ssh-agent-filter.C341
1 files changed, 177 insertions, 164 deletions
diff --git a/ssh-agent-filter.C b/ssh-agent-filter.C
index b6d906b..8a2772d 100644
--- a/ssh-agent-filter.C
+++ b/ssh-agent-filter.C
@@ -22,8 +22,8 @@
#include <boost/program_options.hpp>
namespace po = boost::program_options;
-#include <boost/filesystem.hpp>
-namespace fs = boost::filesystem;
+#include <filesystem>
+namespace fs = std::filesystem;
#include <boost/iostreams/stream.hpp>
#include <boost/iostreams/device/array.hpp>
@@ -61,8 +61,8 @@ using std::pair;
#include <thread>
#include <mutex>
-using std::mutex;
-using std::lock_guard;
+
+#include <optional>
#include <chrono>
@@ -77,6 +77,7 @@ using std::lock_guard;
#include <sys/un.h>
#include <sys/wait.h>
#include <sysexits.h>
+#include <nettle/version.h>
#include <nettle/md5.h>
#include <nettle/base64.h>
#include <nettle/base16.h>
@@ -88,6 +89,9 @@ using std::lock_guard;
#ifndef SOCK_CLOEXEC
#define SOCK_CLOEXEC 0
#endif
+#ifndef SOCK_NONBLOCK
+#define SOCK_NONBLOCK 0
+#endif
vector<string> allowed_b64;
vector<string> allowed_md5;
@@ -101,57 +105,74 @@ bool debug{false};
bool all_confirmed{false};
string saf_name;
fs::path path;
-mutex fd_fork_mutex;
+std::mutex fd_fork_mutex;
string md5_hex (string const & s) {
struct md5_ctx ctx;
md5_init(&ctx);
md5_update(&ctx, s.size(), reinterpret_cast<uint8_t const *>(s.data()));
- uint8_t bin[MD5_DIGEST_SIZE];
- md5_digest(&ctx, MD5_DIGEST_SIZE, bin);
- char hex[BASE16_ENCODE_LENGTH(MD5_DIGEST_SIZE)];
- base16_encode_update(hex, MD5_DIGEST_SIZE, bin);
- return {hex, sizeof(hex)};
+ std::array<uint8_t, MD5_DIGEST_SIZE> bin;
+#if (NETTLE_VERSION_MAJOR >= 4)
+ md5_digest(&ctx, bin.data());
+#else
+ md5_digest(&ctx, MD5_DIGEST_SIZE, bin.data());
+#endif
+ std::array<char, BASE16_ENCODE_LENGTH(MD5_DIGEST_SIZE)> hex;
+ base16_encode_update(hex.data(), MD5_DIGEST_SIZE, bin.data());
+ return {begin(hex), end(hex)};
}
string base64_encode (string const & s) {
- char b64[BASE64_ENCODE_RAW_LENGTH(s.size())];
- base64_encode_raw(b64, s.size(), reinterpret_cast<uint8_t const *>(s.data()));
- return {b64, sizeof(b64)};
+ std::vector<char> b64(BASE64_ENCODE_RAW_LENGTH(s.size()));
+ base64_encode_raw(b64.data(), s.size(), reinterpret_cast<uint8_t const *>(s.data()));
+ return {begin(b64), end(b64)};
}
-void cloexec (int fd) {
- if (fcntl(fd, F_SETFD, fcntl(fd, F_GETFD) | FD_CLOEXEC))
+template <int write_cmd, int set_flags, int clear_flags>
+void alter_fd_flags (int const fd) {
+ static_assert(write_cmd == F_SETFD || write_cmd == F_SETFL, "unsupported fcntl() operation");
+ constexpr int read_cmd = (write_cmd == F_SETFD) ? F_GETFD : F_GETFL;
+ static_assert((set_flags & clear_flags) == 0, "overlap in set and clear");
+
+ int const current_flags = fcntl(fd, read_cmd);
+ if (current_flags == -1)
throw system_error(errno, system_category(), "fcntl");
+ int const new_flags = (current_flags | set_flags) & ~clear_flags;
+ if (current_flags == new_flags)
+ return;
+ if (fcntl(fd, write_cmd, new_flags))
+ throw system_error(errno, system_category(), "fcntl");
+}
+
+int make_cloexec_socket (int const address_family, int const type, int const protocol) {
+ int sock;
+ std::lock_guard lock{fd_fork_mutex};
+ if ((sock = socket(address_family, type | SOCK_CLOEXEC, protocol)) == -1)
+ throw system_error(errno, system_category(), "socket");
+ alter_fd_flags<F_SETFD, FD_CLOEXEC, 0>(sock);
+ return sock;
}
-void arm(std::ios & stream) {
+void arm_exceptions (std::ios & stream) {
stream.exceptions(stream.badbit | stream.failbit);
}
int make_upstream_agent_conn () {
char const * path;
- int sock;
- struct sockaddr_un addr;
if (!(path = getenv("SSH_AUTH_SOCK")))
throw invalid_argument("no $SSH_AUTH_SOCK");
- {
- lock_guard<mutex> lock{fd_fork_mutex};
- if ((sock = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0)) == -1)
- throw system_error(errno, system_category(), "socket");
- cloexec(sock);
- }
+ int const sock = make_cloexec_socket(AF_UNIX, SOCK_STREAM, 0);
- addr.sun_family = AF_UNIX;
+ struct sockaddr_un addr{AF_UNIX, {}};
- if (strlen(path) >= sizeof(addr.sun_path))
+ if (auto len = strlen(path); len < sizeof(addr.sun_path))
+ std::copy(path, path + len, addr.sun_path);
+ else
throw length_error("$SSH_AUTH_SOCK too long");
- strcpy(addr.sun_path, path);
-
if (connect(sock, reinterpret_cast<struct sockaddr const *>(&addr), sizeof(addr)))
throw system_error(errno, system_category(), "connect");
@@ -159,26 +180,17 @@ int make_upstream_agent_conn () {
}
int make_listen_sock () {
- int sock;
- struct sockaddr_un addr;
-
- {
- lock_guard<mutex> lock{fd_fork_mutex};
- if ((sock = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0)) == -1)
- throw system_error(errno, system_category(), "socket");
- cloexec(sock);
- }
+ int const sock = make_cloexec_socket(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK, 0);
- if (fcntl(sock, F_SETFL, fcntl(sock, F_GETFL) | O_NONBLOCK))
- throw system_error(errno, system_category(), "fcntl");
+ alter_fd_flags<F_SETFL, O_NONBLOCK, 0>(sock);
- addr.sun_family = AF_UNIX;
+ struct sockaddr_un addr{AF_UNIX, {}};
- if (path.native().length() >= sizeof(addr.sun_path))
+ if (path.native().length() < sizeof(addr.sun_path))
+ std::copy(path.native().begin(), path.native().end(), addr.sun_path);
+ else
throw length_error("path for listen socket too long");
- strcpy(addr.sun_path, path.c_str());
-
if (bind(sock, reinterpret_cast<struct sockaddr const *>(&addr), sizeof(addr)))
throw system_error(errno, system_category(), "bind");
@@ -188,6 +200,14 @@ int make_listen_sock () {
return sock;
}
+rfc4251::string ask_upstream_agent (rfc4251::string const & request) {
+ io::stream<io::file_descriptor> agent{make_upstream_agent_conn(), io::close_handle};
+ arm_exceptions(agent);
+
+ agent << request;
+ return rfc4251::string{agent};
+}
+
void parse_cmdline (int const argc, char const * const * const argv) {
po::options_description opts{"Options"};
opts.add_options()
@@ -200,7 +220,7 @@ void parse_cmdline (int const argc, char const * const * const argv) {
("help,h", "print this help message")
("key,k", po::value(&allowed_b64), "key specified by base64-encoded pubkey")
("key-confirmed,K", po::value(&confirmed_b64), "key specified by base64-encoded pubkey, with confirmation")
- ("name,n", po::value(&saf_name), "name for this instance of ssh-agent-filter, for confirmation puposes")
+ ("name,n", po::value(&saf_name), "name for this instance of ssh-agent-filter, for confirmation purposes")
("version,V", "print version information")
;
po::variables_map config;
@@ -235,28 +255,24 @@ void parse_cmdline (int const argc, char const * const * const argv) {
}
void setup_filters () {
- io::stream<io::file_descriptor> agent{make_upstream_agent_conn(), io::close_handle};
- arm(agent);
-
- agent << rfc4251::string{string{SSH2_AGENTC_REQUEST_IDENTITIES}};
- rfc4251::string answer{agent};
+ auto const answer = ask_upstream_agent({SSH2_AGENTC_REQUEST_IDENTITIES});
io::stream<io::array_source> answer_iss{answer.data(), answer.size()};
- arm(answer_iss);
- rfc4251::byte resp_code{answer_iss};
+ arm_exceptions(answer_iss);
+ rfc4251::byte const resp_code{answer_iss};
if (resp_code != SSH2_AGENT_IDENTITIES_ANSWER)
throw runtime_error{"unexpected answer from ssh-agent"};
- rfc4251::uint32 keycount{answer_iss};
+ rfc4251::uint32 const keycount{answer_iss};
for (uint32_t i = keycount; i; --i) {
- rfc4251::string key{answer_iss};
- rfc4251::string comment{answer_iss};
+ rfc4251::string key{answer_iss};
+ rfc4251::string const comment{answer_iss};
- auto b64 = base64_encode(key);
+ auto const b64 = base64_encode(key);
if (debug) clog << b64 << endl;
- auto md5 = md5_hex(key);
+ auto const md5 = md5_hex(key);
if (debug) clog << md5 << endl;
- string comm(comment);
+ string comm{comment};
if (debug) clog << comm << endl;
bool allow{false};
@@ -308,16 +324,16 @@ bool confirm (string const & question) {
sap = "ssh-askpass";
pid_t pid;
{
- lock_guard<mutex> lock{fd_fork_mutex};
+ std::lock_guard lock{fd_fork_mutex};
pid = fork();
}
if (pid < 0)
throw runtime_error("fork()");
if (pid == 0) {
// child
- char const * args[3] = { sap, question.c_str(), nullptr };
+ std::array<char const *, 3> const args{ sap, question.c_str(), nullptr };
// see execvp(3p) for cast rationale
- execvp(sap, const_cast<char * const *>(args));
+ execvp(sap, const_cast<char * const *>(args.data()));
throw system_error(errno, system_category(), "exec");
} else {
// parent
@@ -326,97 +342,99 @@ bool confirm (string const & question) {
}
}
-bool dissect_auth_data_ssh_cert (rfc4251::string const & data, string & request_description) try {
+std::optional<string> dissect_auth_data_ssh_cert (rfc4251::string const & data) try {
io::stream<io::array_source> datastream{data.data(), data.size()};
- arm(datastream);
+ arm_exceptions(datastream);
+ string request_description{};
// Format specified in https://cvsweb.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/PROTOCOL.certkeys?annotate=1.13
- rfc4251::string keytype{datastream};
+ rfc4251::string const keytype{datastream};
std::string keytype_str{keytype};
{
// check for and remove suffix to get the base keytype
std::string const suffix{"-cert-v01@openssh.com"};
if (keytype_str.length() <= suffix.length())
- return false;
+ return {};
auto suffix_start = keytype_str.end() - suffix.length();
if (!std::equal(suffix.begin(), suffix.end(), suffix_start))
- return false;
+ return {};
keytype_str.erase(suffix_start, keytype_str.end());
}
- rfc4251::string nonce{datastream};
+ rfc4251::string const nonce{datastream};
std::ostringstream key_to_be_signed{};
if (keytype_str == "ssh-rsa") {
- rfc4251::string e{datastream};
- rfc4251::string n{datastream};
+ rfc4251::mpint const e{datastream};
+ rfc4251::mpint const n{datastream};
key_to_be_signed << rfc4251::string{keytype_str} << e << n;
} else if (keytype_str == "ssh-dss") {
- rfc4251::string p{datastream};
- rfc4251::string q{datastream};
- rfc4251::string g{datastream};
- rfc4251::string y{datastream};
+ rfc4251::mpint const p{datastream};
+ rfc4251::mpint const q{datastream};
+ rfc4251::mpint const g{datastream};
+ rfc4251::mpint const y{datastream};
key_to_be_signed << rfc4251::string{keytype_str} << p << q << g << y;
} else if (keytype_str == "ecdsa-sha2-nistp256"
|| keytype_str == "ecdsa-sha2-nistp384"
|| keytype_str == "ecdsa-sha2-nistp521") {
- rfc4251::string curve{datastream};
- rfc4251::string public_key{datastream};
+ rfc4251::string const curve{datastream};
+ rfc4251::string const public_key{datastream};
key_to_be_signed << rfc4251::string{keytype_str} << curve << public_key;
} else if (keytype_str == "ssh-ed25519") {
- rfc4251::string pk{datastream};
+ rfc4251::string const pk{datastream};
key_to_be_signed << rfc4251::string{keytype_str} << pk;
} else {
- return false;
+ return {};
}
- rfc4251::uint64 serial{datastream};
- rfc4251::uint32 type{datastream};
- rfc4251::string key_id{datastream};
- rfc4251::string valid_principals{datastream};
- rfc4251::uint64 valid_after{datastream};
- rfc4251::uint64 valid_before{datastream};
- rfc4251::string critical_options{datastream};
- rfc4251::string extensions{datastream};
- rfc4251::string reserved{datastream};
- rfc4251::string signature_key{datastream};
+ rfc4251::uint64 const serial{datastream};
+ rfc4251::uint32 const type{datastream};
+ rfc4251::string const key_id{datastream};
+ rfc4251::string const valid_principals{datastream};
+ rfc4251::uint64 const valid_after{datastream};
+ rfc4251::uint64 const valid_before{datastream};
+ rfc4251::string const critical_options{datastream};
+ rfc4251::string const extensions{datastream};
+ rfc4251::string const reserved{datastream};
+ rfc4251::string const signature_key{datastream};
request_description = "The request is for a certificate signature on key " + base64_encode(key_to_be_signed.str()) + ".";
- return true;
+ return request_description;
} catch (...) {
- return false;
+ return {};
}
-bool dissect_auth_data_ssh (rfc4251::string const & data, string & request_description) try {
+std::optional<string> dissect_auth_data_ssh (rfc4251::string const & data) try {
io::stream<io::array_source> datastream{data.data(), data.size()};
- arm(datastream);
+ arm_exceptions(datastream);
+ string request_description{};
// Format specified in RFC 4252 Section 7
- rfc4251::string session_identifier{datastream};
- rfc4251::byte requesttype{datastream};
- rfc4251::string username{datastream};
- rfc4251::string servicename{datastream};
- rfc4251::string publickeystring{datastream};
- rfc4251::boolean shouldbetrue{datastream};
- rfc4251::string publickeyalgorithm{datastream};
- rfc4251::string publickey{datastream};
+ rfc4251::string const session_identifier{datastream};
+ rfc4251::byte const requesttype{datastream};
+ rfc4251::string const username{datastream};
+ rfc4251::string const servicename{datastream};
+ rfc4251::string const publickeystring{datastream};
+ rfc4251::boolean const shouldbetrue{datastream};
+ rfc4251::string const publickeyalgorithm{datastream};
+ rfc4251::string const publickey{datastream};
request_description = "The request is for an ssh connection as user '" + string{username} + "' with service name '" + string{servicename} + "'.";
if (string{servicename} == "pam_ssh_agent_auth") try {
clog << base64_encode(session_identifier) << endl;
io::stream<io::array_source> idstream{session_identifier.data(), session_identifier.size()};
- arm(idstream);
+ arm_exceptions(idstream);
- rfc4251::uint32 type{idstream};
+ rfc4251::uint32 const type{idstream};
if (type == 101) {
// PAM_SSH_AGENT_AUTH_REQUESTv1
- rfc4251::string cookie{idstream};
- rfc4251::string user{idstream};
- rfc4251::string ruser{idstream};
- rfc4251::string pam_service{idstream};
- rfc4251::string pwd{idstream};
- rfc4251::string action{idstream};
- rfc4251::string hostname{idstream};
- rfc4251::uint64 timestamp{idstream};
+ rfc4251::string const cookie{idstream};
+ rfc4251::string const user{idstream};
+ rfc4251::string const ruser{idstream};
+ rfc4251::string const pam_service{idstream};
+ rfc4251::string const pwd{idstream};
+ rfc4251::string const action{idstream};
+ rfc4251::string const hostname{idstream};
+ rfc4251::uint64 const timestamp{idstream};
string singleuser{user};
if (user != ruser)
@@ -427,14 +445,14 @@ bool dissect_auth_data_ssh (rfc4251::string const & data, string & request_descr
additional += "' in '" + string{pwd};
io::stream<io::array_source> actionstream{action.data(), action.size()};
- arm(actionstream);
+ arm_exceptions(actionstream);
- rfc4251::uint32 argc{actionstream};
+ rfc4251::uint32 const argc{actionstream};
if (argc) {
additional += " to run";
for (uint32_t i = argc; i; --i) {
- rfc4251::string argv{actionstream};
+ rfc4251::string const argv{actionstream};
additional += ' ' + string{argv};
}
}
@@ -450,34 +468,45 @@ bool dissect_auth_data_ssh (rfc4251::string const & data, string & request_descr
}
} catch (...) {}
- return true;
+ return request_description;
} catch (...) {
- return false;
+ return {};
+}
+
+string describe_sign_request (rfc4251::string const & data_to_be_signed) {
+ auto const dissectors = {
+ dissect_auth_data_ssh_cert,
+ dissect_auth_data_ssh,
+ };
+ std::optional<string> request_description;
+ for (auto const dissector : dissectors)
+ if (request_description = dissector(data_to_be_signed); request_description)
+ break;
+
+ return request_description.value_or("The request format is unknown.");
}
+
rfc4251::string handle_request (rfc4251::string const & r) {
io::stream<io::array_source> request{r.data(), r.size()};
rfc4251::string ret;
- io::stream<io::back_insert_device<vector<char>>> answer{ret.value};
- arm(request);
- arm(answer);
- rfc4251::byte request_code{request};
+ io::stream<io::back_insert_device<vector<char>>> answer{ret.buf};
+ arm_exceptions(request);
+ arm_exceptions(answer);
+ rfc4251::byte const request_code{request};
switch (request_code) {
case SSH2_AGENTC_REQUEST_IDENTITIES:
{
- io::stream<io::file_descriptor> agent{make_upstream_agent_conn(), io::close_handle};
- arm(agent);
- agent << rfc4251::string{string{SSH2_AGENTC_REQUEST_IDENTITIES}};
+ auto const agent_answer = ask_upstream_agent({SSH2_AGENTC_REQUEST_IDENTITIES});
// temp to test key filtering when signing
- //return rfc4251::string{agent};
- rfc4251::string agent_answer{agent};
+ //return agent_answer;
io::stream<io::array_source> agent_answer_iss{agent_answer.data(), agent_answer.size()};
- arm(agent_answer_iss);
- rfc4251::byte answer_code{agent_answer_iss};
- rfc4251::uint32 keycount{agent_answer_iss};
+ arm_exceptions(agent_answer_iss);
+ rfc4251::byte const answer_code{agent_answer_iss};
+ rfc4251::uint32 const keycount{agent_answer_iss};
if (answer_code != SSH2_AGENT_IDENTITIES_ANSWER)
throw runtime_error{"unexpected answer from ssh-agent"};
- vector<pair<rfc4251::string, rfc4251::string>> keys;
+ vector<pair<rfc4251::string const, rfc4251::string const>> keys;
for (uint32_t i = keycount; i; --i) {
rfc4251::string key{agent_answer_iss};
rfc4251::string comment{agent_answer_iss};
@@ -491,41 +520,24 @@ rfc4251::string handle_request (rfc4251::string const & r) {
break;
case SSH2_AGENTC_SIGN_REQUEST:
{
- rfc4251::string key{request};
- rfc4251::string data{request};
- rfc4251::uint32 flags{request};
+ rfc4251::string const key{request};
+ rfc4251::string const data_to_be_signed{request};
+ rfc4251::uint32 const flags{request};
bool allow{false};
if (allowed_pubkeys.count(key))
allow = true;
- else {
- auto it = confirmed_pubkeys.find(key);
- if (it != confirmed_pubkeys.end()) {
- string request_description;
- bool dissect_ok{false};
- if (!dissect_ok)
- dissect_ok = dissect_auth_data_ssh_cert(data, request_description);
- if (!dissect_ok)
- dissect_ok = dissect_auth_data_ssh(data, request_description);
- if (!dissect_ok)
- request_description = "The request format is unknown.";
-
- string question = "Something behind the ssh-agent-filter";
- if (saf_name.length())
- question += " named '" + saf_name + "'";
- question += " requested use of the key named '" + it->second + "'.\n";
- question += request_description;
- allow = confirm(question);
- }
+ else if (auto it = confirmed_pubkeys.find(key); it != confirmed_pubkeys.end()) {
+ string question = "Something behind the ssh-agent-filter";
+ if (saf_name.length())
+ question += " named '" + saf_name + "'";
+ question += " requested use of the key named '" + it->second + "'.\n";
+ question += describe_sign_request(data_to_be_signed);
+ allow = confirm(question);
}
if (allow) {
- io::stream<io::file_descriptor> agent{make_upstream_agent_conn(), io::close_handle};
- arm(agent);
- rfc4251::string agent_answer;
-
- agent << r;
- return rfc4251::string{agent};
+ return ask_upstream_agent(r);
} else
answer << rfc4251::byte{SSH_AGENT_FAILURE};
}
@@ -561,15 +573,14 @@ rfc4251::string handle_request (rfc4251::string const & r) {
}
void handle_client (int const sock) try {
- if (fcntl(sock, F_SETFL, fcntl(sock, F_GETFL) & ~O_NONBLOCK))
- throw system_error(errno, system_category(), "fcntl");
+ alter_fd_flags<F_SETFL, 0, O_NONBLOCK>(sock);
// we could use only one streambuf and iostream but when
// switching from read to write an lseek call is made that
// fails with ESPIPE and causes an exception
io::stream<io::file_descriptor_source> client_in{sock, io::close_handle};
io::stream<io::file_descriptor_sink> client_out{sock, io::never_close_handle};
- arm(client_out);
+ arm_exceptions(client_out);
for (;;)
client_out << handle_request(rfc4251::string{client_in}) << flush;
@@ -608,11 +619,13 @@ int main (int const argc, char const * const * const argv) {
// the following stuff is optional, so we don't do error checking
setsid();
static_cast<void>(chdir("/"));
- int devnull = open("/dev/null", O_RDWR);
- dup2(devnull, 0);
- dup2(devnull, 1);
- dup2(devnull, 2);
- close(devnull);
+ if (int devnull = open("/dev/null", O_RDWR); devnull != -1) {
+ dup2(devnull, 0);
+ dup2(devnull, 1);
+ dup2(devnull, 2);
+ if (devnull > 2)
+ close(devnull);
+ }
} else {
cout << "copy this to another terminal:" << endl;
cout << "SSH_AUTH_SOCK='" << path.native() << "'; export SSH_AUTH_SOCK;" << endl;
@@ -630,14 +643,14 @@ int main (int const argc, char const * const * const argv) {
select(listen_sock + 1, &fds, nullptr, nullptr, nullptr);
int client_sock;
{
- lock_guard<mutex> lock{fd_fork_mutex};
+ std::lock_guard lock{fd_fork_mutex};
if ((client_sock = accept(listen_sock, nullptr, nullptr)) == -1) {
if (errno == EAGAIN || errno == EWOULDBLOCK)
continue;
else
break;
}
- cloexec(client_sock);
+ alter_fd_flags<F_SETFD, FD_CLOEXEC, 0>(client_sock);
}
std::thread t{handle_client, client_sock};
t.detach();