pub mod channel;
pub mod data;
pub mod establish;
pub mod handshake;
pub mod identify;
pub mod maintenance;
pub mod request;
pub mod resources;
pub mod table;
pub mod transported;
use crate::crypto::{
hkdf_sha256, sha256_chunks, token_open, token_open_in_place, token_seal, token_seal_chunks,
token_seal_in_place, BufferTooShort, Ed25519PublicKey, TokenKey, TokenOpenError,
TokenOpenStream, X25519PublicKey, X25519SharedSecret,
};
use crate::wire::{
DestinationHash, DestinationType, PacketType, WireAddress, WireContext, TRUNCATED_HASH_BYTE_LEN,
};
use zeroize::{Zeroize, ZeroizeOnDrop};
pub const LINK_KEY_LEN: usize = 64;
pub const MAX_LINK_MTU: usize = 524_288;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct LinkId([u8; TRUNCATED_HASH_BYTE_LEN]);
impl LinkId {
pub const fn new(bytes: [u8; TRUNCATED_HASH_BYTE_LEN]) -> Self {
Self(bytes)
}
pub fn derive(
destination: &DestinationHash,
initiator_encryption: &X25519PublicKey,
initiator_signing: &Ed25519PublicKey,
) -> Self {
const FLAGS_NIBBLE: u8 =
((DestinationType::Single as u8) << 2) | (PacketType::LinkRequest as u8);
let digest = sha256_chunks(&[
&[FLAGS_NIBBLE],
destination.as_bytes(),
&[WireContext::None.to_byte()],
&initiator_encryption.0,
&initiator_signing.0,
]);
let mut id = [0u8; TRUNCATED_HASH_BYTE_LEN];
id.copy_from_slice(&digest[..TRUNCATED_HASH_BYTE_LEN]);
Self(id)
}
pub const fn as_bytes(&self) -> &[u8; TRUNCATED_HASH_BYTE_LEN] {
&self.0
}
pub const fn from_address(address: WireAddress) -> Self {
Self(*address.as_bytes())
}
pub const fn to_address(&self) -> WireAddress {
WireAddress::new(self.0)
}
}
#[derive(Zeroize, ZeroizeOnDrop)]
pub struct LinkKey {
material: [u8; LINK_KEY_LEN],
}
impl LinkKey {
pub fn derive(link_id: &LinkId, shared: &X25519SharedSecret) -> Self {
Self {
material: hkdf_sha256::<LINK_KEY_LEN>(shared.as_bytes(), link_id.as_bytes(), &[]),
}
}
pub fn seal(
&self,
iv: &[u8; 16],
plaintext: &[u8],
out: &mut [u8],
) -> Result<usize, BufferTooShort> {
token_seal(&TokenKey::from_aes256(&self.material), iv, plaintext, out)
}
pub fn seal_chunks(
&self,
iv: &[u8; 16],
chunks: &[&[u8]],
out: &mut [u8],
) -> Result<usize, BufferTooShort> {
token_seal_chunks(&TokenKey::from_aes256(&self.material), iv, chunks, out)
}
pub fn cloned(&self) -> LinkKey {
LinkKey {
material: self.material,
}
}
pub fn seal_in_place(
&self,
iv: &[u8; 16],
out: &mut [u8],
plain_len: usize,
) -> Result<usize, BufferTooShort> {
token_seal_in_place(&TokenKey::from_aes256(&self.material), iv, out, plain_len)
}
pub fn open(&self, token: &[u8], out: &mut [u8]) -> Result<usize, TokenOpenError> {
token_open(&TokenKey::from_aes256(&self.material), token, out)
}
pub fn open_in_place<'t>(&self, token: &'t mut [u8]) -> Result<&'t [u8], TokenOpenError> {
token_open_in_place(&TokenKey::from_aes256(&self.material), token)
}
pub fn open_stream(
&self,
iv: &[u8; 16],
token_len: usize,
) -> Result<TokenOpenStream, TokenOpenError> {
TokenOpenStream::begin(&TokenKey::from_aes256(&self.material), iv, token_len)
}
}
impl core::fmt::Debug for LinkKey {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("LinkKey").finish_non_exhaustive()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LinkMode {
Aes256Cbc,
}
impl LinkMode {
const fn to_bits(self) -> u8 {
match self {
Self::Aes256Cbc => 0x01,
}
}
const fn from_bits(bits: u8) -> Option<Self> {
match bits {
0x01 => Some(Self::Aes256Cbc),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::crypto::{x25519_diffie_hellman, X25519SecretKey};
fn bytes_from_hex(s: &str) -> Vec<u8> {
(0..s.len())
.step_by(2)
.map(|i| u8::from_str_radix(&s[i..i + 2], 16).expect("valid hex"))
.collect()
}
fn a16(s: &str) -> [u8; 16] {
bytes_from_hex(s).try_into().expect("16 bytes")
}
fn a32(s: &str) -> [u8; 32] {
bytes_from_hex(s).try_into().expect("32 bytes")
}
fn a64(s: &str) -> [u8; 64] {
bytes_from_hex(s).try_into().expect("64 bytes")
}
const INITIATOR_SCALAR: &str =
"3333333333333333333333333333333333333333333333333333333333333333";
const RESPONDER_PUBLIC: &str =
"ff2ee45601ec1b67310c7790404585ae697331eee1c1f8cf2419731c1fff3e6b";
const SHARED_SECRET: &str = "3c528e9fd39731b15d10de8feb5f71d3f65b73c993581dedb03315a9ed177730";
const LINK_ID: &str = "000102030405060708090a0b0c0d0e0f";
const DERIVED_KEY: &str = "c44718017ed8c8dd932f6e3fc65c00edda249daeaaf006a6920ad02905b3d766\
40ea59958b62b1f452f00d2762ca217f45f2028886e79c8cf4e09eb18d37b83a";
const PLAINTEXT: &[u8] = b"link layer rides the same token!";
const CIPHER_IV: &str = "a1a2a3a4a5a6a7a8a9aaabacadaeafb0";
const LINK_TOKEN: &str = "a1a2a3a4a5a6a7a8a9aaabacadaeafb012a31f7217fde987fbb8bab1ef73d3b3\
b63557757d0c3adea6b0e94e9d27f23ba732763cc4ed566de7c915bafe3e5467\
99a834e0e6579c62ccb6da661641040a56430127964af6eafdae462cd79e8ff0";
fn derived_link_key() -> LinkKey {
let shared = x25519_diffie_hellman(
&X25519SecretKey::new(a32(INITIATOR_SCALAR)),
&X25519PublicKey(a32(RESPONDER_PUBLIC)),
);
assert_eq!(
shared.as_bytes(),
&a32(SHARED_SECRET),
"the ECDH leg must reproduce the reference shared secret",
);
LinkKey::derive(&LinkId::new(a16(LINK_ID)), &shared)
}
#[test]
fn derive_matches_the_reference_handshake() {
assert_eq!(derived_link_key().material, a64(DERIVED_KEY));
}
#[test]
fn the_link_cipher_seals_and_opens_against_the_reference_token() {
let key = derived_link_key();
let mut sealed = [0u8; 128];
let n = key.seal(&a16(CIPHER_IV), PLAINTEXT, &mut sealed).unwrap();
assert_eq!(
&sealed[..n],
&bytes_from_hex(LINK_TOKEN)[..],
"seal matches RNS Token"
);
let mut out = [0u8; 128];
let m = key.open(&bytes_from_hex(LINK_TOKEN), &mut out).unwrap();
assert_eq!(&out[..m], PLAINTEXT, "open recovers the plaintext");
let mut token = bytes_from_hex(LINK_TOKEN);
assert_eq!(key.open_in_place(&mut token).unwrap(), PLAINTEXT);
}
}