You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
431 lines
13 KiB
431 lines
13 KiB
//! Socket encryption wrapper
|
|
use super::*;
|
|
use cryptohelpers::{
|
|
rsa::{
|
|
self,
|
|
RsaPublicKey,
|
|
RsaPrivateKey,
|
|
|
|
openssl::{
|
|
symm::Crypter,
|
|
error::ErrorStack,
|
|
},
|
|
},
|
|
sha256,
|
|
};
|
|
use chacha20stream::{
|
|
AsyncSink,
|
|
AsyncSource,
|
|
|
|
Key, IV,
|
|
|
|
cha,
|
|
};
|
|
use std::sync::Arc;
|
|
use tokio::{
|
|
sync::{
|
|
RwLock,
|
|
RwLockReadGuard,
|
|
RwLockWriteGuard,
|
|
},
|
|
};
|
|
use std::{
|
|
io,
|
|
fmt,
|
|
task::{
|
|
Context, Poll,
|
|
},
|
|
pin::Pin,
|
|
marker::Unpin,
|
|
};
|
|
use smallvec::SmallVec;
|
|
|
|
/// Size of a single RSA ciphertext.
|
|
pub const RSA_CIPHERTEXT_SIZE: usize = 512;
|
|
|
|
/// A single, full block of RSA ciphertext.
|
|
type RsaCiphertextBlock = [u8; RSA_CIPHERTEXT_SIZE];
|
|
|
|
/// Max size to read when exchanging keys
|
|
const TRANS_KEY_MAX_SIZE: usize = 4096;
|
|
|
|
/// Encrypted socket information.
|
|
#[derive(Debug)]
|
|
struct ESockInfo {
|
|
us: RsaPrivateKey,
|
|
them: Option<RsaPublicKey>,
|
|
}
|
|
|
|
impl ESockInfo
|
|
{
|
|
/// Generate a new private key
|
|
pub fn new(us: impl Into<RsaPrivateKey>) -> Self
|
|
{
|
|
Self {
|
|
us: us.into(),
|
|
them: None,
|
|
}
|
|
}
|
|
|
|
/// Generate a new private key for the local endpoint
|
|
pub fn generate() -> Result<Self, rsa::Error>
|
|
{
|
|
Ok(Self::new(RsaPrivateKey::generate()?))
|
|
}
|
|
}
|
|
|
|
/// The encryption state of the Tx and Rx instances.
|
|
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
|
|
struct ESockState {
|
|
encr: bool,
|
|
encw: bool,
|
|
}
|
|
|
|
impl Default for ESockState
|
|
{
|
|
#[inline]
|
|
fn default() -> Self
|
|
{
|
|
Self {
|
|
encr: false,
|
|
encw: false,
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/// Contains a cc20 Key and IV that can be serialized and then encrypted
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
struct ESockSessionKey
|
|
{
|
|
key: Key,
|
|
iv: IV,
|
|
}
|
|
|
|
impl fmt::Display for ESockSessionKey
|
|
{
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result
|
|
{
|
|
write!(f, "Key: {}, IV: {}", self.key.hex(), self.iv.hex())
|
|
}
|
|
}
|
|
|
|
|
|
impl ESockSessionKey
|
|
{
|
|
/// Generate a new cc20 key + iv,
|
|
pub fn generate() -> Self
|
|
{
|
|
let (key,iv) = cha::keygen();
|
|
Self{key,iv}
|
|
}
|
|
|
|
/// Generate an encryption device
|
|
pub fn to_decrypter(&self) -> Result<Crypter, ErrorStack>
|
|
{
|
|
cha::decrypter(&self.key, &self.iv)
|
|
}
|
|
|
|
/// Generate an encryption device
|
|
pub fn to_encrypter(&self) -> Result<Crypter, ErrorStack>
|
|
{
|
|
cha::encrypter(&self.key, &self.iv)
|
|
}
|
|
|
|
/// Encrypt with RSA
|
|
pub fn to_ciphertext<K: ?Sized + rsa::PublicKey>(&self, rsa_key: &K) -> eyre::Result<RsaCiphertextBlock>
|
|
{
|
|
let mut output = [0u8; RSA_CIPHERTEXT_SIZE];
|
|
let mut temp = SmallVec::<[u8; RSA_CIPHERTEXT_SIZE]>::new(); // We know size will fit into here.
|
|
serde_cbor::to_writer(&mut temp, self)
|
|
.wrap_err(eyre!("Failed to CBOR encode session key to buffer"))
|
|
.with_section(|| self.clone().header("Session key was"))?;
|
|
debug_assert!(temp.len() < RSA_CIPHERTEXT_SIZE);
|
|
|
|
let _wr = rsa::encrypt_slice_sync(&temp, rsa_key, &mut &mut output[..])
|
|
.wrap_err(eyre!("Failed to encrypt session key with RSA public key"))
|
|
.with_section(|| self.clone().header("Session key was"))
|
|
.with_section({let temp = temp.len(); move || temp.header("Encoded data size was")})
|
|
.with_section(move || base64::encode(temp).header("Encoded data (base64) was"))?;
|
|
debug_assert_eq!(_wr, output.len());
|
|
|
|
Ok(output)
|
|
}
|
|
|
|
/// Decrypt from RSA
|
|
pub fn from_ciphertext<K: ?Sized + rsa::PrivateKey>(data: &[u8; RSA_CIPHERTEXT_SIZE], rsa_key: &K) -> eyre::Result<Self>
|
|
where <K as rsa::PublicKey>::KeyType: rsa::openssl::pkey::HasPrivate //ugh, why do we have to have this bound??? it should be implied ffs... :/
|
|
{
|
|
let mut temp = SmallVec::<[u8; RSA_CIPHERTEXT_SIZE]>::new();
|
|
rsa::decrypt_slice_sync(data, rsa_key, &mut temp)
|
|
.wrap_err(eyre!("Failed to decrypt ciphertext to session key"))
|
|
.with_section({let data = data.len(); move || data.header("Ciphertext length was")})
|
|
.with_section(|| base64::encode(data).header("Ciphertext was"))?;
|
|
Ok(serde_cbor::from_slice(&temp[..])
|
|
.wrap_err(eyre!("Failed to decode CBOR data to session key object"))
|
|
.with_section({let temp = temp.len(); move || temp.header("Encoded data size was")})
|
|
.with_section(move || base64::encode(temp).header("Encoded data (base64) was"))?)
|
|
}
|
|
}
|
|
|
|
/// A tx+rx socket.
|
|
#[pin_project]
|
|
#[derive(Debug)]
|
|
pub struct ESock<W, R> {
|
|
info: ESockInfo,
|
|
|
|
state: ESockState,
|
|
|
|
#[pin]
|
|
rx: AsyncSource<R>,
|
|
#[pin]
|
|
tx: AsyncSink<W>,
|
|
}
|
|
|
|
impl<W: AsyncWrite, R: AsyncRead> ESock<W, R>
|
|
{
|
|
pub fn inner(&self) -> (&W, &R)
|
|
{
|
|
(self.tx.inner(), self.rx.inner())
|
|
}
|
|
|
|
fn inner_mut(&mut self) -> (&mut W, &mut R)
|
|
{
|
|
(self.tx.inner_mut(), self.rx.inner_mut())
|
|
}
|
|
|
|
///Get a mutable ref to unencrypted read+write
|
|
fn unencrypted(&mut self) -> (&mut W, &mut R)
|
|
{
|
|
(self.tx.inner_mut(), self.rx.inner_mut())
|
|
}
|
|
/// Get a mutable ref to encrypted write+read
|
|
fn encrypted(&mut self) -> (&mut AsyncSink<W>, &mut AsyncSource<R>)
|
|
{
|
|
(&mut self.tx, &mut self.rx)
|
|
}
|
|
|
|
/// Have the RSA keys been exchanged?
|
|
pub fn has_exchanged(&self) -> bool
|
|
{
|
|
self.info.them.is_some()
|
|
}
|
|
|
|
/// Is the Write + Read operation encrypted? Tuple is `(Tx, Rx)`.
|
|
pub fn is_encrypted(&self) -> (bool, bool)
|
|
{
|
|
(self.state.encw, self.state.encr)
|
|
}
|
|
}
|
|
|
|
impl<W: AsyncWrite+ Unpin, R: AsyncRead + Unpin> ESock<W, R>
|
|
{
|
|
/// Get the Tx and Rx of the stream.
|
|
///
|
|
/// # Returns
|
|
/// Returns encrypted stream halfs if the stream is encrypted, unencrypted if not.
|
|
pub fn stream(&mut self) -> (&mut (dyn AsyncWrite + Unpin + '_), &mut (dyn AsyncRead + Unpin + '_))
|
|
{
|
|
(if self.state.encw {
|
|
&mut self.tx
|
|
} else {
|
|
self.tx.inner_mut()
|
|
}, if self.state.encr {
|
|
&mut self.rx
|
|
} else {
|
|
self.rx.inner_mut()
|
|
})
|
|
}
|
|
/// Enable write encryption
|
|
pub async fn set_encrypted_write(&mut self, set: bool) -> eyre::Result<()>
|
|
{
|
|
use tokio::prelude::*;
|
|
if set {
|
|
let session_key = ESockSessionKey::generate();
|
|
let data = {
|
|
let them = self.info.them.as_ref().expect("Cannot set encrypted write when keys have not been exchanged");
|
|
session_key.to_ciphertext(them)
|
|
.wrap_err(eyre!("Failed to encrypt session key with foreign endpoint's key"))
|
|
.with_section(|| session_key.to_string().header("Session key was"))
|
|
.with_section(|| them.to_string().header("Foreign pubkey was"))?
|
|
};
|
|
let crypter = session_key.to_encrypter()
|
|
.wrap_err(eyre!("Failed to create encryption device from session key for Tx"))
|
|
.with_section(|| session_key.to_string().header("Session key was"))?;
|
|
// Send rsa `data` over unencrypted endpoint
|
|
self.unencrypted().0.write_all(&data[..]).await
|
|
.wrap_err(eyre!("Failed to write ciphertext to endpoint"))
|
|
.with_section(|| data.to_base64_string().header("Ciphertext of session key was"))?;
|
|
// Set crypter of `tx` to `session_key`.
|
|
*self.tx.crypter_mut() = crypter;
|
|
// Set `encw` to true
|
|
self.state.encw = true;
|
|
Ok(())
|
|
} else {
|
|
self.state.encw = false;
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// Enable read encryption
|
|
///
|
|
/// The other endpoint must have sent a `set_encrypted_write()`
|
|
pub async fn set_encrypted_read(&mut self, set: bool) -> eyre::Result<()>
|
|
{
|
|
use tokio::prelude::*;
|
|
if set {
|
|
let mut data = [0u8; RSA_CIPHERTEXT_SIZE];
|
|
// Read `data` from unencrypted endpoint
|
|
self.unencrypted().1.read_exact(&mut data[..]).await
|
|
.wrap_err(eyre!("Failed to read ciphertext from endpoint"))?;
|
|
// Decrypt `data`
|
|
let session_key = ESockSessionKey::from_ciphertext(&data, &self.info.us)
|
|
.wrap_err(eyre!("Failed to decrypt session key from ciphertext"))
|
|
.with_section(|| data.to_base64_string().header("Ciphertext was"))
|
|
.with_section(|| self.info.us.to_string().header("Our RSA key is"))?;
|
|
// Set crypter of `rx` to `session_key`.
|
|
*self.rx.crypter_mut() = session_key.to_decrypter()
|
|
.wrap_err(eyre!("Failed to create decryption device from session key for Rx"))
|
|
.with_section(|| session_key.to_string().header("Decrypted session key was"))?;
|
|
// Set `encr` to true
|
|
self.state.encr = true;
|
|
Ok(())
|
|
} else {
|
|
|
|
self.state.encr = false;
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// Get dynamic ref to unencrypted write+read
|
|
fn unencrypted_dyn(&mut self) -> (&mut (dyn AsyncWrite + Unpin + '_), &mut (dyn AsyncRead + Unpin + '_))
|
|
{
|
|
(self.tx.inner_mut(), self.rx.inner_mut())
|
|
}
|
|
/// Get dynamic ref to encrypted write+read
|
|
fn encrypted_dyn(&mut self) -> (&mut (dyn AsyncWrite + Unpin + '_), &mut (dyn AsyncRead + Unpin + '_))
|
|
{
|
|
(&mut self.tx, &mut self.rx)
|
|
}
|
|
/// Exchange keys.
|
|
pub async fn exchange(&mut self) -> eyre::Result<()>
|
|
{
|
|
use tokio::prelude::*;
|
|
let our_key = self.info.us.get_public_parts();
|
|
let (tx, rx) = self.inner_mut();
|
|
let read_fut = {
|
|
|
|
async move {
|
|
// Read the public key from `rx`.
|
|
//TODO: Find pubkey max size.
|
|
let mut sz_buf = [0u8; std::mem::size_of::<u64>()];
|
|
rx.read_exact(&mut sz_buf[..]).await
|
|
.wrap_err(eyre!("Failed to read size of pubkey form endpoint"))?;
|
|
let sz64 = u64::from_be_bytes(sz_buf);
|
|
let sz= match usize::try_from(sz64)
|
|
.wrap_err(eyre!("Read size could not fit into u64"))
|
|
.with_section(|| format!("{:?}", sz_buf).header("Read buffer was"))
|
|
.with_section(|| u64::from_be_bytes(sz_buf).header("64=bit size value was"))
|
|
.with_warning(|| "This should not happen, it is only possible when you are running a machine with a pointer size lower than 64 bits.")
|
|
.with_suggestion(|| "The message is likely malformed. If it is not, then you are communicating with an endpoint of 64 bits whereas your pointer size is far less.")? {
|
|
x if x > TRANS_KEY_MAX_SIZE => return Err(eyre!("Recv'd key size exceeded max acceptable key buffer size")),
|
|
x => x
|
|
};
|
|
let mut key_bytes = Vec::with_capacity(sz);
|
|
tokio::io::copy(&mut rx.take(sz64), &mut key_bytes).await
|
|
.wrap_err("Failed to read key bytes into buffer")
|
|
.with_section(move || sz64.header("Pubkey size to read was"))?;
|
|
if key_bytes.len() != sz {
|
|
return Err(eyre!("Could not read required bytes"));
|
|
}
|
|
let k = RsaPublicKey::from_bytes(&key_bytes)
|
|
.wrap_err("Failed to construct RSA public key from read bytes")
|
|
.with_section(|| sz.header("Pubkey size was"))
|
|
.with_section(move || key_bytes.to_base64_string().header("Pubkey bytes were"))?;
|
|
|
|
Result::<RsaPublicKey, eyre::Report>::Ok(k)
|
|
}
|
|
};
|
|
let write_fut = {
|
|
let key_bytes = our_key.to_bytes();
|
|
assert!(key_bytes.len() <= TRANS_KEY_MAX_SIZE);
|
|
let sz64 = u64::try_from(key_bytes.len())
|
|
.wrap_err(eyre!("Size of our pubkey could not fit into u64"))
|
|
.with_section(|| key_bytes.len().header("Size was"))
|
|
.with_warning(|| "This should not happen, it is only possible when you are running a machine with a pointer size larger than 64 bits.")
|
|
.with_warning(|| "There was likely internal memory corruption.")?;
|
|
let sz_buf = sz64.to_be_bytes();
|
|
async move {
|
|
tx.write_all(&sz_buf[..]).await
|
|
.wrap_err(eyre!("Failed to write key size"))
|
|
.with_section(|| sz64.header("Key size bytes were"))
|
|
.with_section(|| format!("{:?}", sz_buf).header("Key size bytes (BE) were"))?;
|
|
tx.write_all(&key_bytes[..]).await
|
|
.wrap_err(eyre!("Failed to write key bytes"))
|
|
.with_section(|| sz64.header("Size of key was"))
|
|
.with_section(|| key_bytes.to_base64_string().header("Key bytes are"))?;
|
|
Result::<(), eyre::Report>::Ok(())
|
|
}
|
|
};
|
|
let (send, recv) = tokio::join! [write_fut, read_fut];
|
|
send.wrap_err("Failed to send our pubkey")?;
|
|
let recv = recv.wrap_err("Failed to receive foreign pubkey")?;
|
|
self.info.them = Some(recv);
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// Write half for `ESock`.
|
|
#[pin_project]
|
|
#[derive(Debug)]
|
|
pub struct ESockWriteHalf<W>(Arc<(ESockInfo, RwLock<ESockState>)>, #[pin] AsyncSink<W>);
|
|
|
|
/// Read half for `ESock`.
|
|
#[pin_project]
|
|
#[derive(Debug)]
|
|
pub struct ESockReadHalf<R>(Arc<(ESockInfo, RwLock<ESockState>)>, #[pin] AsyncSource<R>);
|
|
|
|
#[cfg(test)]
|
|
mod tests
|
|
{
|
|
#[test]
|
|
fn rsa_ciphertext_len() -> crate::eyre::Result<()>
|
|
{
|
|
let data = {
|
|
use chacha20stream::cha::{KEY_SIZE, IV_SIZE};
|
|
let (key, iv) = chacha20stream::cha::keygen();
|
|
let (sz, d) = crate::bin::collect_slices_exact::<&[u8], _, {KEY_SIZE + IV_SIZE}>([key.as_ref(), iv.as_ref()]);
|
|
assert_eq!(sz, d.len());
|
|
d
|
|
};
|
|
println!("KEY+IV: {} bytes", data.len());
|
|
|
|
let key = cryptohelpers::rsa::RsaPublicKey::generate()?;
|
|
let rsa = cryptohelpers::rsa::encrypt_slice_to_vec(data, &key)?;
|
|
println!("Rsa ciphertext size: {}", rsa.len());
|
|
|
|
assert_eq!(rsa.len(), super::RSA_CIPHERTEXT_SIZE, "Incorrect RSA ciphertext length constant for cc20 KEY+IV encoding.");
|
|
|
|
Ok(())
|
|
}
|
|
#[test]
|
|
fn rsa_serial_ciphertext_len() -> crate::eyre::Result<()>
|
|
{
|
|
let data = serde_cbor::to_vec(&{
|
|
let (key, iv) = chacha20stream::cha::keygen();
|
|
super::ESockSessionKey {
|
|
key, iv,
|
|
}
|
|
}).expect("Failed to CBOR encode Key+IV");
|
|
println!("(cbor) KEY+IV: {} bytes", data.len());
|
|
|
|
let key = cryptohelpers::rsa::RsaPublicKey::generate()?;
|
|
let rsa = cryptohelpers::rsa::encrypt_slice_to_vec(data, &key)?;
|
|
println!("Rsa ciphertext size: {}", rsa.len());
|
|
|
|
assert_eq!(rsa.len(), super::RSA_CIPHERTEXT_SIZE, "Incorrect RSA ciphertext length constant for cc20 KEY+IV CBOR encoding.");
|
|
|
|
Ok(())
|
|
}
|
|
}
|