Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .deny.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,14 @@ exceptions = [

[bans]
multiple-versions = "warn"
wildcards = "deny"
wildcards = "allow"

[sources]
unknown-registry = "deny"
unknown-git = "deny"

allow-git = [
"https://github.com/rozbb/rust-hpke/",
"https://github.com/poljar/olm-rs",
"https://github.com/poljar/olm-sys",
]
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ ed25519-dalek = { version = "3.0.0-pre.6", default-features = false, features =
getrandom = "0.4.2"
hkdf = "0.13.0"
hmac = "0.13.0"
hpke = { git = "https://github.com/rozbb/rust-hpke/", rev = "d1c44674", features = ["alloc", "x25519", "hazmat-streaming-enc"] }
matrix-pickle = { version = "0.2.2" }
prost = "0.14.3"
rand = "0.10.1"
Expand Down
15 changes: 15 additions & 0 deletions afl/hpke-message-decoding/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[package]
name = "hpke-initial-message-decoding"
version = "0.1.0"
publish = false
edition = "2021"

[dependencies]
afl = "*"

[dependencies.vodozemac]
path = "../.."

# Prevent this from interfering with workspaces
[workspace]
members = ["."]
Binary file added afl/hpke-message-decoding/in/msg_1.bin
Binary file not shown.
Binary file added afl/hpke-message-decoding/in/msg_2.bin
Binary file not shown.
Binary file added afl/hpke-message-decoding/in/msg_3.bin
Binary file not shown.
2 changes: 2 additions & 0 deletions afl/hpke-message-decoding/rust-toolchain.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[toolchain]
channel = "nightly"
12 changes: 12 additions & 0 deletions afl/hpke-message-decoding/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
use afl::fuzz;
use vodozemac::hpke::InitialMessage;

fn main() {
fuzz!(|data: &[u8]| {
if let Ok(decoded) = InitialMessage::from_bytes(data) {
let encoded = decoded.to_bytes();
let re_decoded = InitialMessage::from_bytes(&encoded).expect("Re-decoding should always succeed");
assert_eq!(decoded, re_decoded);
}
});
}
64 changes: 9 additions & 55 deletions src/ecies/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ use zeroize::{Zeroize, ZeroizeOnDrop};

pub use self::messages::{InitialMessage, Message, MessageDecodeError};
use crate::Curve25519PublicKey;
pub use crate::hpke::{CheckCode, DigitMode};

mod messages;

Expand Down Expand Up @@ -138,56 +139,6 @@ impl EciesNonce {
}
}

/// A check code that can be used to confirm that two [`EstablishedEcies`]
/// objects share the same secret. This is supposed to be shared out-of-band to
/// protect against active MITM attacks.
///
/// Since the initiator device can always tell whether a MITM attack is in
/// progress after channel establishment, this code technically carries only a
/// single bit of information, representing whether the initiator has determined
/// that the channel is "secure" or "not secure".
///
/// However, given this will need to be interactively confirmed by the user,
/// there is risk that the user would confirm the dialogue without paying
/// attention to its content. By expanding this single bit into a deterministic
/// two-digit check code, the user is forced to pay more attention by having to
/// enter it instead of just clicking through a dialogue.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CheckCode {
bytes: [u8; 2],
}

impl CheckCode {
/// Convert the check code to an array of two bytes.
///
/// The bytes can be converted to a more user-friendly representation. The
/// [`CheckCode::to_digit`] converts the bytes to a two-digit number.
pub const fn as_bytes(&self) -> &[u8; 2] {
&self.bytes
}

/// Convert the check code to two base-10 numbers.
///
/// The number should be displayed with a leading 0 in case the first digit
/// is a 0.
///
/// # Examples
///
/// ```no_run
/// # use vodozemac::ecies::CheckCode;
/// # let check_code: CheckCode = unimplemented!();
/// let check_code = check_code.to_digit();
///
/// println!("The check code of the IECS channel is: {check_code:02}");
/// ```
pub const fn to_digit(&self) -> u8 {
let first = (self.bytes[0] % 10) * 10;
let second = self.bytes[1] % 10;

first + second
}
}

