mirror of
https://github.com/Noratrieb/cluelessh.git
synced 2026-01-15 17:05:05 +01:00
windowing and starting aes-ctr
This commit is contained in:
parent
f4ba9a2939
commit
e3bf214ec6
10 changed files with 489 additions and 232 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
|
@ -926,9 +926,11 @@ dependencies = [
|
||||||
name = "ssh-transport"
|
name = "ssh-transport"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"aes",
|
||||||
"aes-gcm",
|
"aes-gcm",
|
||||||
"chacha20",
|
"chacha20",
|
||||||
"crypto-bigint",
|
"crypto-bigint",
|
||||||
|
"ctr",
|
||||||
"ed25519-dalek",
|
"ed25519-dalek",
|
||||||
"eyre",
|
"eyre",
|
||||||
"hex-literal",
|
"hex-literal",
|
||||||
|
|
|
||||||
38
src/main.rs
38
src/main.rs
|
|
@ -45,10 +45,24 @@ async fn main() -> eyre::Result<()> {
|
||||||
let mut total_sent_data = Vec::new();
|
let mut total_sent_data = Vec::new();
|
||||||
|
|
||||||
if let Err(err) = handle_connection(next, &mut total_sent_data).await {
|
if let Err(err) = handle_connection(next, &mut total_sent_data).await {
|
||||||
|
if let Some(err) = err.downcast_ref::<std::io::Error>() {
|
||||||
|
if err.kind() == std::io::ErrorKind::ConnectionReset {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
error!(?err, "error handling connection");
|
error!(?err, "error handling connection");
|
||||||
}
|
}
|
||||||
|
|
||||||
info!(stdin = %String::from_utf8_lossy(&total_sent_data), "Finished connection");
|
// Limit stdin to 500 characters.
|
||||||
|
let stdin = String::from_utf8_lossy(&total_sent_data);
|
||||||
|
let stdin = if let Some((idx, _)) = stdin.char_indices().nth(500) {
|
||||||
|
&stdin[..idx]
|
||||||
|
} else {
|
||||||
|
&stdin
|
||||||
|
};
|
||||||
|
|
||||||
|
info!(?stdin, "Finished connection");
|
||||||
}
|
}
|
||||||
.instrument(span),
|
.instrument(span),
|
||||||
);
|
);
|
||||||
|
|
@ -111,6 +125,7 @@ async fn handle_connection(
|
||||||
}
|
}
|
||||||
|
|
||||||
while let Some(update) = state.next_channel_update() {
|
while let Some(update) = state.next_channel_update() {
|
||||||
|
//eprintln!("{:?}", update);
|
||||||
match update.kind {
|
match update.kind {
|
||||||
ChannelUpdateKind::Open(kind) => match kind {
|
ChannelUpdateKind::Open(kind) => match kind {
|
||||||
ChannelOpen::Session => {
|
ChannelOpen::Session => {
|
||||||
|
|
@ -126,6 +141,21 @@ async fn handle_connection(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ChannelRequest::Shell { want_reply } => {
|
ChannelRequest::Shell { want_reply } => {
|
||||||
|
state.do_operation(
|
||||||
|
update.number.construct_op(ChannelOperationKind::Data(
|
||||||
|
vec![b'a'; 1_000_000],
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
state.do_operation(
|
||||||
|
update.number.construct_op(ChannelOperationKind::Data(
|
||||||
|
vec![b'b'; 1_000_000],
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
state.do_operation(
|
||||||
|
update.number.construct_op(ChannelOperationKind::Data(
|
||||||
|
vec![b'c'; 1_000_000],
|
||||||
|
)),
|
||||||
|
);
|
||||||
if want_reply {
|
if want_reply {
|
||||||
state.do_operation(success);
|
state.do_operation(success);
|
||||||
}
|
}
|
||||||
|
|
@ -177,11 +207,11 @@ async fn handle_connection(
|
||||||
b"Thanks Hayley!".to_vec(),
|
b"Thanks Hayley!".to_vec(),
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
state.do_operation(update.number.construct_op(ChannelOperationKind::Close));
|
//state.do_operation(update.number.construct_op(ChannelOperationKind::Close));
|
||||||
}
|
}
|
||||||
|
|
||||||
if is_eof {
|
if false && is_eof {
|
||||||
debug!(channel = %update.number, "Received EOF, closing channel");
|
debug!(channel = %update.number, "Received Ctrl-C, closing channel");
|
||||||
|
|
||||||
state.do_operation(update.number.construct_op(ChannelOperationKind::Close));
|
state.do_operation(update.number.construct_op(ChannelOperationKind::Close));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
|
use std::cmp;
|
||||||
use std::collections::{HashMap, VecDeque};
|
use std::collections::{HashMap, VecDeque};
|
||||||
use tracing::{debug, info, warn};
|
use tracing::{debug, info, trace, warn};
|
||||||
|
|
||||||
use ssh_transport::packet::Packet;
|
use ssh_transport::packet::Packet;
|
||||||
use ssh_transport::Result;
|
use ssh_transport::Result;
|
||||||
|
|
@ -28,14 +29,32 @@ struct Channel {
|
||||||
we_closed: bool,
|
we_closed: bool,
|
||||||
/// The channel number for the other side.
|
/// The channel number for the other side.
|
||||||
peer_channel: u32,
|
peer_channel: u32,
|
||||||
|
/// The current max window size of our peer, controls how many bytes we can still send.
|
||||||
|
peer_window_size: u32,
|
||||||
|
/// The max packet size of the peer.
|
||||||
|
// We need to split our packets if the user requests more.
|
||||||
|
peer_max_packet_size: u32,
|
||||||
|
|
||||||
|
/// For validation only.
|
||||||
|
our_window_size: u32,
|
||||||
|
/// For validation only.
|
||||||
|
our_max_packet_size: u32,
|
||||||
|
/// By how much we want to increase the window when it gets small.
|
||||||
|
our_window_size_increase_step: u32,
|
||||||
|
|
||||||
|
/// Queued data that we want to send, but have not been able to because of the window limits.
|
||||||
|
/// Whenever we get more window space, we will send this data.
|
||||||
|
queued_data: Vec<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An update from a channel.
|
/// An update from a channel.
|
||||||
/// The receiver-equivalent of [`ChannelOperation`].
|
/// The receiver-equivalent of [`ChannelOperation`].
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct ChannelUpdate {
|
pub struct ChannelUpdate {
|
||||||
pub number: ChannelNumber,
|
pub number: ChannelNumber,
|
||||||
pub kind: ChannelUpdateKind,
|
pub kind: ChannelUpdateKind,
|
||||||
}
|
}
|
||||||
|
#[derive(Debug)]
|
||||||
pub enum ChannelUpdateKind {
|
pub enum ChannelUpdateKind {
|
||||||
Open(ChannelOpen),
|
Open(ChannelOpen),
|
||||||
Request(ChannelRequest),
|
Request(ChannelRequest),
|
||||||
|
|
@ -44,11 +63,11 @@ pub enum ChannelUpdateKind {
|
||||||
Eof,
|
Eof,
|
||||||
Closed,
|
Closed,
|
||||||
}
|
}
|
||||||
|
#[derive(Debug)]
|
||||||
pub enum ChannelOpen {
|
pub enum ChannelOpen {
|
||||||
Session,
|
Session,
|
||||||
}
|
}
|
||||||
|
#[derive(Debug)]
|
||||||
pub enum ChannelRequest {
|
pub enum ChannelRequest {
|
||||||
PtyReq {
|
PtyReq {
|
||||||
want_reply: bool,
|
want_reply: bool,
|
||||||
|
|
@ -168,6 +187,13 @@ impl ServerChannelsState {
|
||||||
Channel {
|
Channel {
|
||||||
we_closed: false,
|
we_closed: false,
|
||||||
peer_channel: sender_channel,
|
peer_channel: sender_channel,
|
||||||
|
peer_max_packet_size: max_packet_size,
|
||||||
|
peer_window_size: initial_window_size,
|
||||||
|
our_max_packet_size: max_packet_size,
|
||||||
|
our_window_size: initial_window_size,
|
||||||
|
our_window_size_increase_step: initial_window_size,
|
||||||
|
|
||||||
|
queued_data: Vec::new(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -178,11 +204,59 @@ impl ServerChannelsState {
|
||||||
|
|
||||||
debug!(%channel_type, %our_number, "Successfully opened channel");
|
debug!(%channel_type, %our_number, "Successfully opened channel");
|
||||||
}
|
}
|
||||||
|
numbers::SSH_MSG_CHANNEL_WINDOW_ADJUST => {
|
||||||
|
let our_channel = packet.u32()?;
|
||||||
|
let our_channel = self.validate_channel(our_channel)?;
|
||||||
|
let bytes_to_add = packet.u32()?;
|
||||||
|
|
||||||
|
let channel = self.channel(our_channel)?;
|
||||||
|
channel.peer_window_size = channel
|
||||||
|
.peer_window_size
|
||||||
|
.checked_add(bytes_to_add)
|
||||||
|
.ok_or_else(|| client_error!("window size larger than 2^32"))?;
|
||||||
|
|
||||||
|
if !channel.queued_data.is_empty() {
|
||||||
|
let limit =
|
||||||
|
cmp::min(channel.queued_data.len(), channel.peer_window_size as usize);
|
||||||
|
let data_to_send = channel.queued_data.splice(..limit, []).collect::<Vec<_>>();
|
||||||
|
self.send_data(our_channel, &data_to_send);
|
||||||
|
}
|
||||||
|
}
|
||||||
numbers::SSH_MSG_CHANNEL_DATA => {
|
numbers::SSH_MSG_CHANNEL_DATA => {
|
||||||
let our_channel = packet.u32()?;
|
let our_channel = packet.u32()?;
|
||||||
let our_channel = self.validate_channel(our_channel)?;
|
let our_channel = self.validate_channel(our_channel)?;
|
||||||
let data = packet.string()?;
|
let data = packet.string()?;
|
||||||
|
|
||||||
|
let channel = self.channel(our_channel)?;
|
||||||
|
channel.our_window_size = channel
|
||||||
|
.our_window_size
|
||||||
|
.checked_sub(data.len() as u32)
|
||||||
|
.ok_or_else(|| {
|
||||||
|
client_error!(
|
||||||
|
"sent more data than the window allows: {} while the window is {}",
|
||||||
|
data.len(),
|
||||||
|
channel.our_window_size
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
if channel.our_max_packet_size < (data.len() as u32) {
|
||||||
|
return Err(client_error!(
|
||||||
|
"data bigger than allowed packet size: {} while the max packet size is {}",
|
||||||
|
data.len(),
|
||||||
|
channel.our_max_packet_size
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
trace!(channel = %our_channel, window = %channel.our_window_size, "Remaining window on our side");
|
||||||
|
|
||||||
|
// We probably want to make this user-controllable in the future.
|
||||||
|
if channel.our_window_size < 1000 {
|
||||||
|
let peer = channel.peer_channel;
|
||||||
|
let bytes_to_add = channel.our_window_size_increase_step;
|
||||||
|
channel.our_window_size += bytes_to_add;
|
||||||
|
self.packets_to_send
|
||||||
|
.push_back(Packet::new_msg_channel_window_adjust(peer, bytes_to_add))
|
||||||
|
}
|
||||||
|
|
||||||
self.channel_updates.push_back(ChannelUpdate {
|
self.channel_updates.push_back(ChannelUpdate {
|
||||||
number: our_channel,
|
number: our_channel,
|
||||||
kind: ChannelUpdateKind::Data {
|
kind: ChannelUpdateKind::Data {
|
||||||
|
|
@ -206,6 +280,7 @@ impl ServerChannelsState {
|
||||||
let our_channel = self.validate_channel(our_channel)?;
|
let our_channel = self.validate_channel(our_channel)?;
|
||||||
let channel = self.channel(our_channel)?;
|
let channel = self.channel(our_channel)?;
|
||||||
if !channel.we_closed {
|
if !channel.we_closed {
|
||||||
|
info!("closeing here");
|
||||||
let close = Packet::new_msg_channel_close(channel.peer_channel);
|
let close = Packet::new_msg_channel_close(channel.peer_channel);
|
||||||
self.packets_to_send.push_back(close);
|
self.packets_to_send.push_back(close);
|
||||||
}
|
}
|
||||||
|
|
@ -225,7 +300,7 @@ impl ServerChannelsState {
|
||||||
let request_type = packet.utf8_string()?;
|
let request_type = packet.utf8_string()?;
|
||||||
let want_reply = packet.bool()?;
|
let want_reply = packet.bool()?;
|
||||||
|
|
||||||
debug!(%our_channel, %request_type, "Got channel request");
|
debug!(channel = %our_channel, %request_type, "Got channel request");
|
||||||
|
|
||||||
let channel = self.channel(our_channel)?;
|
let channel = self.channel(our_channel)?;
|
||||||
let peer_channel = channel.peer_channel;
|
let peer_channel = channel.peer_channel;
|
||||||
|
|
@ -240,7 +315,7 @@ impl ServerChannelsState {
|
||||||
let term_modes = packet.string()?;
|
let term_modes = packet.string()?;
|
||||||
|
|
||||||
debug!(
|
debug!(
|
||||||
%our_channel,
|
channel = %our_channel,
|
||||||
%term,
|
%term,
|
||||||
%width_chars,
|
%width_chars,
|
||||||
%height_rows,
|
%height_rows,
|
||||||
|
|
@ -258,12 +333,12 @@ impl ServerChannelsState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"shell" => {
|
"shell" => {
|
||||||
info!(%our_channel, "Opening shell");
|
info!(channel = %our_channel, "Opening shell");
|
||||||
ChannelRequest::Shell { want_reply }
|
ChannelRequest::Shell { want_reply }
|
||||||
}
|
}
|
||||||
"exec" => {
|
"exec" => {
|
||||||
let command = packet.string()?;
|
let command = packet.string()?;
|
||||||
info!(%our_channel, command = %String::from_utf8_lossy(command), "Executing command");
|
info!(channel = %our_channel, command = %String::from_utf8_lossy(command), "Executing command");
|
||||||
ChannelRequest::Exec {
|
ChannelRequest::Exec {
|
||||||
want_reply,
|
want_reply,
|
||||||
command: command.to_owned(),
|
command: command.to_owned(),
|
||||||
|
|
@ -273,7 +348,7 @@ impl ServerChannelsState {
|
||||||
let name = packet.utf8_string()?;
|
let name = packet.utf8_string()?;
|
||||||
let value = packet.string()?;
|
let value = packet.string()?;
|
||||||
|
|
||||||
info!(%our_channel, %name, value = %String::from_utf8_lossy(value), "Setting environment variable");
|
info!(channel = %our_channel, %name, value = %String::from_utf8_lossy(value), "Setting environment variable");
|
||||||
|
|
||||||
ChannelRequest::Env {
|
ChannelRequest::Env {
|
||||||
want_reply,
|
want_reply,
|
||||||
|
|
@ -282,12 +357,12 @@ impl ServerChannelsState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"signal" => {
|
"signal" => {
|
||||||
debug!(%our_channel, "Received signal");
|
debug!(channel = %our_channel, "Received signal");
|
||||||
// Ignore signals, something we can do.
|
// Ignore signals, something we can do.
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
warn!(%request_type, %our_channel, "Unknown channel request");
|
warn!(%request_type, channel = %our_channel, "Unknown channel request");
|
||||||
self.send_channel_failure(peer_channel);
|
self.send_channel_failure(peer_channel);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
@ -299,7 +374,10 @@ impl ServerChannelsState {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
todo!("unsupported packet: {} ({packet_type})", numbers::packet_type_to_string(packet_type).unwrap_or("<unknown>"));
|
todo!(
|
||||||
|
"unsupported packet: {} ({packet_type})",
|
||||||
|
numbers::packet_type_to_string(packet_type).unwrap_or("<unknown>")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -314,17 +392,28 @@ impl ServerChannelsState {
|
||||||
self.channel_updates.pop_front()
|
self.channel_updates.pop_front()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Executes an operation on the channel.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
/// This will panic when the channel has already been closed.
|
||||||
pub fn do_operation(&mut self, op: ChannelOperation) {
|
pub fn do_operation(&mut self, op: ChannelOperation) {
|
||||||
let peer = self
|
op.trace();
|
||||||
|
|
||||||
|
let channel = self
|
||||||
.channel(op.number)
|
.channel(op.number)
|
||||||
.expect("passed channel ID that does not exist")
|
.expect("passed channel ID that does not exist");
|
||||||
.peer_channel;
|
let peer = channel.peer_channel;
|
||||||
|
|
||||||
|
if channel.we_closed {
|
||||||
|
debug!("Dropping operation as channel has been closed already");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
match op.kind {
|
match op.kind {
|
||||||
ChannelOperationKind::Success => self.send_channel_success(peer),
|
ChannelOperationKind::Success => self.send_channel_success(peer),
|
||||||
ChannelOperationKind::Failure => self.send_channel_failure(peer),
|
ChannelOperationKind::Failure => self.send_channel_failure(peer),
|
||||||
ChannelOperationKind::Data(data) => {
|
ChannelOperationKind::Data(data) => {
|
||||||
self.packets_to_send
|
self.send_data(op.number, &data);
|
||||||
.push_back(Packet::new_msg_channel_data(peer, &data));
|
|
||||||
}
|
}
|
||||||
ChannelOperationKind::Request(req) => {
|
ChannelOperationKind::Request(req) => {
|
||||||
let packet = match req {
|
let packet = match req {
|
||||||
|
|
@ -358,6 +447,46 @@ impl ServerChannelsState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn send_data(&mut self, channel_number: ChannelNumber, data: &[u8]) {
|
||||||
|
let channel = self.channel(channel_number).unwrap();
|
||||||
|
let peer = channel.peer_channel;
|
||||||
|
|
||||||
|
let mut chunks = data.chunks(channel.peer_max_packet_size as usize);
|
||||||
|
|
||||||
|
while let Some(data) = chunks.next() {
|
||||||
|
let channel = self.channel(channel_number).unwrap();
|
||||||
|
let remaining_window_space_after =
|
||||||
|
channel.peer_window_size.checked_sub(data.len() as u32);
|
||||||
|
match remaining_window_space_after {
|
||||||
|
None => {
|
||||||
|
let rest = channel.peer_window_size;
|
||||||
|
let (to_send, to_keep) = data.split_at(rest as usize);
|
||||||
|
|
||||||
|
// Send everything we can, which empties the window.
|
||||||
|
channel.peer_window_size -= rest;
|
||||||
|
assert_eq!(channel.peer_window_size, 0);
|
||||||
|
self.packets_to_send
|
||||||
|
.push_back(Packet::new_msg_channel_data(peer, to_send));
|
||||||
|
|
||||||
|
// It's over, we have exhausted all window space.
|
||||||
|
// Queue the rest of the bytes.
|
||||||
|
let channel = self.channel(channel_number).unwrap();
|
||||||
|
channel.queued_data.extend_from_slice(to_keep);
|
||||||
|
for data in chunks {
|
||||||
|
channel.queued_data.extend_from_slice(data);
|
||||||
|
}
|
||||||
|
debug!(channel = %channel_number, queue_len = %channel.queued_data.len(), "Exhausted window space, queueing the rest of the data");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Some(space) => channel.peer_window_size = space,
|
||||||
|
}
|
||||||
|
trace!(channel = %channel_number, window = %channel.peer_window_size, "Remaining window on their side");
|
||||||
|
|
||||||
|
self.packets_to_send
|
||||||
|
.push_back(Packet::new_msg_channel_data(peer, data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn send_channel_success(&mut self, recipient_channel: u32) {
|
fn send_channel_success(&mut self, recipient_channel: u32) {
|
||||||
self.packets_to_send
|
self.packets_to_send
|
||||||
.push_back(Packet::new_msg_channel_success(recipient_channel));
|
.push_back(Packet::new_msg_channel_success(recipient_channel));
|
||||||
|
|
@ -381,3 +510,44 @@ impl ServerChannelsState {
|
||||||
.ok_or_else(|| client_error!("unknown channel: {number:?}"))
|
.ok_or_else(|| client_error!("unknown channel: {number:?}"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ChannelOperation {
|
||||||
|
/// Logs the attempted operation.
|
||||||
|
fn trace(&self) {
|
||||||
|
let kind = match &self.kind {
|
||||||
|
ChannelOperationKind::Success => "success",
|
||||||
|
ChannelOperationKind::Failure => "failure",
|
||||||
|
ChannelOperationKind::Data(_) => "data",
|
||||||
|
ChannelOperationKind::Request(req) => match req {
|
||||||
|
ChannelRequest::PtyReq { .. } => "pty-req",
|
||||||
|
ChannelRequest::Shell { .. } => "shell",
|
||||||
|
ChannelRequest::Exec { .. } => "exec",
|
||||||
|
ChannelRequest::Env { .. } => "env",
|
||||||
|
ChannelRequest::ExitStatus { .. } => "exit-status",
|
||||||
|
},
|
||||||
|
ChannelOperationKind::Eof => "eof",
|
||||||
|
ChannelOperationKind::Close => "close",
|
||||||
|
};
|
||||||
|
trace!(number = %self.number, %kind, "Attempt channel operation")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use ssh_transport::packet::Packet;
|
||||||
|
|
||||||
|
use crate::ServerChannelsState;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn only_single_close_for_double_close_operation() {
|
||||||
|
let state = ServerChannelsState::new();
|
||||||
|
//state.recv_packet();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic]
|
||||||
|
fn panic_when_data_operation_after_close() {
|
||||||
|
let state = ServerChannelsState::new();
|
||||||
|
//state.recv_packet();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,11 @@ version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
aes = "0.8.4"
|
||||||
aes-gcm = "0.10.3"
|
aes-gcm = "0.10.3"
|
||||||
chacha20 = "0.9.1"
|
chacha20 = "0.9.1"
|
||||||
crypto-bigint = "0.5.5"
|
crypto-bigint = "0.5.5"
|
||||||
|
ctr = "0.9.2"
|
||||||
ed25519-dalek = { version = "2.1.1" }
|
ed25519-dalek = { version = "2.1.1" }
|
||||||
eyre = "0.6.12"
|
eyre = "0.6.12"
|
||||||
p256 = { version = "0.13.2", features = ["ecdh", "ecdsa"] }
|
p256 = { version = "0.13.2", features = ["ecdh", "ecdsa"] }
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ and [RFC 4251 The Secure Shell (SSH) Protocol Architecture](https://datatracker.
|
||||||
and [RFC 4250 The Secure Shell (SSH) Protocol Assigned Numbers](https://datatracker.ietf.org/doc/html/rfc4250).
|
and [RFC 4250 The Secure Shell (SSH) Protocol Assigned Numbers](https://datatracker.ietf.org/doc/html/rfc4250).
|
||||||
|
|
||||||
Other relevant RFCs:
|
Other relevant RFCs:
|
||||||
|
- [RFC 4344 The Secure Shell (SSH) Transport Layer Encryption Modes](https://datatracker.ietf.org/doc/html/rfc4344)
|
||||||
- [RFC 5649 AES Galois Counter Mode for the Secure Shell Transport Layer Protocol](https://datatracker.ietf.org/doc/html/rfc5647)
|
- [RFC 5649 AES Galois Counter Mode for the Secure Shell Transport Layer Protocol](https://datatracker.ietf.org/doc/html/rfc5647)
|
||||||
- [RFC 5656 Elliptic Curve Algorithm Integration in the Secure Shell Transport Layer](https://datatracker.ietf.org/doc/html/rfc5656)
|
- [RFC 5656 Elliptic Curve Algorithm Integration in the Secure Shell Transport Layer](https://datatracker.ietf.org/doc/html/rfc5656)
|
||||||
- [RFC 6668 SHA-2 Data Integrity Verification for the Secure Shell (SSH) Transport Layer Protocol](https://datatracker.ietf.org/doc/html/rfc6668)
|
- [RFC 6668 SHA-2 Data Integrity Verification for the Secure Shell (SSH) Transport Layer Protocol](https://datatracker.ietf.org/doc/html/rfc6668)
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
use aes_gcm::aead::AeadMutInPlace;
|
pub mod encrypt;
|
||||||
use chacha20::cipher::{KeyInit, StreamCipher, StreamCipherSeek};
|
|
||||||
use p256::ecdsa::signature::Signer;
|
use p256::ecdsa::signature::Signer;
|
||||||
use sha2::Digest;
|
use sha2::Digest;
|
||||||
use subtle::ConstantTimeEq;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
client_error,
|
client_error,
|
||||||
|
|
@ -95,40 +94,6 @@ impl AlgorithmName for EncryptionAlgorithm {
|
||||||
self.name
|
self.name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub const ENC_CHACHA20POLY1305: EncryptionAlgorithm = EncryptionAlgorithm {
|
|
||||||
name: "chacha20-poly1305@openssh.com",
|
|
||||||
iv_size: 0,
|
|
||||||
key_size: 64, // 32 for header, 32 for main
|
|
||||||
decrypt_len: |state, bytes, packet_number| {
|
|
||||||
let alg = ChaCha20Poly1305OpenSsh::from_state(state);
|
|
||||||
alg.decrypt_len(bytes, packet_number)
|
|
||||||
},
|
|
||||||
decrypt_packet: |state, bytes, packet_number| {
|
|
||||||
let alg = ChaCha20Poly1305OpenSsh::from_state(state);
|
|
||||||
alg.decrypt_packet(bytes, packet_number)
|
|
||||||
},
|
|
||||||
encrypt_packet: |state, packet, packet_number| {
|
|
||||||
let alg = ChaCha20Poly1305OpenSsh::from_state(state);
|
|
||||||
alg.encrypt_packet(packet, packet_number)
|
|
||||||
},
|
|
||||||
};
|
|
||||||
pub const ENC_AES256_GCM: EncryptionAlgorithm = EncryptionAlgorithm {
|
|
||||||
name: "aes256-gcm@openssh.com",
|
|
||||||
iv_size: 12,
|
|
||||||
key_size: 32,
|
|
||||||
decrypt_len: |state, bytes, packet_number| {
|
|
||||||
let mut alg = Aes256GcmOpenSsh::from_state(state);
|
|
||||||
alg.decrypt_len(bytes, packet_number)
|
|
||||||
},
|
|
||||||
decrypt_packet: |state, bytes, packet_number| {
|
|
||||||
let mut alg = Aes256GcmOpenSsh::from_state(state);
|
|
||||||
alg.decrypt_packet(bytes, packet_number)
|
|
||||||
},
|
|
||||||
encrypt_packet: |state, packet, packet_number| {
|
|
||||||
let mut alg = Aes256GcmOpenSsh::from_state(state);
|
|
||||||
alg.encrypt_packet(packet, packet_number)
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub struct EncodedSshPublicHostKey(pub Vec<u8>);
|
pub struct EncodedSshPublicHostKey(pub Vec<u8>);
|
||||||
pub struct EncodedSshSignature(pub Vec<u8>);
|
pub struct EncodedSshSignature(pub Vec<u8>);
|
||||||
|
|
@ -425,176 +390,3 @@ pub(crate) fn encode_mpint_for_hash(key: &[u8], mut add_to_hash: impl FnMut(&[u8
|
||||||
}
|
}
|
||||||
add_to_hash(key);
|
add_to_hash(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `chacha20-poly1305@openssh.com` uses a 64-bit nonce, not the 96-bit one in the IETF version.
|
|
||||||
type SshChaCha20 = chacha20::ChaCha20Legacy;
|
|
||||||
|
|
||||||
/// <https://github.com/openssh/openssh-portable/blob/1ec0a64c5dc57b8a2053a93b5ef0d02ff8598e5c/PROTOCOL.chacha20poly1305>
|
|
||||||
struct ChaCha20Poly1305OpenSsh {
|
|
||||||
header_key: chacha20::Key,
|
|
||||||
main_key: chacha20::Key,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ChaCha20Poly1305OpenSsh {
|
|
||||||
fn from_state(keys: &[u8]) -> Self {
|
|
||||||
assert_eq!(keys.len(), 64);
|
|
||||||
Self {
|
|
||||||
main_key: <[u8; 32]>::try_from(&keys[..32]).unwrap().into(),
|
|
||||||
header_key: <[u8; 32]>::try_from(&keys[32..]).unwrap().into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn decrypt_len(&self, bytes: &mut [u8], packet_number: u64) {
|
|
||||||
// <https://github.com/openssh/openssh-portable/blob/1ec0a64c5dc57b8a2053a93b5ef0d02ff8598e5c/PROTOCOL.chacha20poly1305>
|
|
||||||
let mut cipher = <SshChaCha20 as chacha20::cipher::KeyIvInit>::new(
|
|
||||||
&self.header_key,
|
|
||||||
&packet_number.to_be_bytes().into(),
|
|
||||||
);
|
|
||||||
cipher.apply_keystream(bytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn decrypt_packet(&self, mut bytes: RawPacket, packet_number: u64) -> Result<Packet> {
|
|
||||||
// <https://github.com/openssh/openssh-portable/blob/1ec0a64c5dc57b8a2053a93b5ef0d02ff8598e5c/PROTOCOL.chacha20poly1305>
|
|
||||||
|
|
||||||
let mut cipher = <SshChaCha20 as chacha20::cipher::KeyIvInit>::new(
|
|
||||||
&self.main_key,
|
|
||||||
&packet_number.to_be_bytes().into(),
|
|
||||||
);
|
|
||||||
|
|
||||||
let tag_offset = bytes.full_packet().len() - 16;
|
|
||||||
let authenticated = &bytes.full_packet()[..tag_offset];
|
|
||||||
|
|
||||||
let mac = {
|
|
||||||
let mut poly1305_key = [0; poly1305::KEY_SIZE];
|
|
||||||
cipher.apply_keystream(&mut poly1305_key);
|
|
||||||
poly1305::Poly1305::new(&poly1305_key.into()).compute_unpadded(authenticated)
|
|
||||||
};
|
|
||||||
|
|
||||||
let read_tag = poly1305::Tag::from_slice(&bytes.full_packet()[tag_offset..]);
|
|
||||||
|
|
||||||
if !bool::from(mac.ct_eq(read_tag)) {
|
|
||||||
return Err(crate::client_error!(
|
|
||||||
"failed to decrypt: invalid poly1305 MAC"
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Advance ChaCha's block counter to 1
|
|
||||||
cipher
|
|
||||||
.seek(<chacha20::ChaCha20LegacyCore as chacha20::cipher::BlockSizeUser>::block_size());
|
|
||||||
|
|
||||||
let encrypted_packet_content = bytes.content_mut();
|
|
||||||
cipher.apply_keystream(encrypted_packet_content);
|
|
||||||
|
|
||||||
Packet::from_full(encrypted_packet_content)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn encrypt_packet(&self, packet: Packet, packet_number: u64) -> EncryptedPacket {
|
|
||||||
let mut bytes = packet.to_bytes(false, Packet::DEFAULT_BLOCK_SIZE);
|
|
||||||
|
|
||||||
// Prepare the main cipher.
|
|
||||||
let mut main_cipher = <SshChaCha20 as chacha20::cipher::KeyIvInit>::new(
|
|
||||||
&self.main_key,
|
|
||||||
&packet_number.to_be_bytes().into(),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Get the poly1305 key first, but don't use it yet!
|
|
||||||
// We encrypt-then-mac.
|
|
||||||
let mut poly1305_key = [0; poly1305::KEY_SIZE];
|
|
||||||
main_cipher.apply_keystream(&mut poly1305_key);
|
|
||||||
|
|
||||||
// As the first act of encryption, encrypt the length.
|
|
||||||
let mut len_cipher = <SshChaCha20 as chacha20::cipher::KeyIvInit>::new(
|
|
||||||
&self.header_key,
|
|
||||||
&packet_number.to_be_bytes().into(),
|
|
||||||
);
|
|
||||||
len_cipher.apply_keystream(&mut bytes[..4]);
|
|
||||||
|
|
||||||
// Advance ChaCha's block counter to 1
|
|
||||||
main_cipher
|
|
||||||
.seek(<chacha20::ChaCha20LegacyCore as chacha20::cipher::BlockSizeUser>::block_size());
|
|
||||||
// Encrypt the content of the packet, excluding the length and the MAC, which is not pushed yet.
|
|
||||||
main_cipher.apply_keystream(&mut bytes[4..]);
|
|
||||||
|
|
||||||
// Now, MAC the length || content, and push that to the end.
|
|
||||||
let mac = poly1305::Poly1305::new(&poly1305_key.into()).compute_unpadded(&bytes);
|
|
||||||
|
|
||||||
bytes.extend_from_slice(mac.as_slice());
|
|
||||||
|
|
||||||
EncryptedPacket::from_encrypted_full_bytes(bytes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <https://datatracker.ietf.org/doc/html/rfc5647>
|
|
||||||
/// <https://github.com/openssh/openssh-portable/blob/1ec0a64c5dc57b8a2053a93b5ef0d02ff8598e5c/PROTOCOL#L188C49-L188C64>
|
|
||||||
struct Aes256GcmOpenSsh<'a> {
|
|
||||||
key: aes_gcm::Key<aes_gcm::Aes256Gcm>,
|
|
||||||
nonce: &'a mut [u8; 12],
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> Aes256GcmOpenSsh<'a> {
|
|
||||||
fn from_state(keys: &'a mut [u8]) -> Self {
|
|
||||||
assert_eq!(keys.len(), 44);
|
|
||||||
Self {
|
|
||||||
key: <[u8; 32]>::try_from(&keys[..32]).unwrap().into(),
|
|
||||||
nonce: <&mut [u8; 12]>::try_from(&mut keys[32..]).unwrap(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn decrypt_len(&mut self, _: &mut [u8], _: u64) {
|
|
||||||
// AES-GCM does not encrypt the length.
|
|
||||||
// <https://datatracker.ietf.org/doc/html/rfc5647#section-7.3>
|
|
||||||
}
|
|
||||||
|
|
||||||
fn decrypt_packet(&mut self, mut bytes: RawPacket, _packet_number: u64) -> Result<Packet> {
|
|
||||||
let mut cipher = aes_gcm::Aes256Gcm::new(&self.key);
|
|
||||||
|
|
||||||
let mut len = [0; 4];
|
|
||||||
len.copy_from_slice(&bytes.full_packet()[..4]);
|
|
||||||
|
|
||||||
let tag_offset = bytes.full_packet().len() - 16;
|
|
||||||
let mut tag = [0; 16];
|
|
||||||
tag.copy_from_slice(&bytes.full_packet()[tag_offset..]);
|
|
||||||
|
|
||||||
let encrypted_packet_content = bytes.content_mut();
|
|
||||||
|
|
||||||
cipher
|
|
||||||
.decrypt_in_place_detached(
|
|
||||||
(&*self.nonce).into(),
|
|
||||||
&len,
|
|
||||||
encrypted_packet_content,
|
|
||||||
(&tag).into(),
|
|
||||||
)
|
|
||||||
.map_err(|_| crate::client_error!("failed to decrypt: invalid GCM MAC"))?;
|
|
||||||
self.inc_nonce();
|
|
||||||
|
|
||||||
Packet::from_full(encrypted_packet_content)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn encrypt_packet(&mut self, packet: Packet, _packet_number: u64) -> EncryptedPacket {
|
|
||||||
let mut bytes = packet.to_bytes(
|
|
||||||
false,
|
|
||||||
<aes_gcm::aes::Aes256 as aes_gcm::aes::cipher::BlockSizeUser>::block_size() as u8,
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut cipher = aes_gcm::Aes256Gcm::new(&self.key);
|
|
||||||
|
|
||||||
let (aad, plaintext) = bytes.split_at_mut(4);
|
|
||||||
|
|
||||||
let tag = cipher
|
|
||||||
.encrypt_in_place_detached((&*self.nonce).into(), aad, plaintext)
|
|
||||||
.unwrap();
|
|
||||||
bytes.extend_from_slice(&tag);
|
|
||||||
self.inc_nonce();
|
|
||||||
|
|
||||||
EncryptedPacket::from_encrypted_full_bytes(bytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn inc_nonce(&mut self) {
|
|
||||||
let mut carry = 1;
|
|
||||||
for i in (0..self.nonce.len()).rev() {
|
|
||||||
let n = self.nonce[i] as u16 + carry;
|
|
||||||
self.nonce[i] = n as u8;
|
|
||||||
carry = n >> 8;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
249
ssh-transport/src/crypto/encrypt.rs
Normal file
249
ssh-transport/src/crypto/encrypt.rs
Normal file
|
|
@ -0,0 +1,249 @@
|
||||||
|
use crate::Result;
|
||||||
|
use aes_gcm::{aead::AeadMutInPlace, KeyInit};
|
||||||
|
use chacha20::cipher::{StreamCipher, StreamCipherSeek};
|
||||||
|
use subtle::ConstantTimeEq;
|
||||||
|
|
||||||
|
use crate::packet::{EncryptedPacket, Packet, RawPacket};
|
||||||
|
|
||||||
|
use super::EncryptionAlgorithm;
|
||||||
|
|
||||||
|
pub const CHACHA20POLY1305: EncryptionAlgorithm = EncryptionAlgorithm {
|
||||||
|
name: "chacha20-poly1305@openssh.com",
|
||||||
|
iv_size: 0,
|
||||||
|
key_size: 64, // 32 for header, 32 for main
|
||||||
|
decrypt_len: |state, bytes, packet_number| {
|
||||||
|
let alg = ChaCha20Poly1305OpenSsh::from_state(state);
|
||||||
|
alg.decrypt_len(bytes, packet_number)
|
||||||
|
},
|
||||||
|
decrypt_packet: |state, bytes, packet_number| {
|
||||||
|
let alg = ChaCha20Poly1305OpenSsh::from_state(state);
|
||||||
|
alg.decrypt_packet(bytes, packet_number)
|
||||||
|
},
|
||||||
|
encrypt_packet: |state, packet, packet_number| {
|
||||||
|
let alg = ChaCha20Poly1305OpenSsh::from_state(state);
|
||||||
|
alg.encrypt_packet(packet, packet_number)
|
||||||
|
},
|
||||||
|
};
|
||||||
|
pub const AES256_GCM: EncryptionAlgorithm = EncryptionAlgorithm {
|
||||||
|
name: "aes256-gcm@openssh.com",
|
||||||
|
iv_size: 12,
|
||||||
|
key_size: 32,
|
||||||
|
decrypt_len: |state, bytes, packet_number| {
|
||||||
|
let mut alg = Aes256GcmOpenSsh::from_state(state);
|
||||||
|
alg.decrypt_len(bytes, packet_number)
|
||||||
|
},
|
||||||
|
decrypt_packet: |state, bytes, packet_number| {
|
||||||
|
let mut alg = Aes256GcmOpenSsh::from_state(state);
|
||||||
|
alg.decrypt_packet(bytes, packet_number)
|
||||||
|
},
|
||||||
|
encrypt_packet: |state, packet, packet_number| {
|
||||||
|
let mut alg = Aes256GcmOpenSsh::from_state(state);
|
||||||
|
alg.encrypt_packet(packet, packet_number)
|
||||||
|
},
|
||||||
|
};
|
||||||
|
/// RFC 4344 AES128 in counter mode.
|
||||||
|
/// <https://datatracker.ietf.org/doc/html/rfc4344#section-4>
|
||||||
|
pub const ENC_AES128_CTR: EncryptionAlgorithm = EncryptionAlgorithm {
|
||||||
|
name: "aes128-ctr",
|
||||||
|
iv_size: 12,
|
||||||
|
key_size: 32,
|
||||||
|
decrypt_len: |state, bytes, packet_number| {
|
||||||
|
let mut alg = Aes128Ctr::from_state(state);
|
||||||
|
alg.decrypt_len(bytes, packet_number)
|
||||||
|
},
|
||||||
|
decrypt_packet: |state, bytes, packet_number| todo!(),
|
||||||
|
encrypt_packet: |state, packet, packet_number| todo!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
/// `chacha20-poly1305@openssh.com` uses a 64-bit nonce, not the 96-bit one in the IETF version.
|
||||||
|
type SshChaCha20 = chacha20::ChaCha20Legacy;
|
||||||
|
|
||||||
|
/// <https://github.com/openssh/openssh-portable/blob/1ec0a64c5dc57b8a2053a93b5ef0d02ff8598e5c/PROTOCOL.chacha20poly1305>
|
||||||
|
struct ChaCha20Poly1305OpenSsh {
|
||||||
|
header_key: chacha20::Key,
|
||||||
|
main_key: chacha20::Key,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChaCha20Poly1305OpenSsh {
|
||||||
|
fn from_state(keys: &[u8]) -> Self {
|
||||||
|
assert_eq!(keys.len(), 64);
|
||||||
|
Self {
|
||||||
|
main_key: <[u8; 32]>::try_from(&keys[..32]).unwrap().into(),
|
||||||
|
header_key: <[u8; 32]>::try_from(&keys[32..]).unwrap().into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decrypt_len(&self, bytes: &mut [u8], packet_number: u64) {
|
||||||
|
// <https://github.com/openssh/openssh-portable/blob/1ec0a64c5dc57b8a2053a93b5ef0d02ff8598e5c/PROTOCOL.chacha20poly1305>
|
||||||
|
let mut cipher = <SshChaCha20 as chacha20::cipher::KeyIvInit>::new(
|
||||||
|
&self.header_key,
|
||||||
|
&packet_number.to_be_bytes().into(),
|
||||||
|
);
|
||||||
|
cipher.apply_keystream(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decrypt_packet(&self, mut bytes: RawPacket, packet_number: u64) -> Result<Packet> {
|
||||||
|
// <https://github.com/openssh/openssh-portable/blob/1ec0a64c5dc57b8a2053a93b5ef0d02ff8598e5c/PROTOCOL.chacha20poly1305>
|
||||||
|
|
||||||
|
let mut cipher = <SshChaCha20 as chacha20::cipher::KeyIvInit>::new(
|
||||||
|
&self.main_key,
|
||||||
|
&packet_number.to_be_bytes().into(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let tag_offset = bytes.full_packet().len() - 16;
|
||||||
|
let authenticated = &bytes.full_packet()[..tag_offset];
|
||||||
|
|
||||||
|
let mac = {
|
||||||
|
let mut poly1305_key = [0; poly1305::KEY_SIZE];
|
||||||
|
cipher.apply_keystream(&mut poly1305_key);
|
||||||
|
poly1305::Poly1305::new(&poly1305_key.into()).compute_unpadded(authenticated)
|
||||||
|
};
|
||||||
|
|
||||||
|
let read_tag = poly1305::Tag::from_slice(&bytes.full_packet()[tag_offset..]);
|
||||||
|
|
||||||
|
if !bool::from(mac.ct_eq(read_tag)) {
|
||||||
|
return Err(crate::client_error!(
|
||||||
|
"failed to decrypt: invalid poly1305 MAC"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Advance ChaCha's block counter to 1
|
||||||
|
cipher
|
||||||
|
.seek(<chacha20::ChaCha20LegacyCore as chacha20::cipher::BlockSizeUser>::block_size());
|
||||||
|
|
||||||
|
let encrypted_packet_content = bytes.content_mut();
|
||||||
|
cipher.apply_keystream(encrypted_packet_content);
|
||||||
|
|
||||||
|
Packet::from_full(encrypted_packet_content)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encrypt_packet(&self, packet: Packet, packet_number: u64) -> EncryptedPacket {
|
||||||
|
let mut bytes = packet.to_bytes(false, Packet::DEFAULT_BLOCK_SIZE);
|
||||||
|
|
||||||
|
// Prepare the main cipher.
|
||||||
|
let mut main_cipher = <SshChaCha20 as chacha20::cipher::KeyIvInit>::new(
|
||||||
|
&self.main_key,
|
||||||
|
&packet_number.to_be_bytes().into(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get the poly1305 key first, but don't use it yet!
|
||||||
|
// We encrypt-then-mac.
|
||||||
|
let mut poly1305_key = [0; poly1305::KEY_SIZE];
|
||||||
|
main_cipher.apply_keystream(&mut poly1305_key);
|
||||||
|
|
||||||
|
// As the first act of encryption, encrypt the length.
|
||||||
|
let mut len_cipher = <SshChaCha20 as chacha20::cipher::KeyIvInit>::new(
|
||||||
|
&self.header_key,
|
||||||
|
&packet_number.to_be_bytes().into(),
|
||||||
|
);
|
||||||
|
len_cipher.apply_keystream(&mut bytes[..4]);
|
||||||
|
|
||||||
|
// Advance ChaCha's block counter to 1
|
||||||
|
main_cipher
|
||||||
|
.seek(<chacha20::ChaCha20LegacyCore as chacha20::cipher::BlockSizeUser>::block_size());
|
||||||
|
// Encrypt the content of the packet, excluding the length and the MAC, which is not pushed yet.
|
||||||
|
main_cipher.apply_keystream(&mut bytes[4..]);
|
||||||
|
|
||||||
|
// Now, MAC the length || content, and push that to the end.
|
||||||
|
let mac = poly1305::Poly1305::new(&poly1305_key.into()).compute_unpadded(&bytes);
|
||||||
|
|
||||||
|
bytes.extend_from_slice(mac.as_slice());
|
||||||
|
|
||||||
|
EncryptedPacket::from_encrypted_full_bytes(bytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <https://datatracker.ietf.org/doc/html/rfc5647>
|
||||||
|
/// <https://github.com/openssh/openssh-portable/blob/1ec0a64c5dc57b8a2053a93b5ef0d02ff8598e5c/PROTOCOL#L188C49-L188C64>
|
||||||
|
struct Aes256GcmOpenSsh<'a> {
|
||||||
|
key: aes_gcm::Key<aes_gcm::Aes256Gcm>,
|
||||||
|
nonce: &'a mut [u8; 12],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Aes256GcmOpenSsh<'a> {
|
||||||
|
fn from_state(keys: &'a mut [u8]) -> Self {
|
||||||
|
assert_eq!(keys.len(), 44);
|
||||||
|
Self {
|
||||||
|
key: <[u8; 32]>::try_from(&keys[..32]).unwrap().into(),
|
||||||
|
nonce: <&mut [u8; 12]>::try_from(&mut keys[32..]).unwrap(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decrypt_len(&mut self, _: &mut [u8], _: u64) {
|
||||||
|
// AES-GCM does not encrypt the length.
|
||||||
|
// <https://datatracker.ietf.org/doc/html/rfc5647#section-7.3>
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decrypt_packet(&mut self, mut bytes: RawPacket, _packet_number: u64) -> Result<Packet> {
|
||||||
|
let mut cipher = aes_gcm::Aes256Gcm::new(&self.key);
|
||||||
|
|
||||||
|
let mut len = [0; 4];
|
||||||
|
len.copy_from_slice(&bytes.full_packet()[..4]);
|
||||||
|
|
||||||
|
let tag_offset = bytes.full_packet().len() - 16;
|
||||||
|
let mut tag = [0; 16];
|
||||||
|
tag.copy_from_slice(&bytes.full_packet()[tag_offset..]);
|
||||||
|
|
||||||
|
let encrypted_packet_content = bytes.content_mut();
|
||||||
|
|
||||||
|
cipher
|
||||||
|
.decrypt_in_place_detached(
|
||||||
|
(&*self.nonce).into(),
|
||||||
|
&len,
|
||||||
|
encrypted_packet_content,
|
||||||
|
(&tag).into(),
|
||||||
|
)
|
||||||
|
.map_err(|_| crate::client_error!("failed to decrypt: invalid GCM MAC"))?;
|
||||||
|
self.inc_nonce();
|
||||||
|
|
||||||
|
Packet::from_full(encrypted_packet_content)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encrypt_packet(&mut self, packet: Packet, _packet_number: u64) -> EncryptedPacket {
|
||||||
|
let mut bytes = packet.to_bytes(
|
||||||
|
false,
|
||||||
|
<aes_gcm::aes::Aes256 as aes_gcm::aes::cipher::BlockSizeUser>::block_size() as u8,
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut cipher = aes_gcm::Aes256Gcm::new(&self.key);
|
||||||
|
|
||||||
|
let (aad, plaintext) = bytes.split_at_mut(4);
|
||||||
|
|
||||||
|
let tag = cipher
|
||||||
|
.encrypt_in_place_detached((&*self.nonce).into(), aad, plaintext)
|
||||||
|
.unwrap();
|
||||||
|
bytes.extend_from_slice(&tag);
|
||||||
|
self.inc_nonce();
|
||||||
|
|
||||||
|
EncryptedPacket::from_encrypted_full_bytes(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn inc_nonce(&mut self) {
|
||||||
|
let mut carry = 1;
|
||||||
|
for i in (0..self.nonce.len()).rev() {
|
||||||
|
let n = self.nonce[i] as u16 + carry;
|
||||||
|
self.nonce[i] = n as u8;
|
||||||
|
carry = n >> 8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Aes128Ctr {
|
||||||
|
key: ctr::Ctr128BE<aes::Aes128>,
|
||||||
|
}
|
||||||
|
impl Aes128Ctr {
|
||||||
|
fn from_state(keys: &mut [u8]) -> Self {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decrypt_len(&mut self, _: &mut [u8], _: u64) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decrypt_packet(&mut self, mut bytes: RawPacket, _packet_number: u64) -> Result<Packet> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
fn encrypt_packet(&mut self, packet: Packet, _packet_number: u64) -> EncryptedPacket {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -205,10 +205,18 @@ impl ServerConnection {
|
||||||
debug!(name = %server_host_key_algorithm.name(), "Using host key algorithm");
|
debug!(name = %server_host_key_algorithm.name(), "Using host key algorithm");
|
||||||
|
|
||||||
let encryption_algorithms_client_to_server = AlgorithmNegotiation {
|
let encryption_algorithms_client_to_server = AlgorithmNegotiation {
|
||||||
supported: vec![crypto::ENC_CHACHA20POLY1305, crypto::ENC_AES256_GCM],
|
supported: vec![
|
||||||
|
crypto::encrypt::CHACHA20POLY1305,
|
||||||
|
crypto::encrypt::AES256_GCM,
|
||||||
|
// crypto::encrypt::ENC_AES128_CTR,
|
||||||
|
],
|
||||||
};
|
};
|
||||||
let encryption_algorithms_server_to_client = AlgorithmNegotiation {
|
let encryption_algorithms_server_to_client = AlgorithmNegotiation {
|
||||||
supported: vec![crypto::ENC_CHACHA20POLY1305, crypto::ENC_AES256_GCM],
|
supported: vec![
|
||||||
|
crypto::encrypt::CHACHA20POLY1305,
|
||||||
|
crypto::encrypt::AES256_GCM,
|
||||||
|
// crypto::encrypt::ENC_AES128_CTR,
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
let encryption_client_to_server = encryption_algorithms_client_to_server
|
let encryption_client_to_server = encryption_algorithms_client_to_server
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,8 @@ pub fn packet_type_to_string(packet_type: u8) -> Option<&'static str> {
|
||||||
6 => "SSH_MSG_SERVICE_ACCEPT",
|
6 => "SSH_MSG_SERVICE_ACCEPT",
|
||||||
20 => "SSH_MSG_KEXINIT",
|
20 => "SSH_MSG_KEXINIT",
|
||||||
21 => "SSH_MSG_NEWKEYS",
|
21 => "SSH_MSG_NEWKEYS",
|
||||||
|
30 => "SSH_MSG_KEX_ECDH_INIT",
|
||||||
|
31 => "SSH_MSG_KEX_ECDH_REPLY",
|
||||||
50 => "SSH_MSG_USERAUTH_REQUEST",
|
50 => "SSH_MSG_USERAUTH_REQUEST",
|
||||||
51 => "SSH_MSG_USERAUTH_FAILURE",
|
51 => "SSH_MSG_USERAUTH_FAILURE",
|
||||||
52 => "SSH_MSG_USERAUTH_SUCCESS",
|
52 => "SSH_MSG_USERAUTH_SUCCESS",
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,7 @@ ctors! {
|
||||||
description: string,
|
description: string,
|
||||||
language_tag: string,
|
language_tag: string,
|
||||||
);
|
);
|
||||||
|
fn new_msg_channel_window_adjust(SSH_MSG_CHANNEL_WINDOW_ADJUST; recipient_channel: u32, bytes_to_add: u32);
|
||||||
fn new_msg_channel_data(SSH_MSG_CHANNEL_DATA; recipient_channel: u32, data: string);
|
fn new_msg_channel_data(SSH_MSG_CHANNEL_DATA; recipient_channel: u32, data: string);
|
||||||
|
|
||||||
fn new_msg_channel_eof(SSH_MSG_CHANNEL_EOF; recipient_channel: u32);
|
fn new_msg_channel_eof(SSH_MSG_CHANNEL_EOF; recipient_channel: u32);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue