use core::{cell::OnceCell, iter};
use buggy::BugExt as _;
use derive_where::derive_where;
use spideroak_crypto::{csprng::Random as _, import::ImportError, kem::Kem};
use zerocopy::{ByteEq, Immutable, IntoBytes, KnownLayout, Unaligned};
use crate::{
CmdId,
afc::{
keys::{OpenKey, SealKey, Seq},
shared::{RawOpenKey, RawSealKey, RootChannelKey},
},
aranya::{DeviceId, Encap, EncryptionKey, EncryptionPublicKey},
ciphersuite::CipherSuite,
engine::{Engine, unwrapped},
error::Error,
hpke::{self, Mode},
id::{IdError, IdExt as _, custom_id},
misc::sk_misc,
policy::LabelId,
};
pub struct UniChannel<'a, CS: CipherSuite> {
pub parent_cmd_id: CmdId,
pub our_sk: &'a EncryptionKey<CS>,
pub their_pk: &'a EncryptionPublicKey<CS>,
pub seal_id: DeviceId,
pub open_id: DeviceId,
pub label_id: LabelId,
}
impl<CS: CipherSuite> UniChannel<'_, CS> {
pub(crate) const fn info(&self) -> Info {
Info {
domain: *b"AfcUniKey-v1",
parent_cmd_id: self.parent_cmd_id,
seal_id: self.seal_id,
open_id: self.open_id,
label_id: self.label_id,
}
}
}
#[repr(C)]
#[derive(Copy, Clone, Debug, ByteEq, Immutable, IntoBytes, KnownLayout, Unaligned)]
pub(crate) struct Info {
domain: [u8; 12],
parent_cmd_id: CmdId,
seal_id: DeviceId,
open_id: DeviceId,
label_id: LabelId,
}
pub struct UniAuthorSecret<CS: CipherSuite> {
sk: RootChannelKey<CS>,
id: OnceCell<Result<UniAuthorSecretId, IdError>>,
}
sk_misc!(UniAuthorSecret, UniAuthorSecretId, "AFC Uni Author Secret");
unwrapped! {
name: UniAuthorSecret;
type: Decap;
into: |key: Self| { key.sk.into_inner() };
from: |key| { Self { sk: RootChannelKey::new(key), id: OnceCell::new() } };
}
#[derive_where(Serialize, Deserialize)]
#[serde(transparent)]
pub struct UniPeerEncap<CS: CipherSuite> {
encap: Encap<CS>,
#[serde(skip)]
id: OnceCell<UniChannelId>,
}
impl<CS: CipherSuite> UniPeerEncap<CS> {
#[inline]
pub fn id(&self) -> UniChannelId {
*self.id.get_or_init(|| {
UniChannelId::new::<CS>(b"UniChannelId-v1", iter::once(self.as_bytes()))
})
}
#[inline]
pub fn as_bytes(&self) -> &[u8] {
self.encap.as_bytes()
}
#[inline]
pub fn from_bytes(data: &[u8]) -> Result<Self, ImportError> {
Ok(Self {
encap: Encap::from_bytes(data)?,
id: OnceCell::new(),
})
}
fn as_inner(&self) -> &<CS::Kem as Kem>::Encap {
self.encap.as_inner()
}
}
custom_id! {
pub struct UniChannelId;
}
pub struct UniSecrets<CS: CipherSuite> {
pub author: UniAuthorSecret<CS>,
pub peer: UniPeerEncap<CS>,
}
impl<CS: CipherSuite> UniSecrets<CS> {
pub fn new<E: Engine<CS = CS>>(eng: &E, ch: &UniChannel<'_, CS>) -> Result<Self, Error> {
let author_sk = ch.our_sk;
let peer_pk = ch.their_pk;
if ch.seal_id == ch.open_id {
return Err(Error::same_device_id());
}
let root_sk = RootChannelKey::random(eng);
let peer = {
let (enc, _) = hpke::setup_send_deterministically::<CS>(
Mode::Auth(&author_sk.sk),
&peer_pk.pk,
[ch.info().as_bytes()],
root_sk.clone().into_inner(),
)?;
UniPeerEncap {
encap: Encap(enc),
id: OnceCell::new(),
}
};
let author = UniAuthorSecret {
sk: root_sk,
id: OnceCell::new(),
};
Ok(Self { author, peer })
}
#[inline]
pub fn id(&self) -> UniChannelId {
self.peer.id()
}
}
macro_rules! uni_key {
($name:ident, $inner:ident, $doc:expr $(,)?) => {
#[doc = $doc]
pub struct $name<CS: CipherSuite>($inner<CS>);
impl<CS: CipherSuite> $name<CS> {
pub fn from_author_secret(
ch: &UniChannel<'_, CS>,
secret: UniAuthorSecret<CS>,
) -> Result<Self, Error> {
let author_sk = ch.our_sk;
let peer_pk = ch.their_pk;
if ch.seal_id == ch.open_id {
return Err(Error::same_device_id());
}
let (_, ctx) = hpke::setup_send_deterministically::<CS>(
Mode::Auth(&author_sk.sk),
&peer_pk.pk,
[ch.info().as_bytes()],
secret.sk.into_inner(),
)?;
let key = {
let (key, base_nonce) = ctx
.into_raw_parts()
.assume("`SendCtx` should still contain the raw key")?;
$inner { key, base_nonce }
};
Ok(Self(key))
}
pub fn from_peer_encap(
ch: &UniChannel<'_, CS>,
enc: UniPeerEncap<CS>,
) -> Result<Self, Error> {
let peer_sk = ch.our_sk;
let author_pk = ch.their_pk;
if ch.seal_id == ch.open_id {
return Err(Error::same_device_id());
}
let ctx = hpke::setup_recv::<CS>(
Mode::Auth(&author_pk.pk),
enc.as_inner(),
&peer_sk.sk,
[ch.info().as_bytes()],
)?;
let key = {
let (key, base_nonce) = ctx
.into_raw_parts()
.assume("`RecvCtx` should still contain the raw key")?;
$inner { key, base_nonce }
};
Ok(Self(key))
}
pub fn into_raw_key(self) -> $inner<CS> {
self.0
}
#[cfg(any(test, feature = "test_util"))]
pub(crate) fn as_raw_key(&self) -> &$inner<CS> {
&self.0
}
}
};
}
uni_key!(
UniSealKey,
RawSealKey,
"A unidirectional channel encryption key.",
);
impl<CS: CipherSuite> UniSealKey<CS> {
pub fn into_key(self) -> Result<SealKey<CS>, Error> {
let seal = SealKey::from_raw(&self.0, Seq::ZERO)?;
Ok(seal)
}
}
uni_key!(
UniOpenKey,
RawOpenKey,
"A unidirectional channel decryption key.",
);
impl<CS: CipherSuite> UniOpenKey<CS> {
pub fn into_key(self) -> Result<OpenKey<CS>, Error> {
let open = OpenKey::from_raw(&self.0)?;
Ok(open)
}
}
#[cfg(test)]
mod tests {
use spideroak_crypto::{ed25519::Ed25519, import::Import as _, kem::Kem, rust};
use super::*;
use crate::{afc::shared::RootChannelKey, default::DhKemP256HkdfSha256, test_util::TestCs};
type CS = TestCs<
rust::Aes256Gcm,
rust::Sha256,
rust::HkdfSha512,
DhKemP256HkdfSha256,
rust::HmacSha512,
Ed25519,
>;
#[test]
fn test_uni_author_secret_id() {
let tests = [(
[
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c,
0x1d, 0x1e, 0x1f, 0x20,
],
"8QFfLfKymtXHa9MJWhJcKvwYWXtsmuCK3Bsf2tCxpdK1",
)];
for (i, (key_bytes, expected_id)) in tests.iter().enumerate() {
let sk = <<CS as CipherSuite>::Kem as Kem>::DecapKey::import(key_bytes)
.expect("should import decap key");
let root_key = RootChannelKey::<CS>::new(sk);
let uni_author_secret = UniAuthorSecret {
sk: root_key,
id: OnceCell::new(),
};
let got_id = uni_author_secret.id().expect("should compute ID");
let expected =
UniAuthorSecretId::decode(expected_id).expect("should decode expected ID");
assert_eq!(got_id, expected, "test case #{i}");
}
}
}