use chacha20::{
cipher::{KeyIvInit as _, StreamCipher},
ChaCha20,
};
use hmac::{Hmac, Mac as _};
use secp256k1::{
constants, ecdh::SharedSecret, PublicKey, Scalar, Secp256k1, SecretKey, Signing, Verification,
};
use sha2::{Digest as _, Sha256};
use thiserror::Error;
const HMAC_KEY_RHO: &[u8] = b"rho";
const HMAC_KEY_MU: &[u8] = b"mu";
const HMAC_KEY_PAD: &[u8] = b"pad";
const HMAC_KEY_UM: &[u8] = b"um";
const HMAC_KEY_AMMAG: &[u8] = b"ammag";
const CHACHA_NONCE: [u8; 12] = [0u8; 12];
const PACKET_VERSION_LEN: usize = 1;
const PACKET_PUBLIC_KEY_LEN: usize = 33;
const PACKET_HMAC_LEN: usize = 32;
const MIN_ONION_PACKET_LEN: usize = PACKET_VERSION_LEN + PACKET_PUBLIC_KEY_LEN + PACKET_HMAC_LEN;
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct OnionPacket {
pub version: u8,
pub public_key: PublicKey,
pub packet_data: Vec<u8>,
pub hmac: [u8; PACKET_HMAC_LEN],
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct OnionErrorPacket {
pub packet_data: Vec<u8>,
}
impl OnionPacket {
pub fn create<C: Signing>(
session_key: SecretKey,
hops_path: Vec<PublicKey>,
hops_data: Vec<Vec<u8>>,
assoc_data: Option<Vec<u8>>,
packet_data_len: usize,
secp_ctx: &Secp256k1<C>,
) -> Result<OnionPacket, SphinxError> {
if hops_path.len() != hops_data.len() {
return Err(SphinxError::HopsLenMismatch);
}
if hops_path.is_empty() {
return Err(SphinxError::HopsIsEmpty);
}
let hops_keys = derive_hops_forward_keys(&hops_path, session_key, secp_ctx)?;
let pad_key = derive_key(HMAC_KEY_PAD, &session_key.secret_bytes());
let packet_data = generate_padding_data(packet_data_len, &pad_key);
let filler = generate_filler(packet_data_len, &hops_keys, &hops_data)?;
construct_onion_packet(
packet_data,
session_key.public_key(secp_ctx),
&hops_keys,
&hops_data,
assoc_data,
filler,
)
}
pub fn into_bytes(self) -> Vec<u8> {
let mut bytes = Vec::with_capacity(MIN_ONION_PACKET_LEN + self.packet_data.len());
bytes.push(self.version);
bytes.extend_from_slice(&self.public_key.serialize());
bytes.extend_from_slice(&self.packet_data);
bytes.extend_from_slice(&self.hmac);
bytes
}
pub fn from_bytes_with_packet_data_len(
bytes: Vec<u8>,
packet_data_len: usize,
) -> Result<Self, SphinxError> {
let expected_len = MIN_ONION_PACKET_LEN
.checked_add(packet_data_len)
.ok_or(SphinxError::PacketDataLenMismatch)?;
if bytes.len() != expected_len {
return Err(SphinxError::PacketDataLenMismatch);
}
let version = bytes[0];
let public_key = PublicKey::from_slice(
&bytes[PACKET_VERSION_LEN..PACKET_VERSION_LEN + PACKET_PUBLIC_KEY_LEN],
)
.map_err(|_| SphinxError::PublicKeyInvalid)?;
let packet_data_start = PACKET_VERSION_LEN + PACKET_PUBLIC_KEY_LEN;
let packet_data_end = packet_data_start + packet_data_len;
let packet_data = bytes[packet_data_start..packet_data_end].to_vec();
let mut hmac = [0u8; PACKET_HMAC_LEN];
hmac.copy_from_slice(&bytes[packet_data_end..]);
Ok(Self {
version,
public_key,
packet_data,
hmac,
})
}
#[deprecated(
since = "2.4.0",
note = "use OnionPacket::from_bytes_with_packet_data_len to verify the packet data length"
)]
pub fn from_bytes(bytes: Vec<u8>) -> Result<Self, SphinxError> {
if bytes.len() < MIN_ONION_PACKET_LEN {
return Err(SphinxError::PacketDataLenTooSmall);
}
let packet_data_len = bytes.len() - MIN_ONION_PACKET_LEN;
Self::from_bytes_with_packet_data_len(bytes, packet_data_len)
}
pub fn extract_public_key_from_slice(bytes: &[u8]) -> Result<PublicKey, SphinxError> {
if bytes.len() < MIN_ONION_PACKET_LEN {
return Err(SphinxError::PacketDataLenTooSmall);
}
PublicKey::from_slice(
&bytes[PACKET_VERSION_LEN..PACKET_VERSION_LEN + PACKET_PUBLIC_KEY_LEN],
)
.map_err(|_| SphinxError::PublicKeyInvalid)
}
pub fn shared_secret(&self, secret_key: &SecretKey) -> [u8; 32] {
SharedSecret::new(&self.public_key, secret_key).secret_bytes()
}
pub fn peel<C, F>(
self,
secret_key: &SecretKey,
assoc_data: Option<&[u8]>,
secp_ctx: &Secp256k1<C>,
get_hop_data_len: F,
) -> Result<(Vec<u8>, Self), SphinxError>
where
C: Verification,
F: FnOnce(&[u8]) -> Option<usize>,
{
let packet_data_len = self.packet_data.len();
let shared_secret = self.shared_secret(secret_key);
let rho = derive_key(HMAC_KEY_RHO, shared_secret.as_ref());
let mu = derive_key(HMAC_KEY_MU, shared_secret.as_ref());
if !verify_hmac(&mu, &self.packet_data, assoc_data, &self.hmac) {
return Err(SphinxError::HmacMismatch);
}
let mut chacha = ChaCha20::new(&rho.into(), &CHACHA_NONCE.into());
let mut packet_data = self.packet_data;
chacha.apply_keystream(&mut packet_data[..]);
let data_len = get_hop_data_len(&packet_data).ok_or(SphinxError::HopDataLenUnavailable)?;
let hmac_end = data_len
.checked_add(PACKET_HMAC_LEN)
.ok_or(SphinxError::HopDataLenTooLarge)?;
if hmac_end > packet_data_len {
return Err(SphinxError::HopDataLenTooLarge);
}
let hop_data = packet_data[0..data_len].to_vec();
let mut hmac = [0; PACKET_HMAC_LEN];
hmac.copy_from_slice(&packet_data[data_len..hmac_end]);
shift_slice_left(&mut packet_data[..], hmac_end);
chacha.apply_keystream(&mut packet_data[(packet_data_len - hmac_end)..]);
let public_key = derive_next_hop_ephemeral_public_key(
self.public_key,
shared_secret.as_ref(),
secp_ctx,
)?;
Ok((
hop_data,
OnionPacket {
version: self.version,
public_key,
packet_data,
hmac,
},
))
}
}
impl OnionErrorPacket {
pub fn create(shared_secret: &[u8; 32], payload: Vec<u8>) -> Self {
let ReturnKeys { ammag, um } = ReturnKeys::new(shared_secret);
let hmac = compute_hmac(&um, &payload, None);
Self::concat(hmac, payload).xor_cipher_stream_with_ammag(ammag)
}
pub fn concat(hmac: [u8; PACKET_HMAC_LEN], mut payload: Vec<u8>) -> Self {
let mut packet_data = hmac.to_vec();
packet_data.append(&mut payload);
OnionErrorPacket { packet_data }
}
fn xor_cipher_stream_with_ammag(self, ammag: [u8; 32]) -> Self {
let mut chacha = ChaCha20::new(&ammag.into(), &CHACHA_NONCE.into());
let mut packet_data = self.packet_data;
chacha.apply_keystream(&mut packet_data[..]);
Self { packet_data }
}
pub fn xor_cipher_stream(self, shared_secret: &[u8; 32]) -> Self {
let ammag = derive_ammag_key(shared_secret);
self.xor_cipher_stream_with_ammag(ammag)
}
pub fn parse<F, T>(
self,
hops_path: Vec<PublicKey>,
session_key: SecretKey,
parse_payload: F,
) -> Option<(T, usize)>
where
F: Fn(&[u8]) -> Option<T>,
{
if self.packet_data.len() < PACKET_HMAC_LEN {
return None;
}
let secp_ctx = Secp256k1::new();
let mut packet = self;
for (index, shared_secret) in
OnionSharedSecretIter::new(hops_path.iter(), session_key, &secp_ctx).enumerate()
{
let shared_secret = shared_secret.ok()?;
let ReturnKeys { ammag, um } = ReturnKeys::new(&shared_secret);
packet = packet.xor_cipher_stream_with_ammag(ammag);
let payload = &packet.packet_data[PACKET_HMAC_LEN..];
if verify_hmac(&um, payload, None, &packet.packet_data[..PACKET_HMAC_LEN]) {
if let Some(error) = parse_payload(payload) {
return Some((error, index));
}
}
}
None
}
pub fn split(self) -> ([u8; PACKET_HMAC_LEN], Vec<u8>) {
let mut hmac = [0u8; PACKET_HMAC_LEN];
if self.packet_data.len() >= PACKET_HMAC_LEN {
hmac.copy_from_slice(&self.packet_data[..PACKET_HMAC_LEN]);
let payload = self.packet_data[PACKET_HMAC_LEN..].to_vec();
(hmac, payload)
} else {
hmac.copy_from_slice(&self.packet_data[..]);
(hmac, Vec::new())
}
}
pub fn into_bytes(self) -> Vec<u8> {
self.packet_data
}
pub fn from_bytes(bytes: Vec<u8>) -> Self {
Self { packet_data: bytes }
}
}
#[derive(Error, Debug, Eq, PartialEq)]
pub enum SphinxError {
#[error("The hops path does not match the hops data length")]
HopsLenMismatch,
#[error("The hops path is empty")]
HopsIsEmpty,
#[error("The HMAC does not match the packet data and optional assoc data")]
HmacMismatch,
#[error("Unable to parse the data len for the current hop")]
HopDataLenUnavailable,
#[error("The parsed data len is larger than the onion packet len")]
HopDataLenTooLarge,
#[error("The parsed data len is too small")]
PacketDataLenTooSmall,
#[error("The packet data length does not match the bytes length")]
PacketDataLenMismatch,
#[error("Invalid public key")]
PublicKeyInvalid,
#[error("Invalid blinding factor")]
InvalidBlindingFactor,
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct ForwardKeys {
pub rho: [u8; 32],
pub mu: [u8; 32],
}
impl ForwardKeys {
pub fn new(shared_secret: &[u8]) -> ForwardKeys {
ForwardKeys {
rho: derive_key(HMAC_KEY_RHO, shared_secret),
mu: derive_key(HMAC_KEY_MU, shared_secret),
}
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct ReturnKeys {
pub ammag: [u8; 32],
pub um: [u8; 32],
}
impl ReturnKeys {
pub fn new(shared_secret: &[u8]) -> ReturnKeys {
ReturnKeys {
ammag: derive_ammag_key(shared_secret),
um: derive_key(HMAC_KEY_UM, shared_secret),
}
}
}
#[inline]
pub fn derive_ammag_key(shared_secret: &[u8]) -> [u8; 32] {
derive_key(HMAC_KEY_AMMAG, shared_secret)
}
#[derive(Clone)]
pub struct OnionSharedSecretIter<'s, I, C: Signing> {
hops_path_iter: I,
ephemeral_secret_key: SecretKey,
secp_ctx: &'s Secp256k1<C>,
}
impl<'s, I, C: Signing> OnionSharedSecretIter<'s, I, C> {
pub fn new(hops_path_iter: I, session_key: SecretKey, secp_ctx: &'s Secp256k1<C>) -> Self {
OnionSharedSecretIter {
hops_path_iter,
secp_ctx,
ephemeral_secret_key: session_key,
}
}
}
impl<'s, 'i, I: Iterator<Item = &'i PublicKey>, C: Signing> Iterator
for OnionSharedSecretIter<'s, I, C>
{
type Item = Result<[u8; 32], SphinxError>;
fn next(&mut self) -> Option<Self::Item> {
self.hops_path_iter.next().map(|pk| {
let shared_secret = SharedSecret::new(pk, &self.ephemeral_secret_key);
let ephemeral_public_key = self.ephemeral_secret_key.public_key(self.secp_ctx);
self.ephemeral_secret_key = derive_next_hop_ephemeral_secret_key(
self.ephemeral_secret_key,
&ephemeral_public_key,
shared_secret.as_ref(),
)?;
Ok(shared_secret.secret_bytes())
})
}
}
fn derive_hops_forward_keys<C: Signing>(
hops_path: &[PublicKey],
session_key: SecretKey,
secp_ctx: &Secp256k1<C>,
) -> Result<Vec<ForwardKeys>, SphinxError> {
OnionSharedSecretIter::new(hops_path.iter(), session_key, secp_ctx)
.map(|shared_secret| shared_secret.map(|shared_secret| ForwardKeys::new(&shared_secret)))
.collect()
}
#[inline]
fn shift_slice_right(arr: &mut [u8], amt: usize) {
for i in (amt..arr.len()).rev() {
arr[i] = arr[i - amt];
}
for item in arr.iter_mut().take(amt) {
*item = 0;
}
}
#[inline]
fn shift_slice_left(arr: &mut [u8], amt: usize) {
let pivot = arr.len() - amt;
for i in 0..pivot {
arr[i] = arr[i + amt];
}
for item in arr.iter_mut().skip(pivot) {
*item = 0;
}
}
fn compute_hmac(
hmac_key: &[u8; 32],
packet_data: &[u8],
assoc_data: Option<&[u8]>,
) -> [u8; PACKET_HMAC_LEN] {
let mut hmac_engine = Hmac::<Sha256>::new_from_slice(hmac_key).expect("valid hmac key");
hmac_engine.update(packet_data);
if let Some(assoc_data) = assoc_data {
hmac_engine.update(assoc_data);
}
hmac_engine.finalize().into_bytes().into()
}
fn verify_hmac(
hmac_key: &[u8; 32],
packet_data: &[u8],
assoc_data: Option<&[u8]>,
expected_hmac: &[u8],
) -> bool {
let mut hmac_engine = Hmac::<Sha256>::new_from_slice(hmac_key).expect("valid hmac key");
hmac_engine.update(packet_data);
if let Some(assoc_data) = assoc_data {
hmac_engine.update(assoc_data);
}
hmac_engine.verify_slice(expected_hmac).is_ok()
}
fn forward_stream_cipher<S: StreamCipher>(stream: &mut S, n: usize) {
for _ in 0..n {
let mut dummy = [0; 1];
stream.apply_keystream(&mut dummy);
}
}
fn derive_next_hop_ephemeral_secret_key(
ephemeral_secret_key: SecretKey,
ephemeral_public_key: &PublicKey,
shared_secret: &[u8],
) -> Result<SecretKey, SphinxError> {
let blinding_factor = {
let mut sha = Sha256::new();
sha.update(&ephemeral_public_key.serialize()[..]);
sha.update(shared_secret);
scalar_from_blinding_factor(sha.finalize().into())?
};
ephemeral_secret_key
.mul_tweak(&blinding_factor)
.map_err(|_| SphinxError::InvalidBlindingFactor)
}
fn derive_next_hop_ephemeral_public_key<C: Verification>(
ephemeral_public_key: PublicKey,
shared_secret: &[u8],
secp_ctx: &Secp256k1<C>,
) -> Result<PublicKey, SphinxError> {
let blinding_factor = {
let mut sha = Sha256::new();
sha.update(&ephemeral_public_key.serialize()[..]);
sha.update(shared_secret.as_ref());
scalar_from_blinding_factor(sha.finalize().into())?
};
ephemeral_public_key
.mul_tweak(secp_ctx, &blinding_factor)
.map_err(|_| SphinxError::InvalidBlindingFactor)
}
fn scalar_from_blinding_factor(mut blinding_factor: [u8; 32]) -> Result<Scalar, SphinxError> {
if blinding_factor >= constants::CURVE_ORDER {
subtract_secp256k1_order(&mut blinding_factor);
}
if blinding_factor == constants::ZERO {
return Err(SphinxError::InvalidBlindingFactor);
}
Scalar::from_be_bytes(blinding_factor).map_err(|_| SphinxError::InvalidBlindingFactor)
}
fn subtract_secp256k1_order(value: &mut [u8; 32]) {
let mut borrow = 0u16;
for (byte, order_byte) in value.iter_mut().zip(constants::CURVE_ORDER.iter()).rev() {
let subtrahend = *order_byte as u16 + borrow;
let minuend = *byte as u16;
if minuend >= subtrahend {
*byte = (minuend - subtrahend) as u8;
borrow = 0;
} else {
*byte = (minuend + 256 - subtrahend) as u8;
borrow = 1;
}
}
}
fn derive_key(hmac_key: &[u8], shared_secret: &[u8]) -> [u8; 32] {
let mut mac = Hmac::<Sha256>::new_from_slice(hmac_key).expect("valid hmac key");
mac.update(shared_secret);
mac.finalize().into_bytes().into()
}
fn generate_padding_data(packet_data_len: usize, pad_key: &[u8]) -> Vec<u8> {
let mut cipher = ChaCha20::new(pad_key.into(), &CHACHA_NONCE.into());
let mut buffer = vec![0u8; packet_data_len];
cipher.apply_keystream(&mut buffer);
buffer
}
fn generate_filler(
packet_data_len: usize,
hops_keys: &[ForwardKeys],
hops_data: &[Vec<u8>],
) -> Result<Vec<u8>, SphinxError> {
let mut filler = Vec::new();
let mut pos = 0;
for (i, (data, keys)) in hops_data.iter().zip(hops_keys.iter()).enumerate() {
let mut chacha = ChaCha20::new(&keys.rho.into(), &[0u8; 12].into());
forward_stream_cipher(&mut chacha, packet_data_len - pos);
pos += data.len() + PACKET_HMAC_LEN;
if pos > packet_data_len {
return Err(SphinxError::HopDataLenTooLarge);
}
if i == hops_data.len() - 1 {
break;
}
filler.resize(pos, 0u8);
chacha.apply_keystream(&mut filler);
}
Ok(filler)
}
fn construct_onion_packet(
mut packet_data: Vec<u8>,
public_key: PublicKey,
hops_keys: &[ForwardKeys],
hops_data: &[Vec<u8>],
assoc_data: Option<Vec<u8>>,
filler: Vec<u8>,
) -> Result<OnionPacket, SphinxError> {
let mut hmac = [0; PACKET_HMAC_LEN];
for (i, (data, keys)) in hops_data.iter().zip(hops_keys.iter()).rev().enumerate() {
let data_len = data.len();
shift_slice_right(&mut packet_data, data_len + PACKET_HMAC_LEN);
packet_data[0..data_len].copy_from_slice(data);
packet_data[data_len..(data_len + PACKET_HMAC_LEN)].copy_from_slice(&hmac);
let mut chacha = ChaCha20::new(&keys.rho.into(), &[0u8; 12].into());
chacha.apply_keystream(&mut packet_data);
if i == 0 {
let stop_index = packet_data.len();
let start_index = stop_index
.checked_sub(filler.len())
.ok_or(SphinxError::HopDataLenTooLarge)?;
packet_data[start_index..stop_index].copy_from_slice(&filler[..]);
}
hmac = compute_hmac(&keys.mu, &packet_data, assoc_data.as_deref());
}
Ok(OnionPacket {
version: 0,
public_key,
packet_data,
hmac,
})
}
#[cfg(test)]
mod tests;