/// The result of an inbound ECIES channel establishment.
#[derive(Debug)]
pub struct InboundCreationResult {
Expand Down Expand Up @@ -617,7 +568,10 @@ mod test {
"The decrypted plaintext should match our initial plaintext"
);
assert_eq!(alice.check_code(), bob.check_code());
assert_eq!(alice.check_code().to_digit(), bob.check_code().to_digit());
assert_eq!(
alice.check_code().to_digit(DigitMode::AllowLeadingZero),
bob.check_code().to_digit(DigitMode::AllowLeadingZero)
);

let message = bob.encrypt(b"Another plaintext");

Expand Down Expand Up @@ -679,7 +633,7 @@ mod test {
#[test]
fn check_code() {
let check_code = CheckCode { bytes: [0x0, 0x0] };
let digit = check_code.to_digit();
let digit = check_code.to_digit(DigitMode::AllowLeadingZero);
assert_eq!(digit, 0, "Two zero bytes should generate a 0 digit");
assert_eq!(
check_code.as_bytes(),
Expand All @@ -688,7 +642,7 @@ mod test {
);

let check_code = CheckCode { bytes: [0x9, 0x9] };
let digit = check_code.to_digit();
let digit = check_code.to_digit(DigitMode::AllowLeadingZero);
assert_eq!(
check_code.as_bytes(),
&[0x9, 0x9],
Expand All @@ -697,7 +651,7 @@ mod test {
assert_eq!(digit, 99);

let check_code = CheckCode { bytes: [0xff, 0xff] };
let digit = check_code.to_digit();
let digit = check_code.to_digit(DigitMode::AllowLeadingZero);
assert_eq!(
check_code.as_bytes(),
&[0xff, 0xff],
Expand Down Expand Up @@ -762,7 +716,7 @@ mod test {
bytes
};

let digit = check_code.to_digit();
let digit = check_code.to_digit(DigitMode::AllowLeadingZero);

prop_assert!(
(0..=99).contains(&digit),
Expand Down
187 changes: 187 additions & 0 deletions src/hpke/check_code.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
// Copyright 2026 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/// A check code that can be used to confirm that two [`EstablishedHpkeChannel`]
/// objects share the same secret. This is supposed to be shared out-of-band to
/// protect against active MITM attacks.
///
/// Since the initiator device can always tell whether a MITM attack is in
/// progress after channel establishment, this code technically carries only a
/// single bit of information, representing whether the initiator has determined
/// that the channel is "secure" or "not secure".
///
/// However, given this will need to be interactively confirmed by the user,
/// there is risk that the user would confirm the dialogue without paying
/// attention to its content. By expanding this single bit into a deterministic
/// two-digit check code, the user is forced to pay more attention by having to
/// enter it instead of just clicking through a dialogue.
///
/// An example protocol which uses the [`CheckCode`] for out of band
/// confirmation can be found in [MSC4108] and [MSC4388].
///
/// [MSC4108]: https://github.com/matrix-org/matrix-spec-proposals/pull/4108
/// [MSC4388]: https://github.com/matrix-org/matrix-spec-proposals/pull/4388
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CheckCode {
pub(crate) bytes: [u8; 2],
}

/// Enum controlling how the [`CheckCode`] should be represented as digits.
pub enum DigitMode {
/// Allow a zero to be the first digit.
///
/// This corresponds to the way the digits were used in the initial version
/// of [MSC4108].
///
/// [MSC4108]: https://github.com/matrix-org/matrix-spec-proposals/pull/4108
AllowLeadingZero,

/// Don't allow a leading zero.
///
/// This corresponds to the way the digits are defined in [MSC4388].
///
/// [MSC4388]: https://github.com/matrix-org/matrix-spec-proposals/pull/4388
NoLeadingZero,
}

impl CheckCode {
/// Convert the check code to an array of two bytes.
///
/// The bytes can be converted to a more user-friendly representation. The
/// [`CheckCode::to_digit`] converts the bytes to a two-digit number.
pub const fn as_bytes(&self) -> &[u8; 2] {
&self.bytes
}

/// Convert the check code to two base-10 numbers.
///
/// The number should be displayed with a leading 0 in case the first digit
/// is a 0.
///
/// # Examples
///
/// ```no_run
/// # use vodozemac::hpke::{CheckCode, DigitMode};
/// # let check_code: CheckCode = unimplemented!();
/// let check_code = check_code.to_digit(DigitMode::NoLeadingZero);
///
/// println!("The check code of the HPKE channel is: {check_code:02}");
/// ```
pub const fn to_digit(&self, mode: DigitMode) -> u8 {
let first = match mode {
DigitMode::AllowLeadingZero => (self.bytes[0] % 10) * 10,
DigitMode::NoLeadingZero => ((self.bytes[0] % 9) + 1) * 10,
};
let second = self.bytes[1] % 10;

first + second
}
}

#[cfg(test)]
mod tests {
use proptest::prelude::*;

use super::*;

#[test]
fn check_code_with_leading_zero() {
let check_code = CheckCode { bytes: [0x0, 0x0] };
let digit = check_code.to_digit(DigitMode::AllowLeadingZero);
assert_eq!(digit, 0, "Two zero bytes should generate a 0 digit");
assert_eq!(
check_code.as_bytes(),
&[0x0, 0x0],
"CheckCode::as_bytes() should return the exact bytes we generated."
);

let check_code = CheckCode { bytes: [0x9, 0x9] };
let digit = check_code.to_digit(DigitMode::AllowLeadingZero);
assert_eq!(
check_code.as_bytes(),
&[0x9, 0x9],
"CheckCode::as_bytes() should return the exact bytes we generated."
);
assert_eq!(digit, 99);

let check_code = CheckCode { bytes: [0xff, 0xff] };
let digit = check_code.to_digit(DigitMode::AllowLeadingZero);
assert_eq!(
check_code.as_bytes(),
&[0xff, 0xff],
"CheckCode::as_bytes() should return the exact bytes we generated."
);
assert_eq!(digit, 55, "u8::MAX should generate 55");
}

#[test]
fn check_code_no_leading_zero() {
let check_code = CheckCode { bytes: [0x0, 0x0] };
let digit = check_code.to_digit(DigitMode::NoLeadingZero);
assert_eq!(digit, 10, "Two zero bytes should generate a 10 digit");
assert_eq!(
check_code.as_bytes(),
&[0x0, 0x0],
"CheckCode::as_bytes() should return the exact bytes we generated."
);

let check_code = CheckCode { bytes: [0x8, 0x9] };
let digit = check_code.to_digit(DigitMode::NoLeadingZero);
assert_eq!(
check_code.as_bytes(),
&[0x8, 0x9],
"CheckCode::as_bytes() should return the exact bytes we generated."
);
assert_eq!(digit, 99);

let check_code = CheckCode { bytes: [0xff, 0xff] };
let digit = check_code.to_digit(DigitMode::NoLeadingZero);
assert_eq!(
check_code.as_bytes(),
&[0xff, 0xff],
"CheckCode::as_bytes() should return the exact bytes we generated."
);
assert_eq!(digit, 45, "u8::MAX should generate 45");
}

proptest! {
#[test]
fn check_code_proptest_with_leading_zero(bytes in prop::array::uniform2(0u8..) ) {
let check_code = CheckCode {
bytes
};

let digit = check_code.to_digit(DigitMode::AllowLeadingZero);

prop_assert!(
(0..=99).contains(&digit),
"The digit should be in the 0-99 range"
);
}

#[test]
fn check_code_proptest_no_leading_zero(bytes in prop::array::uniform2(0u8..) ) {
let check_code = CheckCode {
bytes
};

let digit = check_code.to_digit(DigitMode::NoLeadingZero);

prop_assert!(
(0..=99).contains(&digit),
"The digit should be in the 0-99 range"
);
}
}
}
Loading