cleanup
This commit is contained in:
parent
c7dbe1c43d
commit
13f63c7700
|
@ -1,11 +1,11 @@
|
||||||
#![feature(array_windows)]
|
#![feature(array_chunks)]
|
||||||
#![feature(write_all_vectored)]
|
#![feature(write_all_vectored)]
|
||||||
use {
|
use {
|
||||||
aes_gcm::{
|
aes_gcm::{
|
||||||
aead::{self, AeadMutInPlace},
|
aead::{self, AeadMutInPlace},
|
||||||
AeadCore, Aes256Gcm, KeyInit,
|
AeadCore, Aes256Gcm, KeyInit,
|
||||||
},
|
},
|
||||||
ed25519_dalek::ed25519::signature::Signer,
|
ed25519_dalek::{self as ed, ed25519::signature::Signer},
|
||||||
rand_core::OsRng,
|
rand_core::OsRng,
|
||||||
std::{
|
std::{
|
||||||
collections::{HashMap, HashSet},
|
collections::{HashMap, HashSet},
|
||||||
|
@ -16,88 +16,51 @@ use {
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
slice,
|
slice,
|
||||||
str::FromStr,
|
str::FromStr,
|
||||||
sync::{atomic, OnceLock},
|
sync::{
|
||||||
|
atomic::{self, AtomicUsize},
|
||||||
|
Arc, Mutex,
|
||||||
|
},
|
||||||
time,
|
time,
|
||||||
},
|
},
|
||||||
x25519_dalek::{EphemeralSecret, SharedSecret},
|
x25519_dalek::{self as x, EphemeralSecret, SharedSecret},
|
||||||
};
|
};
|
||||||
|
|
||||||
static CONN_COUNT: atomic::AtomicUsize = atomic::AtomicUsize::new(0);
|
type Subcommand<'a, T> = (&'a str, &'a str, T);
|
||||||
static USER_DATA_DIR: OnceLock<PathBuf> = OnceLock::new();
|
type BaseSubcommand<'a> = Subcommand<'a, fn(&Cli) -> io::Result<()>>;
|
||||||
static SERVER_SECRET: OnceLock<ed25519_dalek::SigningKey> = OnceLock::new();
|
type ConsumeSubcommand<'a> = Subcommand<'a, fn(&Cli, EncriptedStream) -> io::Result<()>>;
|
||||||
|
|
||||||
#[derive(Default)]
|
type Username = [u8; 32];
|
||||||
struct Cli {
|
type Postname = [u8; 64];
|
||||||
program: String,
|
type Pk = [u8; 32];
|
||||||
args: Vec<String>,
|
type Nonce = u64;
|
||||||
flags: HashSet<String>,
|
|
||||||
options: HashMap<String, String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Cli {
|
const SUBCOMMANDS: &[BaseSubcommand] = &[
|
||||||
pub fn parse() -> Self {
|
|
||||||
let mut s = Self::default();
|
|
||||||
let mut args = std::env::args();
|
|
||||||
s.program = args.next().unwrap();
|
|
||||||
|
|
||||||
for arg in args {
|
|
||||||
if let Some(arg) = arg.strip_prefix("--") {
|
|
||||||
match arg.split_once('=') {
|
|
||||||
Some((name, value)) => _ = s.options.insert(name.to_owned(), value.to_owned()),
|
|
||||||
None => _ = s.flags.insert(arg.to_string()),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
s.args.push(arg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
s
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn arg(&self, index: usize) -> &str {
|
|
||||||
self.args.get(index).map_or("", String::as_str)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn expect_option(&self, name: &str) -> &str {
|
|
||||||
self.options.get(name).unwrap_or_else(|| panic!("--{name} is mandatory"))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn expect_poption<T: FromStr<Err: fmt::Display>>(&self, name: &str) -> T {
|
|
||||||
self.expect_option(name).parse::<T>().unwrap_or_else(|e| {
|
|
||||||
panic!("failed to parse --{name} as {}: {e}", std::any::type_name::<T>())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type Subcommand<'a> = (&'a str, &'a str, fn(&Cli) -> io::Result<()>);
|
|
||||||
|
|
||||||
fn help<T>(subs: &[(&str, &str, T)]) -> io::Result<()> {
|
|
||||||
for (name, desc, _) in subs {
|
|
||||||
eprintln!("{name} - {desc}");
|
|
||||||
}
|
|
||||||
Err(io::ErrorKind::NotFound.into())
|
|
||||||
}
|
|
||||||
|
|
||||||
const SUBCOMMANDS: &[Subcommand] = &[
|
|
||||||
("help", "print command descriptions", |_| help(SUBCOMMANDS)),
|
("help", "print command descriptions", |_| help(SUBCOMMANDS)),
|
||||||
("serve", "run the server", |cli| {
|
("serve", "run the server", |cli| {
|
||||||
let port = cli.expect_poption::<u16>("port");
|
let port = cli.expect_poption::<u16>("port");
|
||||||
let max_conns = cli.expect_poption::<usize>("max-conns");
|
|
||||||
USER_DATA_DIR.set(cli.expect_poption("user-data-path")).unwrap();
|
let config = Arc::new(ServerState {
|
||||||
SERVER_SECRET.set(cli.expect_poption::<HexSk>("secret").0).unwrap();
|
user_data_dir: cli.expect_poption("user-data-path"),
|
||||||
|
secret: cli.expect_poption::<HexSk>("secret").0,
|
||||||
|
active_ips: Default::default(),
|
||||||
|
max_conns: cli.expect_poption::<usize>("max-conns"),
|
||||||
|
conn_count: Default::default(),
|
||||||
|
});
|
||||||
let listener = TcpListener::bind((Ipv4Addr::UNSPECIFIED, port)).unwrap();
|
let listener = TcpListener::bind((Ipv4Addr::UNSPECIFIED, port)).unwrap();
|
||||||
for incoming in listener.incoming() {
|
for incoming in listener.incoming() {
|
||||||
match incoming {
|
match incoming {
|
||||||
Ok(c) => {
|
Ok(c) => {
|
||||||
if CONN_COUNT.fetch_add(1, atomic::Ordering::Relaxed) >= max_conns {
|
let Ok(std::net::SocketAddr::V4(addr)) =
|
||||||
CONN_COUNT.fetch_sub(1, atomic::Ordering::Relaxed);
|
c.peer_addr().ctx("obtaining socket addr")
|
||||||
|
else {
|
||||||
continue;
|
continue;
|
||||||
}
|
};
|
||||||
|
|
||||||
std::thread::spawn(move || {
|
let Some(guard) = ConnectionGuard::new(config.clone(), *addr.ip()) else {
|
||||||
_ = handle_client(c);
|
continue;
|
||||||
CONN_COUNT.fetch_sub(1, atomic::Ordering::Relaxed);
|
};
|
||||||
});
|
|
||||||
|
std::thread::spawn(move || _ = guard.config.handle_client(c));
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("accepting conn conn: {e}")
|
eprintln!("accepting conn conn: {e}")
|
||||||
|
@ -106,13 +69,24 @@ const SUBCOMMANDS: &[Subcommand] = &[
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}),
|
}),
|
||||||
|
("make-secret", "creates secret usable by the server", |_| {
|
||||||
|
println!("{}", DisplayHex(ed::SigningKey::generate(&mut OsRng).to_bytes()));
|
||||||
|
Ok(())
|
||||||
|
}),
|
||||||
|
("make-ping-command", "create a ping command to handshake with the server", |cli| {
|
||||||
|
let HexSk(secret) = cli.expect_poption::<HexSk>("secret");
|
||||||
|
let addr = cli.expect_poption::<SocketAddrV4>("addr");
|
||||||
|
let id = DisplayHex(ed::VerifyingKey::from(&secret).to_bytes());
|
||||||
|
println!("depell consume --server-identity={id} --addr={addr} ping");
|
||||||
|
Ok(())
|
||||||
|
}),
|
||||||
("make-profile", "create profile file (private key + name)", |cli| {
|
("make-profile", "create profile file (private key + name)", |cli| {
|
||||||
let name = cli.expect_option("name");
|
let name = cli.expect_option("name");
|
||||||
let name = str_as_username(name)
|
let name = str_as_username(name)
|
||||||
.ok_or(io::ErrorKind::InvalidData)
|
.ok_or(io::ErrorKind::InvalidData)
|
||||||
.ctx("name is limmited to 32 characters")?;
|
.ctx("name is limmited to 32 characters")?;
|
||||||
|
|
||||||
let &key = ed25519_dalek::SigningKey::generate(&mut rand_core::OsRng).as_bytes();
|
let &key = ed::SigningKey::generate(&mut rand_core::OsRng).as_bytes();
|
||||||
let profile = UserProfile { name, key };
|
let profile = UserProfile { name, key };
|
||||||
|
|
||||||
let out_file = cli.expect_option("out-file");
|
let out_file = cli.expect_option("out-file");
|
||||||
|
@ -123,7 +97,7 @@ const SUBCOMMANDS: &[Subcommand] = &[
|
||||||
let profile_path = cli.expect_option("profile");
|
let profile_path = cli.expect_option("profile");
|
||||||
let mut profile_file = fs::File::open(profile_path).ctx("opening profile file")?;
|
let mut profile_file = fs::File::open(profile_path).ctx("opening profile file")?;
|
||||||
let profile: UserProfile = read_struct(&mut profile_file).ctx("reading the profile")?;
|
let profile: UserProfile = read_struct(&mut profile_file).ctx("reading the profile")?;
|
||||||
let sx = x25519_dalek::EphemeralSecret::random_from_rng(OsRng);
|
let sx = x::EphemeralSecret::random_from_rng(OsRng);
|
||||||
let auth = UserAuth::sign(profile, &sx);
|
let auth = UserAuth::sign(profile, &sx);
|
||||||
|
|
||||||
let addr = cli.expect_poption::<SocketAddrV4>("addr");
|
let addr = cli.expect_poption::<SocketAddrV4>("addr");
|
||||||
|
@ -133,7 +107,7 @@ const SUBCOMMANDS: &[Subcommand] = &[
|
||||||
let HexPk(server_identity) = cli.expect_poption("server-identity");
|
let HexPk(server_identity) = cli.expect_poption("server-identity");
|
||||||
let sauth: ServerAuth = read_struct(&mut stream).ctx("reading server auth")?;
|
let sauth: ServerAuth = read_struct(&mut stream).ctx("reading server auth")?;
|
||||||
let secret = sauth
|
let secret = sauth
|
||||||
.verify(auth.pk, server_identity, sx)
|
.verify(auth.x, server_identity, sx)
|
||||||
.map_err(|_| io::ErrorKind::PermissionDenied)
|
.map_err(|_| io::ErrorKind::PermissionDenied)
|
||||||
.ctx("authenticating server")?;
|
.ctx("authenticating server")?;
|
||||||
let stream = EncriptedStream::new(stream, secret);
|
let stream = EncriptedStream::new(stream, secret);
|
||||||
|
@ -142,76 +116,51 @@ const SUBCOMMANDS: &[Subcommand] = &[
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
type ConsumeSubcommand<'a> = (&'a str, &'a str, fn(&Cli, EncriptedStream) -> io::Result<()>);
|
|
||||||
|
|
||||||
const CONSUME_SUBCOMMAND: &[ConsumeSubcommand] = &[
|
const CONSUME_SUBCOMMAND: &[ConsumeSubcommand] = &[
|
||||||
("help", "this help message", |_, _| help(CONSUME_SUBCOMMAND)),
|
("help", "this help message", |_, _| help(CONSUME_SUBCOMMAND)),
|
||||||
("ping", "ping the server to check the connection", |_, mut stream| {
|
("ping", "ping the server to check the connection", |_, mut stream| {
|
||||||
write_struct(&mut stream, &Qid::Ping)
|
let now = time::Instant::now();
|
||||||
}), //
|
write_struct(&mut stream, &Qid::Ping)?;
|
||||||
|
if !matches!(Aid::try_from(read_struct::<u16>(&mut stream)?)?, Aid::Pong) {
|
||||||
|
eprintln!("server did not respond with ping");
|
||||||
|
}
|
||||||
|
println!("{:?}", now.elapsed());
|
||||||
|
Ok(())
|
||||||
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
fn hex_to_array<const SIZE: usize>(s: &str) -> Result<[u8; SIZE], &'static str> {
|
|
||||||
let mut buf = [0u8; SIZE];
|
|
||||||
|
|
||||||
if s.len() != SIZE * 2 {
|
|
||||||
return Err("expected 64 character hex string");
|
|
||||||
}
|
|
||||||
|
|
||||||
fn byte_to_hex(val: u8) -> Result<u8, &'static str> {
|
|
||||||
Ok(match val {
|
|
||||||
b'0'..=b'9' => val - b'0',
|
|
||||||
b'a'..=b'f' => val - b'a' + 10,
|
|
||||||
b'A'..=b'F' => val - b'A' + 10,
|
|
||||||
_ => return Err("invalid hex char"),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
for (dst, &[a, b]) in buf.iter_mut().zip(s.as_bytes().array_windows()) {
|
|
||||||
*dst = byte_to_hex(a)? | (byte_to_hex(b)? << 4);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(buf)
|
|
||||||
}
|
|
||||||
|
|
||||||
struct HexPk(ed25519_dalek::VerifyingKey);
|
|
||||||
|
|
||||||
impl std::str::FromStr for HexPk {
|
|
||||||
type Err = &'static str;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
ed25519_dalek::VerifyingKey::from_bytes(&hex_to_array(s)?)
|
|
||||||
.map_err(|_| "hex code does not represent the valid key")
|
|
||||||
.map(Self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct HexSk(ed25519_dalek::SigningKey);
|
|
||||||
|
|
||||||
impl std::str::FromStr for HexSk {
|
|
||||||
type Err = &'static str;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
Ok(Self(ed25519_dalek::SigningKey::from_bytes(&hex_to_array(s)?)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn select_subcommand<'a, T>(depth: usize, list: &'a [(&str, &str, T)], cli: &Cli) -> &'a T {
|
|
||||||
&list.iter().find(|&&(name, ..)| name == cli.arg(depth)).unwrap_or(&list[0]).2
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() -> io::Result<()> {
|
fn main() -> io::Result<()> {
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
select_subcommand(0, SUBCOMMANDS, &cli)(&cli)
|
select_subcommand(0, SUBCOMMANDS, &cli)(&cli)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_client(mut stream: TcpStream) -> io::Result<()> {
|
fn help<T>(subs: &[Subcommand<T>]) -> io::Result<()> {
|
||||||
|
for (name, desc, _) in subs {
|
||||||
|
eprintln!("{name} - {desc}");
|
||||||
|
}
|
||||||
|
Err(io::ErrorKind::NotFound.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn select_subcommand<'a, T>(depth: usize, list: &'a [Subcommand<T>], cli: &Cli) -> &'a T {
|
||||||
|
&list.iter().find(|&&(name, ..)| name == cli.arg(depth)).unwrap_or(&list[0]).2
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ServerState {
|
||||||
|
user_data_dir: PathBuf,
|
||||||
|
secret: ed::SigningKey,
|
||||||
|
max_conns: usize,
|
||||||
|
active_ips: Mutex<HashSet<Ipv4Addr>>,
|
||||||
|
conn_count: AtomicUsize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ServerState {
|
||||||
|
fn handle_client(&self, mut stream: TcpStream) -> io::Result<()> {
|
||||||
let (user, sec) = {
|
let (user, sec) = {
|
||||||
let user_auth: UserAuth = read_struct(&mut stream).ctx("reading auth packet")?;
|
let user_auth: UserAuth = read_struct(&mut stream).ctx("reading auth packet")?;
|
||||||
let sx = x25519_dalek::EphemeralSecret::random_from_rng(OsRng);
|
let sx = x::EphemeralSecret::random_from_rng(OsRng);
|
||||||
let pk = x25519_dalek::PublicKey::from(&sx);
|
let pk = x::PublicKey::from(&sx);
|
||||||
let user = UserData::load(&user_auth, sx).ctx("loading user data")?;
|
let user = UserData::load(&user_auth, sx, self).ctx("loading user data")?;
|
||||||
let sauth = ServerAuth::sign(&user_auth, SERVER_SECRET.get().unwrap(), pk);
|
let sauth = ServerAuth::sign(&user_auth, &self.secret, pk);
|
||||||
write_struct(&mut stream, &sauth).ctx("sending handshare response")?;
|
write_struct(&mut stream, &sauth).ctx("sending handshare response")?;
|
||||||
user
|
user
|
||||||
};
|
};
|
||||||
|
@ -223,6 +172,37 @@ fn handle_client(mut stream: TcpStream) -> io::Result<()> {
|
||||||
Qid::Ping => write_struct(&mut stream, &Aid::Pong)?,
|
Qid::Ping => write_struct(&mut stream, &Aid::Pong)?,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ConnectionGuard {
|
||||||
|
ip: Ipv4Addr,
|
||||||
|
config: Arc<ServerState>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConnectionGuard {
|
||||||
|
fn new(config: Arc<ServerState>, ip: Ipv4Addr) -> Option<Self> {
|
||||||
|
if config.conn_count.fetch_add(1, atomic::Ordering::Relaxed) >= config.max_conns {
|
||||||
|
eprintln!("max connection cap reached");
|
||||||
|
config.conn_count.fetch_sub(1, atomic::Ordering::Relaxed);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !config.active_ips.lock().unwrap().insert(ip) {
|
||||||
|
eprintln!("ip already connected, dropping connection");
|
||||||
|
config.conn_count.fetch_sub(1, atomic::Ordering::Relaxed);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(Self { ip, config })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for ConnectionGuard {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.config.active_ips.lock().unwrap().remove(&self.ip);
|
||||||
|
self.config.conn_count.fetch_sub(1, atomic::Ordering::Relaxed);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[repr(u16)]
|
#[repr(u16)]
|
||||||
|
@ -230,6 +210,18 @@ enum Aid {
|
||||||
Pong,
|
Pong,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl TryFrom<u16> for Aid {
|
||||||
|
type Error = io::ErrorKind;
|
||||||
|
|
||||||
|
fn try_from(value: u16) -> Result<Self, Self::Error> {
|
||||||
|
if value <= Self::Pong as u16 {
|
||||||
|
Ok(unsafe { mem::transmute::<u16, Self>(value) })
|
||||||
|
} else {
|
||||||
|
Err(io::ErrorKind::NotFound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[repr(u16)]
|
#[repr(u16)]
|
||||||
enum Qid {
|
enum Qid {
|
||||||
Ping,
|
Ping,
|
||||||
|
@ -240,7 +232,7 @@ impl TryFrom<u16> for Qid {
|
||||||
|
|
||||||
fn try_from(value: u16) -> Result<Self, Self::Error> {
|
fn try_from(value: u16) -> Result<Self, Self::Error> {
|
||||||
if value <= Self::Ping as u16 {
|
if value <= Self::Ping as u16 {
|
||||||
Ok(unsafe { mem::transmute::<u16, Qid>(value) })
|
Ok(unsafe { mem::transmute::<u16, Self>(value) })
|
||||||
} else {
|
} else {
|
||||||
Err(io::ErrorKind::NotFound)
|
Err(io::ErrorKind::NotFound)
|
||||||
}
|
}
|
||||||
|
@ -260,7 +252,191 @@ impl<O, E: fmt::Display> Ctx for Result<O, E> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const ASOC_DATA: &[u8] = b"testicle torsion vizard";
|
fn username_as_str(name: &Username) -> Option<&str> {
|
||||||
|
let len = name.iter().rposition(|&b| b != 0xff)? + 1;
|
||||||
|
std::str::from_utf8(&name[..len]).ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn str_as_username(name: &str) -> Option<Username> {
|
||||||
|
if name.len() > mem::size_of::<Username>() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let mut buff = [0xffu8; mem::size_of::<Username>()];
|
||||||
|
buff[..name.len()].copy_from_slice(name.as_bytes());
|
||||||
|
Some(buff)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(packed)]
|
||||||
|
struct UserProfile {
|
||||||
|
name: Username,
|
||||||
|
key: ed::SecretKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(packed)]
|
||||||
|
struct UserAuth {
|
||||||
|
signature: ed::Signature,
|
||||||
|
pk: Pk,
|
||||||
|
x: x::PublicKey,
|
||||||
|
name: Username,
|
||||||
|
nonce: Nonce,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UserAuth {
|
||||||
|
fn sign(UserProfile { name, key }: UserProfile, sx: &x::EphemeralSecret) -> Self {
|
||||||
|
let nonce =
|
||||||
|
time::SystemTime::now().duration_since(time::SystemTime::UNIX_EPOCH).unwrap().as_secs();
|
||||||
|
let mut message = [0; mem::size_of::<Username>() + mem::size_of::<u64>()];
|
||||||
|
message[..mem::size_of::<Username>()].copy_from_slice(&name);
|
||||||
|
message[mem::size_of::<Username>()..].copy_from_slice(&nonce.to_le_bytes());
|
||||||
|
|
||||||
|
let signing_key = ed::SigningKey::from_bytes(&key);
|
||||||
|
let signature = signing_key.sign(&message);
|
||||||
|
let pk = ed::VerifyingKey::from(&signing_key).to_bytes();
|
||||||
|
let x = x::PublicKey::from(sx);
|
||||||
|
|
||||||
|
Self { signature, pk, x, name, nonce }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn verify(
|
||||||
|
&self,
|
||||||
|
pk: Pk,
|
||||||
|
nonce: Nonce,
|
||||||
|
sx: x::EphemeralSecret,
|
||||||
|
) -> Result<SharedSecret, ed::SignatureError> {
|
||||||
|
if nonce >= self.nonce {
|
||||||
|
eprintln!("invalid auth nonce");
|
||||||
|
return Err(ed::SignatureError::default());
|
||||||
|
}
|
||||||
|
|
||||||
|
let pk = ed::VerifyingKey::from_bytes(&pk)?;
|
||||||
|
|
||||||
|
let mut message = [0; mem::size_of::<Username>() + mem::size_of::<u64>()];
|
||||||
|
message[..mem::size_of::<Username>()].copy_from_slice(&self.name);
|
||||||
|
message[mem::size_of::<Username>()..].copy_from_slice(&self.nonce.to_le_bytes());
|
||||||
|
|
||||||
|
pk.verify_strict(&message, &self.signature)?;
|
||||||
|
|
||||||
|
Ok(sx.diffie_hellman(&self.x))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(packed)]
|
||||||
|
struct ServerAuth {
|
||||||
|
signature: ed::Signature,
|
||||||
|
x: x::PublicKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ServerAuth {
|
||||||
|
fn sign(user_auth: &UserAuth, sk: &ed::SigningKey, x: x::PublicKey) -> Self {
|
||||||
|
let signature = sk.sign(user_auth.x.as_bytes());
|
||||||
|
Self { signature, x }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn verify(
|
||||||
|
&self,
|
||||||
|
x: x::PublicKey,
|
||||||
|
pk: ed::VerifyingKey,
|
||||||
|
sx: EphemeralSecret,
|
||||||
|
) -> Result<SharedSecret, ed::SignatureError> {
|
||||||
|
pk.verify_strict(x.as_bytes(), &self.signature)?;
|
||||||
|
Ok(sx.diffie_hellman(&self.x))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct UserData {
|
||||||
|
header: UserHeader,
|
||||||
|
post_headers: fs::File,
|
||||||
|
posts: fs::File,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UserData {
|
||||||
|
fn load(
|
||||||
|
auth: &UserAuth,
|
||||||
|
sx: x::EphemeralSecret,
|
||||||
|
config: &ServerState,
|
||||||
|
) -> io::Result<(Self, SharedSecret)> {
|
||||||
|
const HEADER_PATH: &str = "header.bin";
|
||||||
|
const POST_HEADERS_PATH: &str = "post-headers.bin";
|
||||||
|
const POST_PATH: &str = "posts.bin";
|
||||||
|
|
||||||
|
let mut path = PathBuf::from_iter([
|
||||||
|
config.user_data_dir.as_path(),
|
||||||
|
username_as_str(&auth.name).ok_or(io::ErrorKind::InvalidData)?.as_ref(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if path.exists() {
|
||||||
|
let mut opts = fs::OpenOptions::new();
|
||||||
|
opts.write(true).read(true);
|
||||||
|
|
||||||
|
path.push(HEADER_PATH);
|
||||||
|
let mut header_file = opts.open(&path).ctx("opening user header file")?;
|
||||||
|
let mut header: UserHeader =
|
||||||
|
read_struct(&mut header_file).ctx("reading the user header")?;
|
||||||
|
path.pop();
|
||||||
|
|
||||||
|
let secret = auth
|
||||||
|
.verify(header.pk, header.nonce, sx)
|
||||||
|
.map_err(|_| io::ErrorKind::PermissionDenied)
|
||||||
|
.ctx("authenticating user")?;
|
||||||
|
|
||||||
|
header.nonce = auth.nonce;
|
||||||
|
write_struct(&mut header_file, &header).ctx("saving user nonce")?;
|
||||||
|
|
||||||
|
path.push(POST_HEADERS_PATH);
|
||||||
|
let post_headers = opts.open(&path).ctx("opening user post header file")?;
|
||||||
|
path.pop();
|
||||||
|
|
||||||
|
path.push(POST_PATH);
|
||||||
|
let posts = opts.open(&path).ctx("opening user post file")?;
|
||||||
|
path.pop();
|
||||||
|
|
||||||
|
Ok((Self { header, post_headers, posts }, secret))
|
||||||
|
} else {
|
||||||
|
let secret = auth
|
||||||
|
.verify(auth.pk, 0, sx)
|
||||||
|
.map_err(|_| io::ErrorKind::PermissionDenied)
|
||||||
|
.ctx("verifiing registratio signature")?;
|
||||||
|
|
||||||
|
fs::create_dir_all(&path).ctx("creating new user directory")?;
|
||||||
|
path.push(HEADER_PATH);
|
||||||
|
let header =
|
||||||
|
UserHeader { pk: auth.pk, nonce: auth.nonce, post_count: 0, runs: 0, imports: 0 };
|
||||||
|
fs::write(&path, as_bytes(&header)).ctx("writing new user header")?;
|
||||||
|
path.pop();
|
||||||
|
|
||||||
|
path.push(POST_HEADERS_PATH);
|
||||||
|
let post_headers = fs::File::create_new(&path).ctx("creating new user post headers")?;
|
||||||
|
path.pop();
|
||||||
|
|
||||||
|
path.push(POST_PATH);
|
||||||
|
let posts = fs::File::create_new(&path).ctx("creating new user posts")?;
|
||||||
|
path.pop();
|
||||||
|
|
||||||
|
Ok((Self { header, post_headers, posts }, secret))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(packed)]
|
||||||
|
struct UserHeader {
|
||||||
|
pk: Pk,
|
||||||
|
nonce: Nonce,
|
||||||
|
post_count: u32,
|
||||||
|
imports: u32,
|
||||||
|
runs: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(packed)]
|
||||||
|
struct PostHeader {
|
||||||
|
name: Postname,
|
||||||
|
timestamp: u64,
|
||||||
|
size: u32,
|
||||||
|
offset: u32,
|
||||||
|
imports: u32,
|
||||||
|
runs: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
const ASOC_DATA: &[u8] = b"testicle torsion wizard";
|
||||||
|
|
||||||
struct EncriptedStream {
|
struct EncriptedStream {
|
||||||
inner: TcpStream,
|
inner: TcpStream,
|
||||||
|
@ -339,7 +515,7 @@ impl Write for EncriptedStream {
|
||||||
fn read_struct<T>(stream: &mut impl Read) -> io::Result<T> {
|
fn read_struct<T>(stream: &mut impl Read) -> io::Result<T> {
|
||||||
let mut res = mem::MaybeUninit::uninit();
|
let mut res = mem::MaybeUninit::uninit();
|
||||||
stream.read_exact(as_mut_bytes(&mut res))?;
|
stream.read_exact(as_mut_bytes(&mut res))?;
|
||||||
unsafe { res.assume_init() }
|
Ok(unsafe { res.assume_init() })
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_struct<T>(stream: &mut impl Write, value: &T) -> io::Result<()> {
|
fn write_struct<T>(stream: &mut impl Write, value: &T) -> io::Result<()> {
|
||||||
|
@ -354,191 +530,112 @@ fn as_bytes<T>(value: &T) -> &[u8] {
|
||||||
unsafe { slice::from_raw_parts(value as *const _ as *const u8, mem::size_of::<T>()) }
|
unsafe { slice::from_raw_parts(value as *const _ as *const u8, mem::size_of::<T>()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
type Username = [u8; 32];
|
#[derive(Default)]
|
||||||
type Postname = [u8; 64];
|
struct Cli {
|
||||||
type Pk = [u8; 32];
|
program: String,
|
||||||
type Nonce = u64;
|
args: Vec<String>,
|
||||||
|
flags: HashSet<String>,
|
||||||
fn username_as_str(name: &Username) -> Option<&str> {
|
options: HashMap<String, String>,
|
||||||
let len = name.iter().rposition(|&b| b != 0xff)? + 1;
|
|
||||||
std::str::from_utf8(&name[..len]).ok()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn str_as_username(name: &str) -> Option<Username> {
|
impl Cli {
|
||||||
if name.len() > mem::size_of::<Username>() {
|
pub fn parse() -> Self {
|
||||||
return None;
|
let mut s = Self::default();
|
||||||
|
let mut args = std::env::args();
|
||||||
|
s.program = args.next().unwrap();
|
||||||
|
|
||||||
|
for arg in args {
|
||||||
|
if let Some(arg) = arg.strip_prefix("--") {
|
||||||
|
match arg.split_once('=') {
|
||||||
|
Some((name, value)) => _ = s.options.insert(name.to_owned(), value.to_owned()),
|
||||||
|
None => _ = s.flags.insert(arg.to_string()),
|
||||||
}
|
}
|
||||||
let mut buff = [0xffu8; mem::size_of::<Username>()];
|
|
||||||
buff[..name.len()].copy_from_slice(name.as_bytes());
|
|
||||||
Some(buff)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[repr(packed)]
|
|
||||||
struct UserProfile {
|
|
||||||
name: Username,
|
|
||||||
key: ed25519_dalek::SecretKey,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[repr(packed)]
|
|
||||||
struct UserAuth {
|
|
||||||
signature: ed25519_dalek::Signature,
|
|
||||||
pk: Pk,
|
|
||||||
x: x25519_dalek::PublicKey,
|
|
||||||
name: Username,
|
|
||||||
nonce: Nonce,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl UserAuth {
|
|
||||||
fn sign(UserProfile { name, key }: UserProfile, sx: &x25519_dalek::EphemeralSecret) -> Self {
|
|
||||||
let nonce =
|
|
||||||
time::SystemTime::now().duration_since(time::SystemTime::UNIX_EPOCH).unwrap().as_secs();
|
|
||||||
let mut message = [0; mem::size_of::<Username>() + mem::size_of::<u64>()];
|
|
||||||
message[..mem::size_of::<Username>()].copy_from_slice(&name);
|
|
||||||
message[mem::size_of::<Username>()..].copy_from_slice(&nonce.to_le_bytes());
|
|
||||||
|
|
||||||
let signing_key = ed25519_dalek::SigningKey::from_bytes(&key);
|
|
||||||
let signature = signing_key.sign(&message);
|
|
||||||
let pk = ed25519_dalek::VerifyingKey::from(&signing_key).to_bytes();
|
|
||||||
let x = x25519_dalek::PublicKey::from(sx);
|
|
||||||
|
|
||||||
Self { signature, pk, x, name, nonce }
|
|
||||||
}
|
|
||||||
|
|
||||||
fn verify(
|
|
||||||
&self,
|
|
||||||
pk: Pk,
|
|
||||||
nonce: Nonce,
|
|
||||||
sx: x25519_dalek::EphemeralSecret,
|
|
||||||
) -> Result<SharedSecret, ed25519_dalek::SignatureError> {
|
|
||||||
if nonce >= self.nonce {
|
|
||||||
eprintln!("invalid auth nonce");
|
|
||||||
return Err(ed25519_dalek::SignatureError::default());
|
|
||||||
}
|
|
||||||
|
|
||||||
let pk = ed25519_dalek::VerifyingKey::from_bytes(&pk)?;
|
|
||||||
|
|
||||||
let mut message = [0; mem::size_of::<Username>() + mem::size_of::<u64>()];
|
|
||||||
message[..mem::size_of::<Username>()].copy_from_slice(&self.name);
|
|
||||||
message[mem::size_of::<Username>()..].copy_from_slice(&nonce.to_le_bytes());
|
|
||||||
|
|
||||||
pk.verify_strict(&message, &self.signature)?;
|
|
||||||
|
|
||||||
Ok(sx.diffie_hellman(&self.x))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[repr(packed)]
|
|
||||||
struct ServerAuth {
|
|
||||||
signature: ed25519_dalek::Signature,
|
|
||||||
x: x25519_dalek::PublicKey,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ServerAuth {
|
|
||||||
fn sign(
|
|
||||||
user_auth: &UserAuth,
|
|
||||||
sk: &ed25519_dalek::SigningKey,
|
|
||||||
x: x25519_dalek::PublicKey,
|
|
||||||
) -> Self {
|
|
||||||
let signature = sk.sign(user_auth.x.as_bytes());
|
|
||||||
Self { signature, x }
|
|
||||||
}
|
|
||||||
|
|
||||||
fn verify(
|
|
||||||
&self,
|
|
||||||
x: Pk,
|
|
||||||
pk: ed25519_dalek::VerifyingKey,
|
|
||||||
sx: EphemeralSecret,
|
|
||||||
) -> Result<SharedSecret, ed25519_dalek::SignatureError> {
|
|
||||||
pk.verify_strict(&x, &self.signature)?;
|
|
||||||
Ok(sx.diffie_hellman(&self.x))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct UserData {
|
|
||||||
header: UserHeader,
|
|
||||||
post_headers: fs::File,
|
|
||||||
posts: fs::File,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl UserData {
|
|
||||||
fn load(
|
|
||||||
auth: &UserAuth,
|
|
||||||
sx: x25519_dalek::EphemeralSecret,
|
|
||||||
) -> io::Result<(Self, SharedSecret)> {
|
|
||||||
const HEADER_PATH: &str = "header.bin";
|
|
||||||
const POST_HEADERS_PATH: &str = "post-headers.bin";
|
|
||||||
const POST_PATH: &str = "posts.bin";
|
|
||||||
|
|
||||||
let mut path = PathBuf::from_iter([
|
|
||||||
USER_DATA_DIR.get().unwrap().as_path(),
|
|
||||||
username_as_str(&auth.name).ok_or(io::ErrorKind::InvalidData)?.as_ref(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
if path.exists() {
|
|
||||||
path.push(HEADER_PATH);
|
|
||||||
let mut header_file = fs::File::open(&path).ctx("opening user header file")?;
|
|
||||||
let mut header: UserHeader =
|
|
||||||
read_struct(&mut header_file).ctx("reading the user header")?;
|
|
||||||
path.pop();
|
|
||||||
|
|
||||||
let secret = auth
|
|
||||||
.verify(header.pk, header.nonce, sx)
|
|
||||||
.map_err(|_| io::ErrorKind::PermissionDenied)
|
|
||||||
.ctx("authenticating user")?;
|
|
||||||
|
|
||||||
header.nonce = auth.nonce;
|
|
||||||
write_struct(&mut header_file, &header).ctx("saving user nonce")?;
|
|
||||||
|
|
||||||
path.push(POST_HEADERS_PATH);
|
|
||||||
let post_headers = fs::File::open(&path).ctx("opening user post header file")?;
|
|
||||||
path.pop();
|
|
||||||
|
|
||||||
path.push(POST_PATH);
|
|
||||||
let posts = fs::File::open(&path).ctx("opening user post file")?;
|
|
||||||
path.pop();
|
|
||||||
|
|
||||||
Ok((Self { header, post_headers, posts }, secret))
|
|
||||||
} else {
|
} else {
|
||||||
let secret = auth
|
s.args.push(arg);
|
||||||
.verify(auth.pk, 0, sx)
|
|
||||||
.map_err(|_| io::ErrorKind::PermissionDenied)
|
|
||||||
.ctx("verifiing registratio signature")?;
|
|
||||||
|
|
||||||
fs::create_dir_all(&path).ctx("creating new user directory")?;
|
|
||||||
path.push(HEADER_PATH);
|
|
||||||
let header =
|
|
||||||
UserHeader { pk: auth.pk, nonce: auth.nonce, post_count: 0, runs: 0, imports: 0 };
|
|
||||||
fs::write(&path, as_bytes(&header)).ctx("writing new user header")?;
|
|
||||||
path.pop();
|
|
||||||
|
|
||||||
path.push(POST_HEADERS_PATH);
|
|
||||||
let post_headers = fs::File::create_new(&path).ctx("creating new user post headers")?;
|
|
||||||
path.pop();
|
|
||||||
|
|
||||||
path.push(POST_PATH);
|
|
||||||
let posts = fs::File::create_new(&path).ctx("creating new user posts")?;
|
|
||||||
path.pop();
|
|
||||||
|
|
||||||
Ok((Self { header, post_headers, posts }, secret))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn arg(&self, index: usize) -> &str {
|
||||||
|
self.args.get(index).map_or("", String::as_str)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn expect_option(&self, name: &str) -> &str {
|
||||||
|
self.options.get(name).unwrap_or_else(|| panic!("--{name}= is mandatory"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn expect_poption<T: FromStr<Err: fmt::Display>>(&self, name: &str) -> T {
|
||||||
|
self.expect_option(name).parse::<T>().unwrap_or_else(|e| {
|
||||||
|
panic!("failed to parse --{name}= as {}: {e}", std::any::type_name::<T>())
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[repr(packed)]
|
fn hex_to_array<const SIZE: usize>(s: &str) -> Result<[u8; SIZE], &'static str> {
|
||||||
struct UserHeader {
|
let mut buf = [0u8; SIZE];
|
||||||
pk: Pk,
|
|
||||||
nonce: Nonce,
|
if s.len() != SIZE * 2 {
|
||||||
post_count: u32,
|
return Err("expected 64 character hex string");
|
||||||
imports: u32,
|
}
|
||||||
runs: u32,
|
|
||||||
|
fn byte_to_hex(val: u8) -> Result<u8, &'static str> {
|
||||||
|
Ok(match val {
|
||||||
|
b'0'..=b'9' => val - b'0',
|
||||||
|
b'a'..=b'f' => val - b'a' + 10,
|
||||||
|
b'A'..=b'F' => val - b'A' + 10,
|
||||||
|
_ => return Err("invalid hex char"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for (dst, &[a, b]) in buf.iter_mut().zip(s.as_bytes().array_chunks()) {
|
||||||
|
*dst = byte_to_hex(b)? | (byte_to_hex(a)? << 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(buf)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[repr(packed)]
|
struct HexPk(ed::VerifyingKey);
|
||||||
struct PostHeader {
|
|
||||||
name: Postname,
|
impl std::str::FromStr for HexPk {
|
||||||
timestamp: u64,
|
type Err = &'static str;
|
||||||
size: u32,
|
|
||||||
offset: u32,
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
imports: u32,
|
ed::VerifyingKey::from_bytes(&hex_to_array(s)?)
|
||||||
runs: u32,
|
.map_err(|_| "hex code does not represent the valid key")
|
||||||
|
.map(Self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct HexSk(ed::SigningKey);
|
||||||
|
|
||||||
|
impl std::str::FromStr for HexSk {
|
||||||
|
type Err = &'static str;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
Ok(Self(ed::SigningKey::from_bytes(&hex_to_array(s)?)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DisplayHex([u8; 32]);
|
||||||
|
|
||||||
|
impl fmt::Display for DisplayHex {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
for b in self.0 {
|
||||||
|
write!(f, "{b:02x}")?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[test]
|
||||||
|
fn test_hex() {
|
||||||
|
let expected = [1u8; 32];
|
||||||
|
|
||||||
|
let hex = dbg!(DisplayHex(expected).to_string());
|
||||||
|
let got: [u8; 32] = hex_to_array(&hex).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(got, expected);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue