mirror of
https://github.com/Noratrieb/cluelessh.git
synced 2026-01-14 16:35:06 +01:00
agent
This commit is contained in:
parent
9b49e09983
commit
a52b6b37d7
19 changed files with 770 additions and 92 deletions
47
Cargo.lock
generated
47
Cargo.lock
generated
|
|
@ -137,6 +137,12 @@ version = "0.2.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.22.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||
|
||||
[[package]]
|
||||
name = "base64ct"
|
||||
version = "1.6.0"
|
||||
|
|
@ -208,9 +214,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.15"
|
||||
version = "4.5.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "11d8838454fda655dafd3accb2b6e2bea645b9e4078abe84a22ceb947235c5cc"
|
||||
checksum = "ed6719fffa43d0d87e5fd8caeab59be1554fb028cd30edc88fc4369b17971019"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
|
|
@ -597,6 +603,12 @@ version = "0.3.9"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
|
||||
|
||||
[[package]]
|
||||
name = "hex"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||
|
||||
[[package]]
|
||||
name = "hex-literal"
|
||||
version = "0.4.1"
|
||||
|
|
@ -1155,6 +1167,7 @@ dependencies = [
|
|||
"eyre",
|
||||
"rand",
|
||||
"rpassword",
|
||||
"ssh-agent-client",
|
||||
"ssh-protocol",
|
||||
"ssh-transport",
|
||||
"tokio",
|
||||
|
|
@ -1162,6 +1175,31 @@ dependencies = [
|
|||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ssh-agent-client"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"eyre",
|
||||
"ssh-transport",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ssh-agentctl"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"eyre",
|
||||
"hex",
|
||||
"rpassword",
|
||||
"sha2",
|
||||
"ssh-agent-client",
|
||||
"ssh-transport",
|
||||
"tokio",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ssh-connection"
|
||||
version = "0.1.0"
|
||||
|
|
@ -1186,6 +1224,7 @@ version = "0.1.0"
|
|||
dependencies = [
|
||||
"aes",
|
||||
"aes-gcm",
|
||||
"base64",
|
||||
"chacha20",
|
||||
"crypto-bigint",
|
||||
"ctr",
|
||||
|
|
@ -1251,9 +1290,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.39.2"
|
||||
version = "1.39.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "daa4fb1bc778bd6f04cbfc4bb2d06a7396a8f299dc33ea1900cedaa316f467b1"
|
||||
checksum = "9babc99b9923bfa4804bd74722ff02c0381021eafa4db9949217e3be8e84fff5"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"bytes",
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ members = [
|
|||
"ssh-connection",
|
||||
"ssh-protocol",
|
||||
"ssh-transport",
|
||||
"ssh-agent-client", "ssh-agentctl",
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
|
|
|
|||
10
ssh-agent-client/Cargo.toml
Normal file
10
ssh-agent-client/Cargo.toml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
[package]
|
||||
name = "ssh-agent-client"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
eyre = "0.6.12"
|
||||
ssh-transport = { path = "../ssh-transport" }
|
||||
tokio = { version = "1.39.3", features = ["net"] }
|
||||
tracing.workspace = true
|
||||
4
ssh-agent-client/README.md
Normal file
4
ssh-agent-client/README.md
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
# ssh-agent-client
|
||||
|
||||
Client for the SSH agent protocol specified in https://datatracker.ietf.org/doc/html/draft-miller-ssh-agent
|
||||
and https://github.com/openssh/openssh-portable/blob/a76a6b85108e3032c8175611ecc5746e7131f876/PROTOCOL.agent.
|
||||
336
ssh-agent-client/src/lib.rs
Normal file
336
ssh-agent-client/src/lib.rs
Normal file
|
|
@ -0,0 +1,336 @@
|
|||
use eyre::{bail, eyre, Context};
|
||||
use ssh_transport::{
|
||||
packet::PacketParser,
|
||||
parse::{Parser, Writer},
|
||||
SshStatus,
|
||||
};
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tracing::{debug, trace};
|
||||
|
||||
/// A message to send to the byte stream.
|
||||
pub enum Request {
|
||||
RemoveAllIdentities,
|
||||
ListIdentities,
|
||||
Sign {
|
||||
key_blob: Vec<u8>,
|
||||
data: Vec<u8>,
|
||||
flags: u32,
|
||||
},
|
||||
Lock {
|
||||
passphrase: String,
|
||||
},
|
||||
Unlock {
|
||||
passphrase: String,
|
||||
},
|
||||
Extension(ExtensionRequest),
|
||||
}
|
||||
|
||||
pub enum ExtensionRequest {
|
||||
Query,
|
||||
}
|
||||
|
||||
impl Request {
|
||||
pub fn to_bytes(&self) -> Vec<u8> {
|
||||
let mut p = Writer::new();
|
||||
match self {
|
||||
Self::RemoveAllIdentities => p.u8(numbers::SSH_AGENTC_REMOVE_ALL_IDENTITIES),
|
||||
Self::ListIdentities => p.u8(numbers::SSH_AGENTC_REQUEST_IDENTITIES),
|
||||
Self::Sign {
|
||||
key_blob,
|
||||
data,
|
||||
flags,
|
||||
} => {
|
||||
p.u8(numbers::SSH_AGENTC_SIGN_REQUEST);
|
||||
p.string(&key_blob);
|
||||
p.string(&data);
|
||||
p.u32(*flags);
|
||||
}
|
||||
Self::Lock { passphrase } => {
|
||||
p.u8(numbers::SSH_AGENTC_LOCK);
|
||||
p.string(passphrase.as_bytes());
|
||||
}
|
||||
Self::Unlock { passphrase } => {
|
||||
p.u8(numbers::SSH_AGENTC_UNLOCK);
|
||||
p.string(passphrase.as_bytes());
|
||||
}
|
||||
Self::Extension(ext) => {
|
||||
p.u8(numbers::SSH_AGENTC_EXTENSION);
|
||||
match ext {
|
||||
ExtensionRequest::Query => {
|
||||
p.string(b"query");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut buf = p.finish();
|
||||
let len = u32::try_from(buf.len()).unwrap();
|
||||
buf.splice(0..0, len.to_be_bytes());
|
||||
buf
|
||||
}
|
||||
}
|
||||
|
||||
/// A server response for an agent message.
|
||||
#[derive(Debug)]
|
||||
pub enum ServerResponse {
|
||||
/// SSH_AGENT_SUCCESS
|
||||
Success,
|
||||
/// SSH_AGENT_FAILURE
|
||||
Failure,
|
||||
|
||||
IdentitiesAnswer {
|
||||
identities: Vec<IdentityAnswer>,
|
||||
},
|
||||
|
||||
/// SSH_AGENT_SIGN_RESPONSE
|
||||
SignResponse {
|
||||
signature: Vec<u8>,
|
||||
},
|
||||
|
||||
Extension(ExtensionResponse),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ExtensionResponse {
|
||||
Query { types: Vec<String> },
|
||||
}
|
||||
|
||||
/// A single identity in SSH_AGENT_IDENTITIES_ANSWER.
|
||||
#[derive(Debug)]
|
||||
pub struct IdentityAnswer {
|
||||
pub key_blob: Vec<u8>,
|
||||
pub comment: String,
|
||||
}
|
||||
|
||||
impl ServerResponse {
|
||||
pub fn parse(bytes: &[u8]) -> eyre::Result<Self> {
|
||||
let bytes = &bytes[4..];
|
||||
let mut p = Parser::new(bytes);
|
||||
let msg_type = p.u8()?;
|
||||
trace!(%msg_type, msg_type_str = %numbers::server_response_type_to_string(msg_type), "Received message");
|
||||
let resp = match msg_type {
|
||||
numbers::SSH_AGENT_FAILURE => Self::Failure,
|
||||
numbers::SSH_AGENT_SUCCESS => Self::Success,
|
||||
numbers::SSH_AGENT_IDENTITIES_ANSWER => {
|
||||
let nkeys = p.u32()?;
|
||||
let mut identities = Vec::new();
|
||||
for _ in 0..nkeys {
|
||||
let key_blob = p.string()?;
|
||||
let comment = p.utf8_string()?;
|
||||
identities.push(IdentityAnswer {
|
||||
key_blob: key_blob.to_owned(),
|
||||
comment: comment.to_owned(),
|
||||
});
|
||||
}
|
||||
Self::IdentitiesAnswer { identities }
|
||||
}
|
||||
numbers::SSH_AGENT_SIGN_RESPONSE => {
|
||||
let signature = p.string()?;
|
||||
Self::SignResponse {
|
||||
signature: signature.to_owned(),
|
||||
}
|
||||
}
|
||||
numbers::SSH_AGENT_EXTENSION_RESPONSE => {
|
||||
let ext_type = p.utf8_string()?;
|
||||
trace!(?ext_type, "Received extension response");
|
||||
match ext_type {
|
||||
"query" => {
|
||||
let mut types = Vec::new();
|
||||
while p.has_data() {
|
||||
let name = p.utf8_string()?;
|
||||
types.push(name.to_owned());
|
||||
}
|
||||
Self::Extension(ExtensionResponse::Query { types })
|
||||
}
|
||||
_ => bail!("unknown extension response type: {ext_type}"),
|
||||
}
|
||||
}
|
||||
_ => bail!(
|
||||
"unknown server message: {msg_type} ({})",
|
||||
numbers::server_response_type_to_string(msg_type)
|
||||
),
|
||||
};
|
||||
Ok(resp)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AgentConnection {
|
||||
packets: PacketParser,
|
||||
}
|
||||
|
||||
impl AgentConnection {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
packets: PacketParser::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn recv_bytes<'a>(
|
||||
&'a mut self,
|
||||
mut bytes: &'a [u8],
|
||||
) -> impl Iterator<Item = eyre::Result<ServerResponse>> + 'a {
|
||||
std::iter::from_fn(move || -> Option<eyre::Result<ServerResponse>> {
|
||||
if bytes.len() == 0 {
|
||||
return None;
|
||||
}
|
||||
match self.packets.recv_plaintext_bytes(bytes) {
|
||||
Err(err) => Some(Err(match err {
|
||||
SshStatus::PeerError(err) => eyre!(err),
|
||||
SshStatus::Disconnect => unreachable!(),
|
||||
})),
|
||||
Ok(None) => None,
|
||||
Ok(Some((consumed, data))) => {
|
||||
bytes = &bytes[consumed..];
|
||||
self.packets = PacketParser::new();
|
||||
Some(ServerResponse::parse(&data))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SocketAgentConnection {
|
||||
conn: AgentConnection,
|
||||
uds: tokio::net::UnixStream,
|
||||
}
|
||||
|
||||
impl SocketAgentConnection {
|
||||
pub async fn from_env() -> eyre::Result<Self> {
|
||||
let sock = std::env::var("SSH_AUTH_SOCK").wrap_err("$SSH_AUTH_SOCK not found")?;
|
||||
|
||||
debug!(%sock, "Connecting to SSH agent");
|
||||
|
||||
let socket = tokio::net::UnixSocket::new_stream()
|
||||
.wrap_err("creating unix stream socket")?
|
||||
.connect(&sock)
|
||||
.await
|
||||
.wrap_err_with(|| format!("connecting to Unix stream socket on {sock}"))?;
|
||||
|
||||
Ok(Self {
|
||||
conn: AgentConnection::new(),
|
||||
uds: socket,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn remove_all_identities(&mut self) -> eyre::Result<()> {
|
||||
self.send(Request::RemoveAllIdentities).await?;
|
||||
self.generic_response().await
|
||||
}
|
||||
|
||||
pub async fn list_identities(&mut self) -> eyre::Result<Vec<IdentityAnswer>> {
|
||||
self.send(Request::ListIdentities).await?;
|
||||
|
||||
let resp = self.get_response().await?;
|
||||
match resp {
|
||||
ServerResponse::IdentitiesAnswer { identities } => Ok(identities),
|
||||
_ => bail!("unexpected response: {resp:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn sign(
|
||||
&mut self,
|
||||
key_blob: &[u8],
|
||||
data: &[u8],
|
||||
flags: u32,
|
||||
) -> eyre::Result<Vec<u8>> {
|
||||
self.send(Request::Sign {
|
||||
key_blob: key_blob.to_owned(),
|
||||
data: data.to_owned(),
|
||||
flags,
|
||||
})
|
||||
.await?;
|
||||
|
||||
let resp = self.get_response().await?;
|
||||
match resp {
|
||||
ServerResponse::SignResponse { signature } => Ok(signature),
|
||||
_ => bail!("unexpected response: {resp:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn lock(&mut self, passphrase: &str) -> eyre::Result<()> {
|
||||
self.send(Request::Lock {
|
||||
passphrase: passphrase.to_owned(),
|
||||
})
|
||||
.await?;
|
||||
self.generic_response().await
|
||||
}
|
||||
|
||||
pub async fn unlock(&mut self, passphrase: &str) -> eyre::Result<()> {
|
||||
self.send(Request::Unlock {
|
||||
passphrase: passphrase.to_owned(),
|
||||
})
|
||||
.await?;
|
||||
self.generic_response().await
|
||||
}
|
||||
|
||||
pub async fn extension_query(&mut self) -> eyre::Result<Vec<String>> {
|
||||
self.send(Request::Extension(ExtensionRequest::Query))
|
||||
.await?;
|
||||
let resp = self.get_response().await?;
|
||||
match resp {
|
||||
ServerResponse::Extension(ExtensionResponse::Query { types }) => Ok(types),
|
||||
_ => bail!("unexpected response: {resp:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
async fn generic_response(&mut self) -> eyre::Result<()> {
|
||||
let resp = self.get_response().await?;
|
||||
|
||||
match resp {
|
||||
ServerResponse::Success => Ok(()),
|
||||
ServerResponse::Failure => bail!("agent operation failed"),
|
||||
_ => bail!("unexpected response: {resp:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
async fn send(&mut self, msg: Request) -> eyre::Result<()> {
|
||||
self.uds.write_all(&msg.to_bytes()).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_response(&mut self) -> eyre::Result<ServerResponse> {
|
||||
let mut buf = [0_u8; 1024];
|
||||
loop {
|
||||
let read = self.uds.read(&mut buf).await?;
|
||||
let bytes = &buf[..read];
|
||||
// In practice, the server won't send more than one packet.
|
||||
if let Some(resp) = self.conn.recv_bytes(bytes).next() {
|
||||
return resp;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod numbers {
|
||||
pub const SSH_AGENTC_REQUEST_IDENTITIES: u8 = 11;
|
||||
pub const SSH_AGENTC_SIGN_REQUEST: u8 = 13;
|
||||
pub const SSH_AGENTC_ADD_IDENTITY: u8 = 17;
|
||||
pub const SSH_AGENTC_REMOVE_IDENTITY: u8 = 18;
|
||||
pub const SSH_AGENTC_REMOVE_ALL_IDENTITIES: u8 = 19;
|
||||
pub const SSH_AGENTC_ADD_SMARTCARD_KEY: u8 = 20;
|
||||
pub const SSH_AGENTC_REMOVE_SMARTCARD_KEY: u8 = 21;
|
||||
pub const SSH_AGENTC_LOCK: u8 = 22;
|
||||
pub const SSH_AGENTC_UNLOCK: u8 = 23;
|
||||
pub const SSH_AGENTC_ADD_ID_CONSTRAINED: u8 = 25;
|
||||
pub const SSH_AGENTC_ADD_SMARTCARD_KEY_CONSTRAINED: u8 = 26;
|
||||
pub const SSH_AGENTC_EXTENSION: u8 = 27;
|
||||
|
||||
pub const SSH_AGENT_FAILURE: u8 = 5;
|
||||
pub const SSH_AGENT_SUCCESS: u8 = 6;
|
||||
pub const SSH_AGENT_IDENTITIES_ANSWER: u8 = 12;
|
||||
pub const SSH_AGENT_SIGN_RESPONSE: u8 = 14;
|
||||
pub const SSH_AGENT_EXTENSION_FAILURE: u8 = 28;
|
||||
pub const SSH_AGENT_EXTENSION_RESPONSE: u8 = 29;
|
||||
|
||||
pub fn server_response_type_to_string(response_type: u8) -> &'static str {
|
||||
match response_type {
|
||||
SSH_AGENT_FAILURE => "SSH_AGENT_FAILURE",
|
||||
SSH_AGENT_SUCCESS => "SSH_AGENT_SUCCESS",
|
||||
SSH_AGENT_IDENTITIES_ANSWER => "SSH_AGENT_IDENTITIES_ANSWER",
|
||||
SSH_AGENT_SIGN_RESPONSE => "SSH_AGENT_SIGN_RESPONSE",
|
||||
SSH_AGENT_EXTENSION_FAILURE => "SSH_AGENT_EXTENSION_FAILURE",
|
||||
SSH_AGENT_EXTENSION_RESPONSE => "SSH_AGENT_EXTENSION_RESPONSE",
|
||||
_ => "<unknown>",
|
||||
}
|
||||
}
|
||||
}
|
||||
16
ssh-agentctl/Cargo.toml
Normal file
16
ssh-agentctl/Cargo.toml
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
[package]
|
||||
name = "ssh-agentctl"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
ssh-agent-client = { path = "../ssh-agent-client" }
|
||||
ssh-transport = { path = "../ssh-transport" }
|
||||
|
||||
clap = { version = "4.5.16", features = ["derive"] }
|
||||
eyre = "0.6.12"
|
||||
tokio = { version = "1.39.3", features = ["full"] }
|
||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
||||
rpassword = "7.3.1"
|
||||
sha2 = "0.10.8"
|
||||
hex = "0.4.3"
|
||||
7
ssh-agentctl/here
Normal file
7
ssh-agentctl/here
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
|
||||
QyNTUxOQAAACDpK6HZbsijDttnop9lQyLLGXZi7lS5Hb3bY7DKMDC1vAAAAIhd37wfXd+8
|
||||
HwAAAAtzc2gtZWQyNTUxOQAAACDpK6HZbsijDttnop9lQyLLGXZi7lS5Hb3bY7DKMDC1vA
|
||||
AAAEBCev7X+rchYbMmzYfiyBzZhV/RaZZhYh+MR4/Ktcu0l+krodluyKMO22ein2VDIssZ
|
||||
dmLuVLkdvdtjsMowMLW8AAAAA3V3dQEC
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
1
ssh-agentctl/here.pub
Normal file
1
ssh-agentctl/here.pub
Normal file
|
|
@ -0,0 +1 @@
|
|||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOkrodluyKMO22ein2VDIssZdmLuVLkdvdtjsMowMLW8 uwu
|
||||
156
ssh-agentctl/src/main.rs
Normal file
156
ssh-agentctl/src/main.rs
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use clap::Parser;
|
||||
use eyre::{bail, Context};
|
||||
use ssh_agent_client::{IdentityAnswer, SocketAgentConnection};
|
||||
use ssh_transport::key::SshPubkey;
|
||||
|
||||
#[derive(clap::Parser, Debug)]
|
||||
struct Args {
|
||||
#[command(subcommand)]
|
||||
command: Subcommand,
|
||||
}
|
||||
|
||||
#[derive(clap::Subcommand, Debug)]
|
||||
enum Subcommand {
|
||||
/// Remove all identities from the agent, SSH_AGENTC_REMOVE_ALL_IDENTITIES
|
||||
RemoveAllIdentities,
|
||||
/// List all identities in the agent, SSH_AGENTC_REQUEST_IDENTITIES
|
||||
ListIdentities {
|
||||
#[arg(short, long = "key-id")]
|
||||
key_id: bool,
|
||||
},
|
||||
/// Sign a blob, SSH_AGENTC_SIGN_REQUEST
|
||||
Sign {
|
||||
/// The key-id of the key, obtained with list-identities --key-id
|
||||
#[arg(short, long = "key")]
|
||||
key: Option<String>,
|
||||
file: PathBuf,
|
||||
},
|
||||
/// Temporarily lock the agent with a passphrase, SSH_AGENTC_LOCK
|
||||
Lock,
|
||||
/// Temporarily unlock a temporarily locked agent with a passphrase, SSH_AGENTC_UNLOCK
|
||||
Unlock,
|
||||
/// Query all available extension types SSH_AGENTC_EXTENSION/query
|
||||
ExtensionQuery,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> eyre::Result<()> {
|
||||
let args = Args::parse();
|
||||
|
||||
let env_filter = tracing_subscriber::EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info"));
|
||||
tracing_subscriber::fmt().with_env_filter(env_filter).init();
|
||||
|
||||
let mut agent = ssh_agent_client::SocketAgentConnection::from_env().await?;
|
||||
|
||||
match args.command {
|
||||
Subcommand::RemoveAllIdentities => {
|
||||
agent.remove_all_identities().await?;
|
||||
println!("Removed all identities from the agent");
|
||||
}
|
||||
Subcommand::ListIdentities { key_id } => {
|
||||
list_ids(&mut agent, key_id).await?;
|
||||
}
|
||||
Subcommand::Sign { file, key } => {
|
||||
let file = std::fs::read(&file)
|
||||
.wrap_err_with(|| format!("reading file {}", file.display()))?;
|
||||
|
||||
let ids = agent
|
||||
.list_identities()
|
||||
.await
|
||||
.wrap_err("listing identities")?;
|
||||
|
||||
let key = match ids.len() {
|
||||
0 => {
|
||||
bail!("no keys found");
|
||||
}
|
||||
1 => {
|
||||
let id = &ids[0];
|
||||
if let Some(key) = key {
|
||||
if key_id(id) != key {
|
||||
eprintln!("error: key {key} not found. pass a key-id found below:");
|
||||
list_ids(&mut agent, true).await?;
|
||||
eprintln!(
|
||||
"note: there is only one key, passing the key-id is not required"
|
||||
);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
id
|
||||
}
|
||||
_ => {
|
||||
let Some(key) = key else {
|
||||
eprintln!("error: missing argument --key. pass the key-id found below:");
|
||||
list_ids(&mut agent, true).await?;
|
||||
std::process::exit(1);
|
||||
};
|
||||
|
||||
let Some(id) = ids.iter().find(|item| key_id(item) == key) else {
|
||||
eprintln!("error: key {key} not found. pass a key-id from below");
|
||||
list_ids(&mut agent, true).await?;
|
||||
std::process::exit(1);
|
||||
};
|
||||
id
|
||||
}
|
||||
};
|
||||
|
||||
let signature = agent.sign(&key.key_blob, &file, 0).await?;
|
||||
}
|
||||
Subcommand::Lock => {
|
||||
let passphrase =
|
||||
tokio::task::spawn_blocking(|| rpassword::prompt_password("passphrase: "))
|
||||
.await?
|
||||
.wrap_err("failed to prompt passphrase")?;
|
||||
agent.lock(&passphrase).await?;
|
||||
println!("Locked SSH agent");
|
||||
}
|
||||
Subcommand::Unlock => {
|
||||
let passphrase =
|
||||
tokio::task::spawn_blocking(|| rpassword::prompt_password("passphrase: "))
|
||||
.await?
|
||||
.wrap_err("failed to prompt passphrase")?;
|
||||
agent.unlock(&passphrase).await?;
|
||||
println!("Unlocked SSH agent");
|
||||
}
|
||||
Subcommand::ExtensionQuery => {
|
||||
let extensions = agent.extension_query().await?;
|
||||
for ext in extensions {
|
||||
println!("{ext}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn list_ids(agent: &mut SocketAgentConnection, print_key_id: bool) -> eyre::Result<()> {
|
||||
let ids = agent.list_identities().await?;
|
||||
for id in ids {
|
||||
print_key(id, print_key_id);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_key(id: IdentityAnswer, show_key_id: bool) {
|
||||
let key = SshPubkey::from_wire_encoding(&id.key_blob);
|
||||
match key {
|
||||
Ok(key) => {
|
||||
if show_key_id {
|
||||
print!("{} ", key_id(&id));
|
||||
}
|
||||
println!("{key} {}", id.comment);
|
||||
}
|
||||
Err(key) => {
|
||||
eprintln!("{key}");
|
||||
println!("<unknown> {}", id.comment);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn key_id(key: &IdentityAnswer) -> String {
|
||||
use sha2::Digest;
|
||||
let digest = sha2::Sha256::digest(&key.key_blob);
|
||||
hex::encode(&digest[..4])
|
||||
}
|
||||
|
|
@ -111,8 +111,9 @@ impl ClientConnection {
|
|||
self.transport.recv_bytes(bytes)?;
|
||||
|
||||
if let ClientConnectionState::Setup(auth) = &mut self.state {
|
||||
if self.transport.is_open() {
|
||||
if let Some(session_ident) = self.transport.is_open() {
|
||||
let mut auth = mem::take(auth).unwrap();
|
||||
auth.set_session_identifier(session_ident);
|
||||
for to_send in auth.packets_to_send() {
|
||||
self.transport.send_plaintext_packet(to_send);
|
||||
}
|
||||
|
|
@ -314,10 +315,12 @@ pub mod auth {
|
|||
packets_to_send: VecDeque<Packet>,
|
||||
user_requests: VecDeque<ClientUserRequest>,
|
||||
is_authenticated: bool,
|
||||
session_identifier: Option<[u8; 32]>,
|
||||
}
|
||||
|
||||
pub enum ClientUserRequest {
|
||||
Password,
|
||||
PrivateKeySign { session_identifier: [u8; 32] },
|
||||
Banner(Vec<u8>),
|
||||
}
|
||||
|
||||
|
|
@ -333,9 +336,15 @@ pub mod auth {
|
|||
username,
|
||||
user_requests: VecDeque::new(),
|
||||
is_authenticated: false,
|
||||
session_identifier: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_session_identifier(&mut self, ident: [u8; 32]) {
|
||||
assert!(self.session_identifier.is_none());
|
||||
self.session_identifier = Some(ident);
|
||||
}
|
||||
|
||||
pub fn is_authenticated(&self) -> bool {
|
||||
self.is_authenticated
|
||||
}
|
||||
|
|
@ -379,6 +388,15 @@ pub mod auth {
|
|||
|
||||
if authentications.iter().any(|item| item == "password") {
|
||||
self.user_requests.push_back(ClientUserRequest::Password);
|
||||
} else if authentications.iter().any(|item| item == "publickey") {
|
||||
// <https://datatracker.ietf.org/doc/html/rfc4252#section-7>
|
||||
// TODO: Ask the server whether there are any keys we can use instead of just yoloing the signature.
|
||||
self.user_requests
|
||||
.push_back(ClientUserRequest::PrivateKeySign {
|
||||
session_identifier: self
|
||||
.session_identifier
|
||||
.expect("set_session_identifier has not been called"),
|
||||
});
|
||||
} else {
|
||||
return Err(peer_error!(
|
||||
"server does not support password authentication"
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ subtle = "2.6.1"
|
|||
x25519-dalek = "2.0.1"
|
||||
|
||||
tracing.workspace = true
|
||||
base64 = "0.22.1"
|
||||
|
||||
[dev-dependencies]
|
||||
hex-literal = "0.4.1"
|
||||
|
|
|
|||
|
|
@ -49,8 +49,12 @@ enum ClientState {
|
|||
encryption_client_to_server: EncryptionAlgorithm,
|
||||
encryption_server_to_client: EncryptionAlgorithm,
|
||||
},
|
||||
ServiceRequest,
|
||||
Open,
|
||||
ServiceRequest {
|
||||
session_identifier: [u8; 32],
|
||||
},
|
||||
Open {
|
||||
session_identifier: [u8; 32],
|
||||
},
|
||||
}
|
||||
|
||||
impl ClientConnection {
|
||||
|
|
@ -229,6 +233,7 @@ impl ClientConnection {
|
|||
let kex_secret = mem::take(kex_secret).unwrap();
|
||||
let shared_secret = (kex_secret.exchange)(server_ephermal_key)?;
|
||||
|
||||
// The exchange hash serves as the session identifier.
|
||||
let hash = crypto::key_exchange_hash(
|
||||
client_ident,
|
||||
server_ident,
|
||||
|
|
@ -279,13 +284,15 @@ impl ClientConnection {
|
|||
false,
|
||||
);
|
||||
|
||||
debug!("Requestin ssh-userauth service");
|
||||
debug!("Requesting ssh-userauth service");
|
||||
self.packet_transport
|
||||
.queue_packet(Packet::new_msg_service_request(b"ssh-userauth"));
|
||||
|
||||
self.state = ClientState::ServiceRequest;
|
||||
self.state = ClientState::ServiceRequest {
|
||||
session_identifier: *h,
|
||||
};
|
||||
}
|
||||
ClientState::ServiceRequest => {
|
||||
ClientState::ServiceRequest { session_identifier } => {
|
||||
let mut accept = packet.payload_parser();
|
||||
let packet_type = accept.u8()?;
|
||||
if packet_type != numbers::SSH_MSG_SERVICE_ACCEPT {
|
||||
|
|
@ -297,9 +304,11 @@ impl ClientConnection {
|
|||
}
|
||||
|
||||
debug!("Connection has been opened successfully");
|
||||
self.state = ClientState::Open;
|
||||
self.state = ClientState::Open {
|
||||
session_identifier: *session_identifier,
|
||||
};
|
||||
}
|
||||
ClientState::Open => {
|
||||
ClientState::Open { .. } => {
|
||||
self.plaintext_packets.push_back(packet);
|
||||
}
|
||||
}
|
||||
|
|
@ -319,8 +328,11 @@ impl ClientConnection {
|
|||
self.packet_transport.queue_packet(packet);
|
||||
}
|
||||
|
||||
pub fn is_open(&self) -> bool {
|
||||
matches!(self.state, ClientState::Open)
|
||||
pub fn is_open(&self) -> Option<[u8; 32]> {
|
||||
match self.state {
|
||||
ClientState::Open { session_identifier } => Some(session_identifier),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn send_kexinit(&mut self, client_ident: Vec<u8>, server_ident: Vec<u8>) {
|
||||
|
|
|
|||
59
ssh-transport/src/key.rs
Normal file
59
ssh-transport/src/key.rs
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
//! Operations on SSH keys.
|
||||
|
||||
use std::fmt::Display;
|
||||
|
||||
use base64::Engine;
|
||||
|
||||
use crate::parse::{self, ParseError, Parser, Writer};
|
||||
|
||||
pub enum SshPubkey {
|
||||
Ed25519 { public_key: [u8; 32] },
|
||||
}
|
||||
|
||||
impl SshPubkey {
|
||||
/// Parses an SSH public key from its wire encoding as specified in
|
||||
/// RFC4253, RFC5656, and RFC8709.
|
||||
pub fn from_wire_encoding(bytes: &[u8]) -> parse::Result<Self> {
|
||||
let mut p = Parser::new(bytes);
|
||||
let alg = p.utf8_string()?;
|
||||
|
||||
let k = match alg {
|
||||
"ssh-ed25519" => {
|
||||
let len = p.u32()?;
|
||||
if len != 32 {
|
||||
return Err(ParseError(format!("incorrect ed25519 len: {len}")));
|
||||
}
|
||||
let public_key = p.array::<32>()?;
|
||||
Self::Ed25519 { public_key }
|
||||
}
|
||||
_ => return Err(ParseError(format!("unsupported key type: {alg}"))),
|
||||
};
|
||||
Ok(k)
|
||||
}
|
||||
|
||||
pub fn to_wire_encoding(&self) -> Vec<u8> {
|
||||
let mut p = Writer::new();
|
||||
match self {
|
||||
Self::Ed25519 { public_key } => {
|
||||
p.string(b"ssh-ed25519");
|
||||
p.string(public_key);
|
||||
}
|
||||
}
|
||||
p.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for SshPubkey {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Ed25519 { .. } => {
|
||||
let encoded_pubkey = b64encode(&self.to_wire_encoding());
|
||||
write!(f, "ssh-ed25519 {encoded_pubkey}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn b64encode(bytes: &[u8]) -> String {
|
||||
base64::prelude::BASE64_STANDARD_NO_PAD.encode(bytes)
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
pub mod client;
|
||||
mod crypto;
|
||||
pub mod key;
|
||||
pub mod numbers;
|
||||
pub mod packet;
|
||||
pub mod parse;
|
||||
|
|
@ -20,7 +21,6 @@ pub enum SshStatus {
|
|||
|
||||
pub type Result<T, E = SshStatus> = std::result::Result<T, E>;
|
||||
|
||||
|
||||
pub trait SshRng {
|
||||
fn fill_bytes(&mut self, dest: &mut [u8]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use std::mem;
|
|||
|
||||
use tracing::{debug, trace};
|
||||
|
||||
use crate::crypto::{EncryptionAlgorithm, Keys, Plaintext, Session};
|
||||
use crate::crypto::{self, EncryptionAlgorithm, Keys, Plaintext, Session};
|
||||
use crate::parse::{NameList, Parser, Writer};
|
||||
use crate::Result;
|
||||
use crate::{numbers, peer_error};
|
||||
|
|
@ -347,19 +347,38 @@ impl RawPacket {
|
|||
}
|
||||
}
|
||||
|
||||
struct PacketParser {
|
||||
pub struct PacketParser {
|
||||
// The length of the packet.
|
||||
packet_length: Option<usize>,
|
||||
// The raw data *encrypted*, including the length.
|
||||
raw_data: Vec<u8>,
|
||||
done: bool,
|
||||
}
|
||||
impl PacketParser {
|
||||
fn new() -> Self {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
packet_length: None,
|
||||
raw_data: Vec::new(),
|
||||
done: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a raw packet body out of a plaintext stream of bytes.
|
||||
/// # Returns
|
||||
/// - `Err()` - if the packet was invalid
|
||||
/// - `Ok(None)` - if the packet is incomplete and needs more data
|
||||
/// - `Ok(Some(consumed, all_data))` if a packet has been parsed.
|
||||
/// `consumed` is the amount of bytes from `bytes` that were actually consumed,
|
||||
/// `all_data` is the entire packet including the length.
|
||||
pub fn recv_plaintext_bytes(&mut self, bytes: &[u8]) -> Result<Option<(usize, Vec<u8>)>> {
|
||||
let Some((consumed, data)) = self.recv_bytes_inner(bytes, &mut crypto::Plaintext, 0)?
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
self.done = true;
|
||||
Ok(Some((consumed, data.raw)))
|
||||
}
|
||||
|
||||
fn recv_bytes(
|
||||
&mut self,
|
||||
bytes: &[u8],
|
||||
|
|
@ -378,6 +397,11 @@ impl PacketParser {
|
|||
keys: &mut dyn Keys,
|
||||
next_seq_nr: u64,
|
||||
) -> Result<Option<(usize, RawPacket)>> {
|
||||
assert!(
|
||||
!self.done,
|
||||
"Passed bytes to packet parser even after it was completed"
|
||||
);
|
||||
|
||||
let mut consumed = 0;
|
||||
let packet_length = match self.packet_length {
|
||||
Some(packet_length) => {
|
||||
|
|
@ -460,7 +484,7 @@ impl ProtocolIdentParser {
|
|||
// The peer will not send any more information than this until we respond, so discord the rest of the bytes.
|
||||
let peer_ident = mem::take(&mut self.0);
|
||||
let peer_ident_string = String::from_utf8_lossy(&peer_ident);
|
||||
debug!(identification = %peer_ident_string, "Peer identifier");
|
||||
debug!(identification = %peer_ident_string.trim(), "Peer identifier");
|
||||
|
||||
Some(peer_ident)
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,24 @@
|
|||
use core::str;
|
||||
use std::fmt::Debug;
|
||||
use std::fmt::{Debug, Display};
|
||||
|
||||
use crate::Result;
|
||||
use crate::SshStatus;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ParseError(pub String);
|
||||
impl Display for ParseError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(&self.0)
|
||||
}
|
||||
}
|
||||
impl std::error::Error for ParseError {}
|
||||
|
||||
impl From<ParseError> for SshStatus {
|
||||
fn from(err: ParseError) -> Self {
|
||||
Self::PeerError(err.0)
|
||||
}
|
||||
}
|
||||
|
||||
pub type Result<T, E = ParseError> = std::result::Result<T, E>;
|
||||
|
||||
/// A simplified `byteorder` clone that emits client errors when the data is too short.
|
||||
pub struct Parser<'a>(&'a [u8]);
|
||||
|
|
@ -11,6 +28,10 @@ impl<'a> Parser<'a> {
|
|||
Self(data)
|
||||
}
|
||||
|
||||
pub fn has_data(&self) -> bool {
|
||||
!self.0.is_empty()
|
||||
}
|
||||
|
||||
pub fn u8(&mut self) -> Result<u8> {
|
||||
let arr = self.array::<1>()?;
|
||||
Ok(arr[0])
|
||||
|
|
@ -24,7 +45,7 @@ impl<'a> Parser<'a> {
|
|||
pub fn array<const N: usize>(&mut self) -> Result<[u8; N]> {
|
||||
assert!(N < 100_000);
|
||||
if self.0.len() < N {
|
||||
return Err(crate::peer_error!("packet too short"));
|
||||
return Err(ParseError(format!("packet too short")));
|
||||
}
|
||||
let result = self.0[..N].try_into().unwrap();
|
||||
self.0 = &self.0[N..];
|
||||
|
|
@ -33,10 +54,10 @@ impl<'a> Parser<'a> {
|
|||
|
||||
pub fn slice(&mut self, len: usize) -> Result<&'a [u8]> {
|
||||
if self.0.len() < len {
|
||||
return Err(crate::peer_error!("packet too short"));
|
||||
return Err(ParseError(format!("packet too short")));
|
||||
}
|
||||
if len > 100_000 {
|
||||
return Err(crate::peer_error!("bytes too long: {len}"));
|
||||
return Err(ParseError(format!("bytes too long: {len}")));
|
||||
}
|
||||
let result = &self.0[..len];
|
||||
self.0 = &self.0[len..];
|
||||
|
|
@ -48,7 +69,7 @@ impl<'a> Parser<'a> {
|
|||
match b {
|
||||
0 => Ok(false),
|
||||
1 => Ok(true),
|
||||
_ => Err(crate::peer_error!("invalid bool: {b}")),
|
||||
_ => Err(ParseError(format!("invalid bool: {b}"))),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -70,7 +91,7 @@ impl<'a> Parser<'a> {
|
|||
pub fn utf8_string(&mut self) -> Result<&'a str> {
|
||||
let s = self.string()?;
|
||||
let Ok(s) = str::from_utf8(s) else {
|
||||
return Err(crate::peer_error!("name-list is invalid UTF-8"));
|
||||
return Err(ParseError(format!("name-list is invalid UTF-8")));
|
||||
};
|
||||
Ok(s)
|
||||
}
|
||||
|
|
@ -165,7 +186,7 @@ impl<'a> NameList<'a> {
|
|||
|
||||
impl Debug for NameList<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
self.0.fmt(f)
|
||||
Debug::fmt(&self.0, f)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ edition = "2021"
|
|||
[dependencies]
|
||||
ssh-protocol = { path = "../ssh-protocol" }
|
||||
ssh-transport = { path = "../ssh-transport" }
|
||||
ssh-agent-client = { path = "../ssh-agent-client" }
|
||||
|
||||
clap = { version = "4.5.15", features = ["derive"] }
|
||||
eyre = "0.6.12"
|
||||
rand = "0.8.5"
|
||||
|
|
|
|||
|
|
@ -79,6 +79,18 @@ async fn main() -> eyre::Result<()> {
|
|||
let _ = send_op.blocking_send(Operation::PasswordEntered(password));
|
||||
});
|
||||
}
|
||||
ssh_protocol::auth::ClientUserRequest::PrivateKeySign {
|
||||
session_identifier: _,
|
||||
} => {
|
||||
// TODO: move
|
||||
let mut agent = ssh_agent_client::SocketAgentConnection::from_env()
|
||||
.await
|
||||
.wrap_err("failed to connect to SSH agent")?;
|
||||
let identities = agent.list_identities().await?;
|
||||
for identity in identities {
|
||||
debug!(comment = ?identity.comment, "Found identity");
|
||||
}
|
||||
}
|
||||
ssh_protocol::auth::ClientUserRequest::Banner(banner) => {
|
||||
let banner = String::from_utf8_lossy(&banner);
|
||||
std::io::stdout().write(&banner.as_bytes())?;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
use std::{
|
||||
io::Write,
|
||||
sync::{
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
Arc,
|
||||
|
|
@ -15,7 +14,7 @@ use tokio::{
|
|||
io::{AsyncReadExt, AsyncWriteExt},
|
||||
net::TcpStream,
|
||||
};
|
||||
use tracing::{debug, error, info};
|
||||
use tracing::{error, info};
|
||||
|
||||
use ssh_protocol::{
|
||||
transport::{self},
|
||||
|
|
@ -41,11 +40,6 @@ struct Args {
|
|||
#[arg(short = 'c', long)]
|
||||
chill: bool,
|
||||
destination: String,
|
||||
command: Vec<String>,
|
||||
}
|
||||
|
||||
enum Operation {
|
||||
PasswordEntered(std::io::Result<String>),
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
|
|
@ -86,7 +80,7 @@ async fn main() -> eyre::Result<()> {
|
|||
async fn execute_attempt(args: &Args) -> eyre::Result<()> {
|
||||
let conn = TcpStream::connect(&format!("{}:{}", args.destination, args.port)).await?;
|
||||
|
||||
let result = execute_attempt_inner(args, conn).await;
|
||||
let result = execute_attempt_inner(conn).await;
|
||||
|
||||
if args.chill {
|
||||
info!("Chilling, taking up space");
|
||||
|
|
@ -96,7 +90,7 @@ async fn execute_attempt(args: &Args) -> eyre::Result<()> {
|
|||
result
|
||||
}
|
||||
|
||||
async fn execute_attempt_inner(args: &Args, mut conn: TcpStream) -> eyre::Result<()> {
|
||||
async fn execute_attempt_inner(mut conn: TcpStream) -> eyre::Result<()> {
|
||||
let username = "dos";
|
||||
|
||||
let mut transport = transport::client::ClientConnection::new(ThreadRngRand);
|
||||
|
|
@ -107,8 +101,6 @@ async fn execute_attempt_inner(args: &Args, mut conn: TcpStream) -> eyre::Result
|
|||
ssh_protocol::auth::ClientAuth::new(username.as_bytes().to_vec()),
|
||||
);
|
||||
|
||||
let (send_op, mut recv_op) = tokio::sync::mpsc::channel::<Operation>(10);
|
||||
|
||||
let mut buf = [0; 1024];
|
||||
|
||||
loop {
|
||||
|
|
@ -118,66 +110,33 @@ async fn execute_attempt_inner(args: &Args, mut conn: TcpStream) -> eyre::Result
|
|||
.wrap_err("writing response")?;
|
||||
}
|
||||
|
||||
if let Some(auth) = state.auth() {
|
||||
for req in auth.user_requests() {
|
||||
match req {
|
||||
ssh_protocol::auth::ClientUserRequest::Password => {
|
||||
let username = username.to_owned();
|
||||
let destination = args.destination.clone();
|
||||
let send_op = send_op.clone();
|
||||
std::thread::spawn(move || {
|
||||
let password = rpassword::prompt_password(format!(
|
||||
"{}@{}'s password: ",
|
||||
username, destination
|
||||
));
|
||||
let _ = send_op.blocking_send(Operation::PasswordEntered(password));
|
||||
});
|
||||
}
|
||||
ssh_protocol::auth::ClientUserRequest::Banner(banner) => {
|
||||
let banner = String::from_utf8_lossy(&banner);
|
||||
std::io::stdout().write(&banner.as_bytes())?;
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(_) = state.auth() {
|
||||
unreachable!();
|
||||
}
|
||||
|
||||
tokio::select! {
|
||||
read = conn.read(&mut buf) => {
|
||||
let read = read.wrap_err("reading from connection")?;
|
||||
if read == 0 {
|
||||
info!("Did not read any bytes from TCP stream, EOF");
|
||||
let read = conn
|
||||
.read(&mut buf)
|
||||
.await
|
||||
.wrap_err("reading from connection")?;
|
||||
if read == 0 {
|
||||
info!("Did not read any bytes from TCP stream, EOF");
|
||||
return Ok(());
|
||||
}
|
||||
if let Err(err) = state.recv_bytes(&buf[..read]) {
|
||||
match err {
|
||||
SshStatus::PeerError(err) => {
|
||||
if err == "early abort" {
|
||||
// Expected.
|
||||
return Ok(());
|
||||
}
|
||||
error!(?err, "disconnecting client after invalid operation");
|
||||
return Ok(());
|
||||
}
|
||||
if let Err(err) = state.recv_bytes(&buf[..read]) {
|
||||
match err {
|
||||
SshStatus::PeerError(err) => {
|
||||
if err == "early abort" {
|
||||
// Expected.
|
||||
return Ok(());
|
||||
}
|
||||
error!(?err, "disconnecting client after invalid operation");
|
||||
return Ok(());
|
||||
}
|
||||
SshStatus::Disconnect => {
|
||||
error!("Received disconnect from server");
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
SshStatus::Disconnect => {
|
||||
error!("Received disconnect from server");
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
op = recv_op.recv() => {
|
||||
match op {
|
||||
Some(Operation::PasswordEntered(password)) => {
|
||||
if let Some(auth) = state.auth() {
|
||||
auth.send_password(&password?);
|
||||
} else {
|
||||
debug!("Ignoring entered password as the state has moved on");
|
||||
}
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
state.progress();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue