use crate::curve::p256::{P256, P256r1PublicKey};
use crate::curve::{Curve, DhCurve};
use crate::noise::buffers::{RecvBuffer, SendBuffer};
use crate::noise::handshake::HandshakeInner;
use crate::noise::process::{
do_es_initiator, do_es_responder, recv_e, recv_payload, recv_to_transport, send_e, send_payload,
};
use crate::noise::role::{Initiator, Responder};
use crate::noise::{Blake2b, ChaChaPoly, HandshakeError, Noise, Transport, pattern};
use crate::provider::DhProviderAsync;
type N = Noise<pattern::N, P256, ChaChaPoly, Blake2b>;
pub(crate) const SEAL_PAYLOAD_SIZE: usize = 32;
pub(crate) const NOISE_N_MSG1_SIZE: usize = 81;
const SEALED_TRANSPORT_SIZE: usize = SEAL_PAYLOAD_SIZE + Transport::<N>::OVERHEAD;
pub(crate) const SEALED_SIZE: usize = NOISE_N_MSG1_SIZE + SEALED_TRANSPORT_SIZE;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub(crate) enum SealError {
#[error("Noise N seal failed: {0}")]
Seal(#[source] Box<HandshakeError>),
#[error("Noise N open failed: {0}")]
Open(#[source] Box<HandshakeError>),
}
pub(crate) async fn seal_32<P>(
provider: P,
recipient_pub: &P256r1PublicKey,
payload: &[u8; SEAL_PAYLOAD_SIZE],
) -> Result<[u8; SEALED_SIZE], SealError>
where
P: DhProviderAsync<P256>,
<P256 as DhCurve>::SharedSecret: AsRef<[u8]>,
<P256 as Curve>::PublicKey: AsRef<[u8]>,
{
let seal = |e: HandshakeError| SealError::Seal(Box::new(e));
let mut inner = HandshakeInner::new::<N>(provider, &[]);
inner.symmetric.mix_hash(recipient_pub.as_ref());
inner.rs = Some(*recipient_pub);
let mut msg1 = [0u8; NOISE_N_MSG1_SIZE];
{
let mut buffer = SendBuffer::new(&mut msg1);
send_e(&mut inner, &mut buffer).await.map_err(seal)?;
do_es_initiator(&mut inner).await.map_err(seal)?;
send_payload(&mut inner, &mut buffer).map_err(seal)?;
}
let mut transport = recv_to_transport::<N, Initiator, P>(inner);
let mut sealed_transport = [0u8; SEALED_TRANSPORT_SIZE];
let sealed_len = transport
.send(payload, &mut sealed_transport)
.map_err(seal)?;
let mut blob = [0u8; SEALED_SIZE];
blob[..NOISE_N_MSG1_SIZE].copy_from_slice(&msg1);
blob[NOISE_N_MSG1_SIZE..NOISE_N_MSG1_SIZE + sealed_len]
.copy_from_slice(&sealed_transport[..sealed_len]);
Ok(blob)
}
pub(crate) async fn open_32<P>(
provider: P,
recipient_key: P::PrivateKey,
sealed: &[u8; SEALED_SIZE],
) -> Result<[u8; SEAL_PAYLOAD_SIZE], SealError>
where
P: DhProviderAsync<P256>,
<P256 as DhCurve>::SharedSecret: AsRef<[u8]>,
<P256 as Curve>::PublicKey: AsRef<[u8]>,
{
let open = |e: HandshakeError| SealError::Open(Box::new(e));
let msg1 = &sealed[..NOISE_N_MSG1_SIZE];
let ciphertext = &sealed[NOISE_N_MSG1_SIZE..];
let mut inner = HandshakeInner::new::<N>(provider, &[]);
let s_pub = inner
.provider
.public_key(&recipient_key)
.map_err(|e| SealError::Open(Box::new(HandshakeError::Crypto(Box::new(e)))))?;
inner.symmetric.mix_hash(s_pub.as_ref());
inner.s_pub = Some(s_pub);
inner.s = Some(recipient_key);
let e_bytes = &msg1[..<P256 as Curve>::PUBLIC_KEY_SIZE];
let tag = &msg1[<P256 as Curve>::PUBLIC_KEY_SIZE..];
{
let mut e_buffer = RecvBuffer::new(e_bytes);
recv_e(&mut inner, &mut e_buffer).map_err(open)?;
}
do_es_responder(&mut inner).await.map_err(open)?;
{
let mut tag_buffer = RecvBuffer::new(tag);
recv_payload(&mut inner, &mut tag_buffer).map_err(open)?;
}
let mut transport = recv_to_transport::<N, Responder, P>(inner);
let mut payload = [0u8; SEAL_PAYLOAD_SIZE];
transport.receive(ciphertext, &mut payload).map_err(open)?;
Ok(payload)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::curve::p256::P256r1PrivateKey;
use crate::provider::EphemeralOnly;
use rand::{SeedableRng, rngs::StdRng};
#[tokio::test]
async fn seal_open_round_trip() {
let device_key = P256r1PrivateKey::generate(rand::rng()).unwrap();
let device_pub = device_key.public();
let payload: [u8; 32] = [0x42; 32];
let sealed = seal_32(
EphemeralOnly::new(StdRng::from_os_rng()),
&device_pub,
&payload,
)
.await
.unwrap();
assert_eq!(sealed.len(), SEALED_SIZE);
assert_eq!(
SEALED_SIZE, 129,
"Noise-N sealed envelope is 129 bytes (81-byte msg1 + 48-byte transport)",
);
assert!(
sealed.windows(32).all(|w| w != payload),
"sealed envelope must not contain cleartext payload",
);
let opened = open_32(
EphemeralOnly::new(StdRng::from_os_rng()),
device_key,
&sealed,
)
.await
.unwrap();
assert_eq!(opened, payload);
}
#[tokio::test]
async fn open_with_wrong_key_fails() {
let device_key = P256r1PrivateKey::generate(rand::rng()).unwrap();
let device_pub = device_key.public();
let payload: [u8; 32] = [0x99; 32];
let sealed = seal_32(
EphemeralOnly::new(StdRng::from_os_rng()),
&device_pub,
&payload,
)
.await
.unwrap();
let other_key = P256r1PrivateKey::generate(rand::rng()).unwrap();
let result = open_32(
EphemeralOnly::new(StdRng::from_os_rng()),
other_key,
&sealed,
)
.await;
assert!(
matches!(result, Err(SealError::Open(_))),
"opening with wrong key must fail",
);
}
#[tokio::test]
async fn fresh_ephemeral_per_seal() {
let device_key = P256r1PrivateKey::generate(rand::rng()).unwrap();
let device_pub = device_key.public();
let payload: [u8; 32] = [0xAB; 32];
let sealed_a = seal_32(
EphemeralOnly::new(StdRng::from_os_rng()),
&device_pub,
&payload,
)
.await
.unwrap();
let sealed_b = seal_32(
EphemeralOnly::new(StdRng::from_os_rng()),
&device_pub,
&payload,
)
.await
.unwrap();
assert_ne!(sealed_a, sealed_b);
}
#[tokio::test]
async fn seal_format_pin() {
let recipient_key = P256r1PrivateKey::from_bytes([0x07; 32]).unwrap();
let recipient_pub = recipient_key.public();
let payload: [u8; 32] = [0x11; 32];
let sealed = seal_32(
EphemeralOnly::new(StdRng::seed_from_u64(0xA5A5_5A5A_DEAD_BEEF)),
&recipient_pub,
&payload,
)
.await
.unwrap();
const EXPECTED: [u8; SEALED_SIZE] = SEAL_FORMAT_PIN;
assert_eq!(
sealed, EXPECTED,
"frozen 129-byte seal envelope changed — Keychain format would break",
);
let opened = open_32(
EphemeralOnly::new(StdRng::from_os_rng()),
recipient_key,
&sealed,
)
.await
.unwrap();
assert_eq!(opened, payload);
}
#[rustfmt::skip]
const SEAL_FORMAT_PIN: [u8; SEALED_SIZE] = [
0x04, 0x8c, 0x0b, 0x89, 0xb3, 0x75, 0x88, 0x5b, 0x4e, 0xc0, 0x9b, 0x36, 0x63, 0x6d, 0xc9, 0x28,
0x2a, 0x1d, 0x87, 0xe8, 0xa1, 0xea, 0x86, 0x7e, 0x05, 0xa0, 0x99, 0x98, 0x94, 0xd9, 0x2a, 0x48,
0x01, 0x74, 0x4c, 0xba, 0x76, 0x25, 0x6f, 0x56, 0xb1, 0x97, 0xf3, 0xcf, 0x93, 0x8e, 0xed, 0x44,
0x55, 0x13, 0x21, 0xee, 0x0c, 0x09, 0xeb, 0x22, 0xff, 0x2a, 0x5e, 0xba, 0x4b, 0x2d, 0xb5, 0x82,
0x37, 0x22, 0x12, 0xf2, 0x1e, 0xfe, 0xbe, 0x3e, 0xd7, 0xc0, 0xae, 0xa4, 0xac, 0x0c, 0xf4, 0xec,
0x9d, 0x23, 0x23, 0x5f, 0xc4, 0xc5, 0x7d, 0xd9, 0x81, 0xeb, 0xc6, 0xf0, 0xd5, 0x9d, 0x08, 0xb9,
0x8b, 0x8d, 0x50, 0xc3, 0x82, 0x86, 0xd1, 0x42, 0x05, 0x01, 0xc0, 0xa8, 0xe6, 0xd1, 0x41, 0xd7,
0x18, 0x10, 0x0e, 0x8b, 0xa1, 0xe6, 0x11, 0x1c, 0xee, 0xa0, 0xf7, 0x6c, 0x3a, 0x17, 0x44, 0xf0,
0xeb,
];
}