Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Unreleased

Thanks to @ALEX11BR for contributing to this release.
Thanks to @ALEX11BR and @trevarj for contributing to this release.

- Fixed handling of CR, LF, and tab characters in IRC format parser. IRC RFCs
don't allow standalone CR and LF characters, but some servers still send
Expand All @@ -13,6 +13,10 @@ Thanks to @ALEX11BR for contributing to this release.
temporary file would not be read properly when `$EDITOR` is closed.
- Passwords can now be read from external commands (e.g. a password manager).
See README for details. (#246, #315)
- Added support for SASL EXTERNAL authentication. See the
[wiki page][sasl-wiki] for more details. (#196, #363)

[sasl-wiki]: https://github.com/osa1/tiny/wiki/SASL-EXTERNAL

# 2021/11/07: 0.10.0

Expand Down Expand Up @@ -228,7 +232,7 @@ release.
# 2019/10/05: 0.5.0

Starting with this release tiny is no longer distributed on crates.io. Please
get it from the git repo at https://github.com/osa1/tiny.
get it from the git repo at <https://github.com/osa1/tiny>.

- With the exception of TUI most of tiny is rewritten for this release. See #138
for the details. The TLDR is that the code should now be easier to hack on.
Expand Down
16 changes: 13 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion crates/libtiny_client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ edition = "2021"
[features]
default = ["tls-rustls"]
tls-native = ["native-tls", "tokio-native-tls"]
tls-rustls = ["rustls-native-certs", "tokio-rustls"]
tls-rustls = ["rustls-native-certs", "tokio-rustls", "rustls-pemfile"]

[dependencies]
base64 = "0.13"
Expand All @@ -19,6 +19,7 @@ libtiny_wire = { path = "../libtiny_wire" }
log = "0.4"
native-tls = { version = "0.2", optional = true }
rustls-native-certs = { version = "0.6", optional = true }
rustls-pemfile = { version = "0.3", optional = true }
tokio = { version = "1.17", default-features = false, features = ["net", "rt", "io-util", "macros"] }
tokio-native-tls = { version = "0.3", optional = true }
tokio-rustls = { version = "0.23", optional = true }
Expand Down
26 changes: 21 additions & 5 deletions crates/libtiny_client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,19 @@ pub struct ServerInfo {
pub sasl_auth: Option<SASLAuth>,
}

/// SASL authentication credentials
/// SASL authentication mechanisms
/// - <https://ircv3.net/docs/sasl-mechs>
/// - <https://www.alphachat.net/sasl.xhtml>
#[derive(Debug, Clone)]
pub struct SASLAuth {
pub username: String,
pub password: String,
pub enum SASLAuth {
Plain {
username: String,
password: String,
},
External {
/// PEM-encoded X509 private cert and private key file
pem: Vec<u8>,
},
}

/// IRC client events. Returned by `Client` to the users via a channel.
Expand Down Expand Up @@ -387,10 +395,17 @@ async fn main_loop(
// Establish TCP connection to the server
//

let sasl_pem = if let Some(SASLAuth::External { pem }) = &server_info.sasl_auth {
Some(pem)
} else {
None
};

let stream = match try_connect(
addrs,
&serv_name,
server_info.tls,
sasl_pem,
&mut rcv_cmd,
&mut snd_ev,
)
Expand Down Expand Up @@ -623,14 +638,15 @@ async fn try_connect<S: StreamExt<Item = Cmd> + Unpin>(
addrs: Vec<SocketAddr>,
serv_name: &str,
use_tls: bool,
sasl_pem: Option<&Vec<u8>>,
rcv_cmd: &mut S,
snd_ev: &mut mpsc::Sender<Event>,
) -> TaskResult<Option<Stream>> {
let connect_task = async move {
for addr in addrs {
snd_ev.send(Event::Connecting(addr)).await.unwrap();
let mb_stream = if use_tls {
Stream::new_tls(addr, serv_name).await
Stream::new_tls(addr, serv_name, sasl_pem).await
} else {
Stream::new_tcp(addr).await
};
Expand Down
30 changes: 20 additions & 10 deletions crates/libtiny_client/src/state.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#![allow(clippy::zero_prefixed_literal)]

use crate::utils;
use crate::{utils, SASLAuth};
use crate::{Cmd, Event, ServerInfo};
use libtiny_common::{ChanName, ChanNameRef};
use libtiny_wire as wire;
Expand Down Expand Up @@ -630,7 +630,15 @@ impl StateInner {
match subcommand.as_ref() {
"ACK" => {
if params.iter().any(|cap| cap.as_str() == "sasl") {
snd_irc_msg.try_send(wire::authenticate("PLAIN")).unwrap();
if let Some(sasl) = &self.server_info.sasl_auth {
let msg = match sasl {
SASLAuth::Plain { .. } => "PLAIN",
SASLAuth::External { .. } => "EXTERNAL",
};
snd_irc_msg.try_send(wire::authenticate(msg)).unwrap();
} else {
warn!("SASL AUTH not set but got SASL ACK");
}
}
}
"NAK" => {
Expand All @@ -647,18 +655,20 @@ impl StateInner {
}
}

// https://ircv3.net/specs/extensions/sasl-3.1.html
AUTHENTICATE { ref param } => {
if param.as_str() == "+" {
// Empty AUTHENTICATE response; server accepted the specified SASL mechanism
// (PLAIN)
if let Some(ref auth) = self.server_info.sasl_auth {
let msg = format!(
"{}\x00{}\x00{}",
auth.username, auth.username, auth.password
);
snd_irc_msg
.try_send(wire::authenticate(&base64::encode(msg)))
.unwrap();
let msg = match auth {
SASLAuth::Plain { username, password } => {
let msg = format!("{}\x00{}\x00{}", username, username, password);
base64::encode(&msg)
}
// Reply with an empty response (Empty responses are sent as "AUTHENTICATE +")
SASLAuth::External { .. } => "+".to_string(),
};
snd_irc_msg.try_send(wire::authenticate(&msg)).unwrap();
}
}
}
Expand Down
96 changes: 76 additions & 20 deletions crates/libtiny_client/src/stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,27 +16,64 @@ use tokio_rustls::client::TlsStream;

#[cfg(feature = "tls-native")]
lazy_static! {
static ref TLS_CONNECTOR: tokio_native_tls::TlsConnector =
tokio_native_tls::TlsConnector::from(native_tls::TlsConnector::builder().build().unwrap());
static ref TLS_CONNECTOR: tokio_native_tls::TlsConnector = tls_connector(None);
}

#[cfg(feature = "tls-native")]
fn tls_connector(pem: Option<&Vec<u8>>) -> tokio_native_tls::TlsConnector {
use native_tls::Identity;

let mut builder = native_tls::TlsConnector::builder();
if let Some(pem) = pem {
let identity = Identity::from_pkcs8(pem, pem).expect("X509 Cert and private key");
builder.identity(identity);
}
tokio_native_tls::TlsConnector::from(builder.build().unwrap())
}

#[cfg(feature = "tls-rustls")]
lazy_static! {
static ref TLS_CONNECTOR: tokio_rustls::TlsConnector = {
use tokio_rustls::rustls;
let mut roots = rustls::RootCertStore::empty();
for cert in rustls_native_certs::load_native_certs().unwrap() {
roots.add(&rustls::Certificate(cert.0)).unwrap();
}
let config = rustls::ClientConfig::builder()
.with_safe_defaults()
.with_root_certificates(roots)
.with_no_client_auth();
tokio_rustls::TlsConnector::from(std::sync::Arc::new(config))
static ref TLS_CONNECTOR: tokio_rustls::TlsConnector = tls_connector(None);
}

#[cfg(feature = "tls-rustls")]
fn tls_connector(sasl: Option<&Vec<u8>>) -> tokio_rustls::TlsConnector {
use std::io::{Cursor, Seek, SeekFrom};
use tokio_rustls::rustls::{Certificate, ClientConfig, PrivateKey, RootCertStore};

let mut roots = RootCertStore::empty();
for cert in rustls_native_certs::load_native_certs().expect("could not load platform certs") {
roots.add(&Certificate(cert.0)).unwrap();
}

let builder = ClientConfig::builder()
.with_safe_defaults()
.with_root_certificates(roots);

let config = if let Some(pem) = sasl {
let mut buf = Cursor::new(pem);
// extract certificate
let cert = rustls_pemfile::certs(&mut buf)
.expect("Could not parse PKCS8 PEM")
.pop()
.expect("Cert PEM must have at least one cert");

// extract private key
buf.seek(SeekFrom::Start(0)).unwrap();
let key = rustls_pemfile::pkcs8_private_keys(&mut buf)
.expect("Could not parse PKCS8 PEM")
.pop()
.expect("Cert PEM must have at least one private key");

builder
.with_single_cert(vec![Certificate(cert)], PrivateKey(key))
.expect("Client auth cert")
} else {
builder.with_no_client_auth()
};
tokio_rustls::TlsConnector::from(std::sync::Arc::new(config))
}

#[derive(Debug)]
// We box the fields to reduce type size. Without boxing the type size is 64 with native-tls and
// 1288 with native-tls. With boxing it's 16 in both. More importantly, there's a large size
// difference between the variants when using rustls, see #189.
Expand Down Expand Up @@ -73,18 +110,37 @@ impl Stream {
}

#[cfg(feature = "tls-native")]
pub(crate) async fn new_tls(addr: SocketAddr, host_name: &str) -> Result<Stream, StreamError> {
pub(crate) async fn new_tls(
addr: SocketAddr,
host_name: &str,
sasl: Option<&Vec<u8>>,
) -> Result<Stream, StreamError> {
let tcp_stream = TcpStream::connect(addr).await?;
let tls_stream = TLS_CONNECTOR.connect(host_name, tcp_stream).await?;
// If SASL EXTERNAL is enabled create a new TLS connector with client auth cert
let tls_stream = if sasl.is_some() {
tls_connector(sasl).connect(host_name, tcp_stream).await?
} else {
TLS_CONNECTOR.connect(host_name, tcp_stream).await?
};
Ok(Stream::TlsStream(tls_stream.into()))
}

#[cfg(feature = "tls-rustls")]
pub(crate) async fn new_tls(addr: SocketAddr, host_name: &str) -> Result<Stream, StreamError> {
pub(crate) async fn new_tls(
addr: SocketAddr,
host_name: &str,
sasl: Option<&Vec<u8>>,
) -> Result<Stream, StreamError> {
use tokio_rustls::rustls::ServerName;

let tcp_stream = TcpStream::connect(addr).await?;
let name = tokio_rustls::rustls::ServerName::try_from(host_name)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
let tls_stream = TLS_CONNECTOR.connect(name, tcp_stream).await?;
let name = ServerName::try_from(host_name).unwrap();
// If SASL EXTERNAL is enabled create a new TLS connector with client auth cert
let tls_stream = if sasl.is_some() {
tls_connector(sasl).connect(name, tcp_stream).await?
} else {
TLS_CONNECTOR.connect(name, tcp_stream).await?
};
Ok(Stream::TlsStream(tls_stream.into()))
}
}
Expand Down
Loading