feat(qr-login): Support HPKE for the cryptographic channel

This commit is contained in:
Damir Jelić
2025-12-05 13:09:14 +01:00
parent 22e812655c
commit 146fc6f40d
2 changed files with 77 additions and 10 deletions

View File

@@ -31,6 +31,7 @@
use vodozemac::{
Curve25519PublicKey,
ecies::{CheckCode, Ecies, EstablishedEcies, InboundCreationResult, InitialMessage, Message},
hpke::{self, EstablishedHpkeChannel, HpkeRecipientChannel, RecipientCreationResult},
};
use crate::authentication::oauth::qrcode::SecureChannelError as Error;
@@ -38,6 +39,7 @@ use crate::authentication::oauth::qrcode::SecureChannelError as Error;
/// A cryptographic communication channel.
pub(super) enum CryptoChannel {
Ecies(Ecies),
Hpke(HpkeRecipientChannel),
}
impl CryptoChannel {
@@ -46,10 +48,16 @@ impl CryptoChannel {
CryptoChannel::Ecies(Ecies::new())
}
/// Create a new HPKE-based [`CryptoChannel`].
pub(super) fn new_hpke() -> Self {
CryptoChannel::Hpke(HpkeRecipientChannel::new())
}
/// Get the [`Curve25519PublicKey`] of this cryptographic channel.
pub(super) fn public_key(&self) -> Curve25519PublicKey {
match self {
CryptoChannel::Ecies(ecies) => ecies.public_key(),
CryptoChannel::Hpke(hpke) => hpke.public_key(),
}
}
@@ -63,12 +71,19 @@ impl CryptoChannel {
let message = InitialMessage::decode(message)?;
Ok(CryptoChannelCreationResult::Ecies(ecies.establish_inbound_channel(&message)?))
}
CryptoChannel::Hpke(hpke) => {
let message = hpke::InitialMessage::decode(message).unwrap();
Ok(CryptoChannelCreationResult::Hpke(
hpke.establish_channel(&message, &[]).unwrap(),
))
}
}
}
}
pub(super) enum CryptoChannelCreationResult {
Ecies(InboundCreationResult),
Hpke(RecipientCreationResult),
}
impl CryptoChannelCreationResult {
@@ -78,6 +93,7 @@ impl CryptoChannelCreationResult {
CryptoChannelCreationResult::Ecies(inbound_creation_result) => {
&inbound_creation_result.message
}
CryptoChannelCreationResult::Hpke(result) => &result.message,
}
}
}
@@ -88,6 +104,7 @@ impl CryptoChannelCreationResult {
/// cryptographic messages.
pub(super) enum EstablishedCryptoChannel {
Ecies(EstablishedEcies),
Hpke(EstablishedHpkeChannel),
}
impl EstablishedCryptoChannel {
@@ -95,26 +112,37 @@ impl EstablishedCryptoChannel {
pub(super) fn check_code(&self) -> &CheckCode {
match self {
EstablishedCryptoChannel::Ecies(established_ecies) => established_ecies.check_code(),
EstablishedCryptoChannel::Hpke(established_hpke_channel) => {
established_hpke_channel.check_code()
}
}
}
/// Seal the given plaintext using this [`EstablishedCryptoChannel`].
pub(super) fn seal(&mut self, plaintext: &str) -> String {
pub(super) fn seal(&mut self, plaintext: &str, aad: &[u8]) -> String {
match self {
EstablishedCryptoChannel::Ecies(channel) => {
let message = channel.encrypt(plaintext.as_bytes());
message.encode()
}
EstablishedCryptoChannel::Hpke(channel) => {
let message = channel.seal(plaintext.as_bytes(), aad);
message.encode()
}
}
}
/// Open the given sealed message using this [`EstablishedCryptoChannel`].
pub(super) fn open(&mut self, message: &str) -> Result<String, Error> {
pub(super) fn open(&mut self, message: &str, aad: &[u8]) -> Result<String, Error> {
let plaintext = match self {
EstablishedCryptoChannel::Ecies(channel) => {
let message = Message::decode(message)?;
channel.decrypt(&message)?
}
EstablishedCryptoChannel::Hpke(channel) => {
let message = hpke::Message::decode(message).unwrap();
channel.open(&message, aad).unwrap()
}
};
Ok(String::from_utf8(plaintext).map_err(|e| e.utf8_error())?)

View File

@@ -19,8 +19,12 @@ use matrix_sdk_base::crypto::types::qr_login::{
use serde::{Serialize, de::DeserializeOwned};
use tracing::{instrument, trace};
use url::Url;
use vodozemac::ecies::{
CheckCode, DigitMode, Ecies, EstablishedEcies, InboundCreationResult, OutboundCreationResult,
use vodozemac::{
ecies::{CheckCode, Ecies, EstablishedEcies, InboundCreationResult, OutboundCreationResult},
hpke::{
BidirectionalCreationResult, DigitMode, HpkeSenderChannel, InitialResponse,
RecipientCreationResult, SenderCreationResult, UnidirectionalSenderChannel,
},
};
use super::{
@@ -116,6 +120,15 @@ impl SecureChannel {
secure_channel.send(LOGIN_OK_MESSAGE).await?;
secure_channel
}
CryptoChannelCreationResult::Hpke(RecipientCreationResult { channel, .. }) => {
let BidirectionalCreationResult { channel, message } =
channel.establish_bidirectional_channel(LOGIN_OK_MESSAGE.as_bytes(), &[]);
self.channel.send(message.encode()).await?;
let crypto_channel = EstablishedCryptoChannel::Hpke(channel);
EstablishedSecureChannel { channel: self.channel, crypto_channel }
}
};
Ok(AlmostEstablishedSecureChannel { secure_channel })
@@ -163,6 +176,7 @@ impl EstablishedSecureChannel {
) -> Result<Self, Error> {
enum ChannelType {
Ecies(EstablishedEcies),
Hpke(UnidirectionalSenderChannel),
}
if qr_code_data.intent() == expected_mode {
@@ -176,7 +190,7 @@ impl EstablishedSecureChannel {
// it's talking to us, the device that scanned the QR code, until it
// receives and successfully decrypts the initial message. We're here encrypting
// the `LOGIN_INITIATE_MESSAGE`.
let (crypto_channel, encoded_message) = {
let (crypto_channel, encoded_message) = if true {
let ecies = Ecies::new();
let OutboundCreationResult { ecies, message } = ecies.establish_outbound_channel(
@@ -184,6 +198,15 @@ impl EstablishedSecureChannel {
LOGIN_INITIATE_MESSAGE.as_bytes(),
)?;
(ChannelType::Ecies(ecies), message.encode())
} else {
let SenderCreationResult { channel, message } = HpkeSenderChannel::new()
.establish_channel(
qr_code_data.public_key(),
LOGIN_INITIATE_MESSAGE.as_bytes(),
// TODO: Do we want to include some additional authenticated data here?
&[],
);
(ChannelType::Hpke(channel), message.encode())
};
// The other side has crated a rendezvous channel, we're going to connect to it
@@ -213,15 +236,32 @@ impl EstablishedSecureChannel {
trace!("Waiting for the LOGIN OK message");
let (response, channel) = match crypto_channel {
ChannelType::Ecies(ecies) => {
ChannelType::Ecies(crypto_channel) => {
// We can create our EstablishedSecureChannel struct now and use the
// convenient helpers which transparently decrypt on receival.
let crypto_channel = EstablishedCryptoChannel::Ecies(ecies);
let crypto_channel = EstablishedCryptoChannel::Ecies(crypto_channel);
let mut channel = Self { channel, crypto_channel };
let response = channel.receive().await?;
(response, channel)
}
ChannelType::Hpke(crypto_channel) => {
let response = channel.receive().await?;
let response = InitialResponse::decode(&response).unwrap();
let BidirectionalCreationResult { channel: crypto_channel, message } =
crypto_channel.establish_bidirectional_channel(&response, &[]).unwrap();
let response = String::from_utf8(message).unwrap();
let crypto_channel = EstablishedCryptoChannel::Hpke(crypto_channel);
// We can create our EstablishedSecureChannel struct now and use the
// convenient helpers which transparently decrypt on receival.
let channel = Self { channel, crypto_channel };
// We can create our EstablishedSecureChannel struct now and use the
// convenient helpers which transparently decrypt on receival.
(response, channel)
}
};
trace!("Received the LOGIN OK message, maybe.");
@@ -260,14 +300,13 @@ impl EstablishedSecureChannel {
}
async fn send(&mut self, message: &str) -> Result<(), Error> {
let message = self.crypto_channel.seal(message);
let message = self.crypto_channel.seal(message, &[]);
Ok(self.channel.send(message).await?)
}
async fn receive(&mut self) -> Result<String, Error> {
let message = self.channel.receive().await?;
self.crypto_channel.open(&message)
self.crypto_channel.open(&message, &[])
}
}