Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add an Elliptic Curve Encryption Scheme #151

Merged
merged 2 commits into from
May 15, 2024
Merged
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
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ aes = "0.8.4"
arrayvec = { version = "0.7.4", features = ["serde"] }
base64 = "0.22.1"
cbc = { version = "0.1.2", features = ["std"] }
chacha20poly1305 = "0.10.1"
curve25519-dalek = { version = "4.1.2", default-features = false, features = ["zeroize"] }
ed25519-dalek = { version = "2.1.1", default-features = false, features = ["rand_core", "std", "serde", "hazmat", "zeroize"] }
getrandom = "0.2.14"
Expand All @@ -51,6 +52,7 @@ zeroize = "1.7.0"
[dev-dependencies]
anyhow = "1.0.82"
assert_matches = "1.5.0"
assert_matches2 = "0.1.2"
olm-rs = "2.2.0"
proptest = "1.4.0"

Expand Down
130 changes: 130 additions & 0 deletions src/ecies/messages.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// Copyright 2024 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.

use thiserror::Error;

#[cfg(doc)]
use super::EstablishedEcies;
use crate::{base64_decode, base64_encode, Curve25519PublicKey, KeyError};

/// The error type for the ECIES message decoding failures.
#[derive(Debug, Error)]
pub enum MessageDecodeError {
/// The initial message could not have been decoded, it's missing the `|`
/// separator.
#[error("The initial message is missing the | separator")]
MissingSeparator,
/// The initial message could not have been decoded, the embedded Curve25519
/// key is malformed.
#[error("The embedded ephemeral Curve25519 key could not have been decoded: {0:?}")]
KeyError(#[from] KeyError),
/// The ciphertext is not valid base64.
#[error("The ciphertext could not have been decoded from a base64 string: {0:?}")]
Base64(#[from] base64::DecodeError),
}

/// The initial message, sent by the ECIES channel establisher.
///
/// This message embeds the public key of the message creator allowing the other
/// side to establish a channel using this message.
///
/// This key is *unauthenticated* so authentication needs to happen out-of-band
/// in order for the established channel to become secure.
#[derive(Debug, PartialEq, Eq)]
pub struct InitialMessage {
/// The ephemeral public key that was used to establish the ECIES channel.
pub public_key: Curve25519PublicKey,
/// The ciphertext of the initial message.
pub ciphertext: Vec<u8>,
}

impl InitialMessage {
/// Encode the message as a string.
///
/// The string will contain the base64-encoded Curve25519 public key and the
/// ciphertext of the message separated by a `|`.
pub fn encode(&self) -> String {
let ciphertext = base64_encode(&self.ciphertext);
let key = self.public_key.to_base64();

format!("{ciphertext}|{key}")
}

/// Attempt do decode a string into a [`InitialMessage`].
pub fn decode(message: &str) -> Result<Self, MessageDecodeError> {
match message.split_once('|') {
Some((ciphertext, key)) => {
let public_key = Curve25519PublicKey::from_base64(key)?;
let ciphertext = base64_decode(ciphertext)?;

Ok(Self { ciphertext, public_key })
}
None => Err(MessageDecodeError::MissingSeparator),
}
}
}

/// An encrypted message a [`EstablishedEcies`] channel has sent.
#[derive(Debug)]
pub struct Message {
/// The ciphertext of the message.
pub ciphertext: Vec<u8>,
}

impl Message {
/// Encode the message as a string.
///
/// The ciphertext bytes will be encoded using unpadded base64.
pub fn encode(&self) -> String {
base64_encode(&self.ciphertext)
}

/// Attempt do decode a base64 string into a [`Message`].
pub fn decode(message: &str) -> Result<Self, MessageDecodeError> {
Ok(Self { ciphertext: base64_decode(message)? })
}
}

#[cfg(test)]
mod test {
use super::*;

const INITIAL_MESSAGE: &str = "3On7QFJyLQMAErua9K/yIOcJALvuMYax1AW0iWgf64AwtSMZXwAA012Q|9yA/CX8pJKF02Prd75ZyBQHg3fGTVVGDNl86q1z17Us";
const MESSAGE: &str = "ZmtSLdzMcyjC5eV6L8xBI6amsq7gDNbCjz1W5OjX4Z8W";
const PUBLIC_KEY: &str = "9yA/CX8pJKF02Prd75ZyBQHg3fGTVVGDNl86q1z17Us";

#[test]
fn initial_message() {
let message = InitialMessage::decode(INITIAL_MESSAGE)
.expect("We should be able to decode our known-valid initial message");

assert_eq!(
message.public_key.to_base64(),
PUBLIC_KEY,
"The decoded public key should match the expected one"
);

let encoded = message.encode();
assert_eq!(INITIAL_MESSAGE, encoded);
}

#[test]
fn message() {
let message = Message::decode(MESSAGE)
.expect("We should be able to decode our known-valid initial message");

let encoded = message.encode();
assert_eq!(MESSAGE, encoded);
}
}
Loading