diff --git a/.clippy.toml b/.clippy.toml new file mode 100644 index 000000000..1645c19f3 --- /dev/null +++ b/.clippy.toml @@ -0,0 +1 @@ +msrv = "1.70.0" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5f27e4045..b3f78b42a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,74 +4,61 @@ on: - master pull_request: schedule: - - cron: '30 3 * * 2' + - cron: "30 3 * * 2" name: CI -jobs: +env: + RUST_VERSION: "1.70.0" +jobs: test: name: run tests strategy: matrix: - rust: [1.72, stable] + rust: [$RUST_VERSION, stable] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions-rs/toolchain@v1 + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@v1 with: toolchain: ${{ matrix.rust }} - override: true - name: Build with default features - uses: actions-rs/cargo@v1 - with: - command: build - args: --all-targets + run: cargo build --all-targets - name: Build with all features - uses: actions-rs/cargo@v1 - with: - command: build - args: --all-targets --all-features + run: cargo build --all-targets --all-features - name: Run tests - uses: actions-rs/cargo@v1 - with: - command: test - args: --all-features + run: cargo test --all-features fmt: name: run rustfmt runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions-rs/toolchain@v1 - with: - toolchain: 1.57 - override: true - - run: rustup component add rustfmt - - uses: actions-rs/cargo@v1 - with: - command: fmt - args: --all -- --check + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@v1 + with: + toolchain: $RUST_VERSION + components: rustfmt + - name: Check formatting in all files + run: cargo fmt --all -- --check clippy: name: run clippy lints runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions-rs/toolchain@v1 + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@v1 with: toolchain: stable components: clippy - override: true - - uses: actions-rs/clippy-check@v1 - with: - token: ${{ secrets.GITHUB_TOKEN }} - args: --all-features + - name: Check if there are clippy warnings + run: cargo clippy --all-features -- -D warnings audit: name: run cargo audit runs-on: ubuntu-latest container: dbrgn/cargo-audit:latest steps: - - uses: actions/checkout@v2 - - run: cargo audit + - uses: actions/checkout@v4 + - name: Audit all used dependencies + run: cargo audit diff --git a/CHANGELOG.md b/CHANGELOG.md index 3897b7e26..7dec63c9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,13 @@ Possible log types: ### Unreleased +- [added] Re-export `crypto_secretbox::Nonce` +- [changed] Replace `sodiumoxide` with `crypto_box` and `crypto_secretbox` +- [changed] Replace re-exports of `PublicKey`, `SecretKey` and `Key` +- [changed] Use dedicated `Nonce` type instead of `&[u8; 24]` +- [changed] Return result in `encrypt_*` functions +- [changed] Use `hmac` and `sha2` crates for calculating MAC + ### v0.16.0 (2023-09-04) - [added] Expose encryption functions: `encrypt` and `encrypt_raw` (#59) diff --git a/Cargo.toml b/Cargo.toml index 0e7c72776..bc90cd16c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,15 +24,20 @@ receive = ["form_urlencoded", "serde_urlencoded"] # Support for receiving and de [dependencies] byteorder = "1.0" +crypto_box = "0.9.1" +crypto_secretbox = "0.1.1" data-encoding = "2.1" form_urlencoded = { version = "1", optional = true } +hmac = "0.12.1" log = "0.4" -thiserror = "1" reqwest = { version = "0.11", features = ["rustls-tls-native-roots", "multipart"], default-features = false } +rand = "0.8.5" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" serde_urlencoded = { version = "0.7", optional = true } -sodiumoxide = "0.2.0" +sha2 = "0.10.8" +thiserror = "1" +zeroize = { version = "1", features = ["zeroize_derive"], default-features = false } [dev-dependencies] docopt = "1.1.0" diff --git a/examples/download_blob.rs b/examples/download_blob.rs index 4a41de46d..d7b96b905 100644 --- a/examples/download_blob.rs +++ b/examples/download_blob.rs @@ -33,7 +33,8 @@ async fn main() { let bytes = HEXLOWER_PERMISSIVE .decode(blob_key_raw.as_bytes()) .expect("Invalid blob key"); - Some(Key::from_slice(&bytes).expect("Invalid blob key bytes")) + let key = Key::try_from(bytes).expect("Invalid size of blob key"); + Some(key) } else { None }; diff --git a/examples/generate_keypair.rs b/examples/generate_keypair.rs index dcd4ca55b..32f575c3f 100644 --- a/examples/generate_keypair.rs +++ b/examples/generate_keypair.rs @@ -1,10 +1,11 @@ +use crypto_secretbox::aead::OsRng; use data_encoding::HEXLOWER; -use sodiumoxide::crypto::box_; fn main() { println!("Generating new random nacl/libsodium crypto box keypair:\n"); - let (pk, sk) = box_::gen_keypair(); - println!(" Public: {}", HEXLOWER.encode(&pk.0)); - println!(" Private: {}", HEXLOWER.encode(&sk.0)); + let sk = crypto_box::SecretKey::generate(&mut OsRng); + let pk = sk.public_key(); + println!(" Public: {}", HEXLOWER.encode(pk.as_bytes())); + println!(" Private: {}", HEXLOWER.encode(&sk.to_bytes())); println!("\nKeep the private key safe, and don't share it with anybody!"); } diff --git a/examples/receive.rs b/examples/receive.rs index 2fb2bb150..fb19d8f66 100644 --- a/examples/receive.rs +++ b/examples/receive.rs @@ -18,20 +18,22 @@ async fn main() { // Command line arguments let our_id = args.get_str(""); let secret = args.get_str(""); - let private_key = HEXLOWER_PERMISSIVE + let key_bytes = HEXLOWER_PERMISSIVE .decode(args.get_str("").as_bytes()) - .ok() - .and_then(|bytes| SecretKey::from_slice(&bytes)) - .unwrap_or_else(|| { - eprintln!("Invalid private key"); + .unwrap_or_else(|_| { + eprintln!("No private key provided"); std::process::exit(1); }); + let private_key = SecretKey::from_slice(&key_bytes).unwrap_or_else(|_| { + eprintln!("Invalid private key"); + std::process::exit(1); + }); let request_body = args.get_str(""); // Create E2eApi instance let api = ApiBuilder::new(our_id, secret) - .with_private_key_bytes(private_key.as_ref()) - .and_then(|builder| builder.into_e2e()) + .with_private_key(private_key) + .into_e2e() .unwrap(); // Parse request body diff --git a/examples/send_e2e_file.rs b/examples/send_e2e_file.rs index d7d84f6e2..ff55ed600 100644 --- a/examples/send_e2e_file.rs +++ b/examples/send_e2e_file.rs @@ -93,7 +93,7 @@ async fn main() { file: file_bytes, thumbnail: thumbnail_bytes, }; - let (encrypted, key) = encrypt_file_data(&file_data); + let (encrypted, key) = etry!(encrypt_file_data(&file_data), "Could not encrypt file"); // Upload files to blob server let file_blob_id = etry!( @@ -126,7 +126,10 @@ async fn main() { .rendering_type(rendering_type) .build() .expect("Could not build FileMessage"); - let encrypted = api.encrypt_file_msg(&msg, &public_key.into()); + let encrypted = etry!( + api.encrypt_file_msg(&msg, &public_key.into()), + "Could not encrypt file msg" + ); // Send let msg_id = api.send(to, &encrypted, false).await; diff --git a/examples/send_e2e_image.rs b/examples/send_e2e_image.rs index 337e8db9c..e939687c2 100644 --- a/examples/send_e2e_image.rs +++ b/examples/send_e2e_image.rs @@ -61,7 +61,12 @@ async fn main() { println!("Could not read file: {}", e); process::exit(1); }); - let encrypted_image = api.encrypt_raw(&img_data, &recipient_key); + let encrypted_image = api + .encrypt_raw(&img_data, &recipient_key) + .unwrap_or_else(|_| { + println!("Could encrypt raw msg"); + process::exit(1); + }); // Upload image to blob server let blob_id = api @@ -73,15 +78,20 @@ async fn main() { }); // Create image message - let msg = api.encrypt_image_msg( - &blob_id, - img_data.len() as u32, - &encrypted_image.nonce, - &recipient_key, - ); + let msg = api + .encrypt_image_msg( + &blob_id, + img_data.len() as u32, + &encrypted_image.nonce, + &recipient_key, + ) + .unwrap_or_else(|e| { + println!("Could not encrypt image msg: {e}"); + process::exit(1); + }); // Send - let msg_id = api.send(&to, &msg, false).await; + let msg_id = api.send(to, &msg, false).await; match msg_id { Ok(id) => println!("Sent. Message id is {}.", id), Err(e) => println!("Could not send message: {}", e), diff --git a/examples/send_e2e_text.rs b/examples/send_e2e_text.rs index dcf512303..d0e9ecc99 100644 --- a/examples/send_e2e_text.rs +++ b/examples/send_e2e_text.rs @@ -37,8 +37,13 @@ async fn main() { }); // Encrypt and send - let encrypted = api.encrypt_text_msg(&text, &public_key.into()); - let msg_id = api.send(&to, &encrypted, false).await; + let encrypted = api + .encrypt_text_msg(&text, &public_key.into()) + .unwrap_or_else(|e| { + println!("Could not encrypt text msg: {e}"); + process::exit(1); + }); + let msg_id = api.send(to, &encrypted, false).await; match msg_id { Ok(id) => println!("Sent. Message id is {}.", id), diff --git a/src/api.rs b/src/api.rs index fce7343d4..a7c906b1f 100644 --- a/src/api.rs +++ b/src/api.rs @@ -4,9 +4,10 @@ use std::{ time::Duration, }; +use crypto_box::{PublicKey, SecretKey}; +use crypto_secretbox::Nonce; use data_encoding::HEXLOWER_PERMISSIVE; use reqwest::Client; -use sodiumoxide::crypto::box_::PublicKey; use crate::{ connection::{blob_download, blob_upload, send_e2e, send_simple, Recipient}, @@ -20,7 +21,7 @@ use crate::{ }, receive::IncomingMessage, types::{BlobId, FileMessage, MessageType}, - SecretKey, MSGAPI_URL, + MSGAPI_URL, }; fn make_reqwest_client() -> Client { @@ -173,7 +174,11 @@ impl E2eApi { } /// Encrypt a text message for the specified recipient public key. - pub fn encrypt_text_msg(&self, text: &str, recipient_key: &RecipientKey) -> EncryptedMessage { + pub fn encrypt_text_msg( + &self, + text: &str, + recipient_key: &RecipientKey, + ) -> Result { let data = text.as_bytes(); let msgtype = MessageType::Text; encrypt(data, msgtype, &recipient_key.0, &self.private_key) @@ -192,9 +197,9 @@ impl E2eApi { &self, blob_id: &BlobId, img_size_bytes: u32, - image_data_nonce: &[u8; 24], + image_data_nonce: &Nonce, recipient_key: &RecipientKey, - ) -> EncryptedMessage { + ) -> Result { encrypt_image_msg( blob_id, img_size_bytes, @@ -214,7 +219,7 @@ impl E2eApi { &self, msg: &FileMessage, recipient_key: &RecipientKey, - ) -> EncryptedMessage { + ) -> Result { encrypt_file_msg(msg, &recipient_key.0, &self.private_key) } @@ -233,12 +238,16 @@ impl E2eApi { raw_data: &[u8], msgtype: MessageType, recipient_key: &RecipientKey, - ) -> EncryptedMessage { + ) -> Result { encrypt(raw_data, msgtype, &recipient_key.0, &self.private_key) } /// Encrypt raw bytes for the specified recipient public key. - pub fn encrypt_raw(&self, raw_data: &[u8], recipient_key: &RecipientKey) -> EncryptedMessage { + pub fn encrypt_raw( + &self, + raw_data: &[u8], + recipient_key: &RecipientKey, + ) -> Result { encrypt_raw(raw_data, &recipient_key.0, &self.private_key) } @@ -507,8 +516,9 @@ impl ApiBuilder { /// Set the private key from a byte slice. Only needed for E2e mode. pub fn with_private_key_bytes(mut self, private_key: &[u8]) -> Result { - let private_key = SecretKey::from_slice(private_key) - .ok_or_else(|| ApiBuilderError::InvalidKey("Invalid libsodium private key".into()))?; + let private_key = SecretKey::from_slice(private_key).map_err(|e| { + ApiBuilderError::InvalidKey(format!("Invalid libsodium private key: {e}")) + })?; self.private_key = Some(private_key); Ok(self) } diff --git a/src/connection.rs b/src/connection.rs index 6db5b0d72..0054d22f0 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -223,13 +223,11 @@ pub(crate) async fn blob_download( mod tests { use super::*; - use std::iter::repeat; - use crate::{errors::ApiError, MSGAPI_URL}; #[tokio::test] async fn test_simple_max_length_ok() { - let text: String = repeat("à").take(3500 / 2).collect(); + let text: String = "à".repeat(3500 / 2); let client = Client::new(); let result = send_simple( &client, @@ -247,7 +245,7 @@ mod tests { #[tokio::test] async fn test_simple_max_length_too_long() { - let mut text: String = repeat("à").take(3500 / 2).collect(); + let mut text: String = "à".repeat(3500 / 2); text.push('x'); let client = Client::new(); let result = send_simple( diff --git a/src/crypto.rs b/src/crypto.rs index f7d3734fd..8b5352941 100644 --- a/src/crypto.rs +++ b/src/crypto.rs @@ -1,36 +1,108 @@ //! Encrypt and decrypt messages. -use std::{convert::Into, io::Write, iter::repeat, str::FromStr}; +use std::{convert::Into, fmt::Debug, io::Write, iter::repeat, str::FromStr, sync::OnceLock}; use byteorder::{LittleEndian, WriteBytesExt}; +use crypto_box::{aead::Aead, SalsaBox}; +use crypto_secretbox::{ + aead::{OsRng, Payload}, + cipher::generic_array::GenericArray, + AeadCore, Key as SecretboxKey, KeyInit, Nonce, XSalsa20Poly1305, +}; use data_encoding::{HEXLOWER, HEXLOWER_PERMISSIVE}; +use rand::Rng; +use serde::{Serialize, Serializer}; use serde_json as json; -use sodiumoxide::{ - crypto::{box_, secretbox}, - randombytes::randombytes_into, -}; +use zeroize::{Zeroize, ZeroizeOnDrop}; use crate::{ - errors::CryptoError, + errors::{self, CryptoError}, types::{BlobId, FileMessage, MessageType}, - Key, PublicKey, SecretKey, + PublicKey, SecretKey, }; +pub const NONCE_SIZE: usize = 24; +const KEY_SIZE: usize = 32; + +/// Key type used for nacl secretbox cryptography +#[derive(PartialEq, Zeroize, ZeroizeOnDrop)] +pub struct Key(SecretboxKey); + +impl AsRef for Key { + fn as_ref(&self) -> &SecretboxKey { + &self.0 + } +} + +impl Debug for Key { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write! {f, "Key([…])"} + } +} + +impl From for Key { + fn from(value: SecretboxKey) -> Self { + Self(value) + } +} + +impl From<[u8; KEY_SIZE]> for Key { + fn from(value: [u8; KEY_SIZE]) -> Self { + Self(GenericArray::from(value)) + } +} + +impl TryFrom> for Key { + type Error = errors::CryptoError; + + fn try_from(value: Vec) -> Result { + <[u8; KEY_SIZE]>::try_from(value) + .map_err(|original| { + CryptoError::BadKey(format!( + "Key has wrong size: {} instead of {}", + original.len(), + KEY_SIZE + )) + }) + .map(SecretboxKey::from) + .map(Self::from) + } +} + +impl Serialize for Key { + fn serialize(&self, serializer: S) -> Result { + serializer.serialize_str(&HEXLOWER.encode(&self.0)) + } +} + +fn get_file_nonce() -> &'static Nonce { + static FILE_NONCE: OnceLock = OnceLock::new(); + FILE_NONCE.get_or_init(|| { + Nonce::from([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, + ]) + }) +} + +fn get_thumb_nonce() -> &'static Nonce { + static THUMB_NONCE: OnceLock = OnceLock::new(); + THUMB_NONCE.get_or_init(|| { + Nonce::from([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, + ]) + }) +} + /// Return a random number in the range `[1, 255]`. fn random_padding_amount() -> u8 { - let mut buf: [u8; 1] = [0]; - loop { - randombytes_into(&mut buf); - if buf[0] < 255 { - return buf[0] + 1; - } - } + let mut rng = rand::thread_rng(); + rng.gen_range(1..=255) } /// An encrypted message. Contains both the ciphertext and the nonce. pub struct EncryptedMessage { pub ciphertext: Vec, - pub nonce: [u8; 24], + pub nonce: Nonce, } /// The public key of a recipient. @@ -46,27 +118,26 @@ impl From for RecipientKey { impl From<[u8; 32]> for RecipientKey { /// Create a `RecipientKey` from a byte array fn from(val: [u8; 32]) -> Self { - RecipientKey(PublicKey(val)) + RecipientKey(PublicKey::from(val)) } } impl RecipientKey { /// Create a `RecipientKey` from a byte slice. It must contain 32 bytes. pub fn from_bytes(val: &[u8]) -> Result { - match PublicKey::from_slice(val) { - Some(pk) => Ok(RecipientKey(pk)), - None => Err(CryptoError::BadKey("Invalid libsodium public key".into())), - } + PublicKey::from_slice(val) + .map(RecipientKey::from) + .map_err(|_| CryptoError::BadKey("Invalid public key".into())) } /// Return a reference to the contained key bytes. pub fn as_bytes(&self) -> &[u8] { - &(self.0).0 + self.0.as_ref() } /// Encode the key bytes as lowercase hex string. pub fn to_hex_string(&self) -> String { - HEXLOWER.encode(&(self.0).0) + HEXLOWER.encode(self.as_bytes()) } } @@ -87,14 +158,13 @@ pub fn encrypt_raw( data: &[u8], public_key: &PublicKey, private_key: &SecretKey, -) -> EncryptedMessage { - sodiumoxide::init().expect("Could not initialize sodiumoxide library."); - let nonce = box_::gen_nonce(); - let ciphertext = box_::seal(data, &nonce, public_key, private_key); - EncryptedMessage { - ciphertext, - nonce: nonce.0, - } +) -> Result { + let crypto_box: SalsaBox = SalsaBox::new(public_key, private_key); + let nonce: Nonce = SalsaBox::generate_nonce(&mut OsRng); + let ciphertext = crypto_box + .encrypt(&nonce, data) + .map_err(|_| CryptoError::EncryptionFailed)?; + Ok(EncryptedMessage { ciphertext, nonce }) } /// Encrypt a message with the specified `msgtype` for the recipient. @@ -105,7 +175,7 @@ pub fn encrypt( msgtype: MessageType, public_key: &PublicKey, private_key: &SecretKey, -) -> EncryptedMessage { +) -> Result { // Add random amount of PKCS#7 style padding let padding_amount = random_padding_amount(); let padding = repeat(padding_amount).take(padding_amount as usize); @@ -123,10 +193,10 @@ pub fn encrypt( pub fn encrypt_image_msg( blob_id: &BlobId, img_size_bytes: u32, - image_data_nonce: &[u8; 24], + image_data_nonce: &Nonce, public_key: &PublicKey, private_key: &SecretKey, -) -> EncryptedMessage { +) -> Result { let mut data = [0; 44]; // Since we're writing to an array and not to a file or socket, these // write operations should never fail. @@ -148,19 +218,12 @@ pub fn encrypt_file_msg( msg: &FileMessage, public_key: &PublicKey, private_key: &SecretKey, -) -> EncryptedMessage { +) -> Result { let data = json::to_string(msg).unwrap(); let msgtype = MessageType::File; encrypt(data.as_bytes(), msgtype, public_key, private_key) } -static FILE_NONCE: secretbox::Nonce = secretbox::Nonce([ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, -]); -static THUMB_NONCE: secretbox::Nonce = secretbox::Nonce([ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, -]); - /// Raw unencrypted bytes of a file and optionally a thumbnail. /// /// This struct is used as a parameter type for [`encrypt_file_data`] and @@ -185,22 +248,24 @@ pub struct EncryptedFileData { /// symmetric key. /// /// Return the encrypted bytes and the key. -pub fn encrypt_file_data(data: &FileData) -> (EncryptedFileData, Key) { - // Make sure to init sodiumoxide library - sodiumoxide::init().unwrap(); - +pub fn encrypt_file_data(data: &FileData) -> Result<(EncryptedFileData, Key), CryptoError> { // Generate a random encryption key - let key = secretbox::gen_key(); + let key: Key = XSalsa20Poly1305::generate_key(&mut OsRng).into(); + let secretbox = XSalsa20Poly1305::new(key.as_ref()); // Encrypt data // Note: Since we generate a random key, we can safely re-use constant nonces. - let file = secretbox::seal(&data.file, &FILE_NONCE, &key); + let file = secretbox + .encrypt(get_file_nonce(), Payload::from(data.file.as_ref())) + .map_err(|_| CryptoError::EncryptionFailed)?; let thumbnail = data .thumbnail .as_ref() - .map(|t| secretbox::seal(t, &THUMB_NONCE, &key)); + .map(|bytes| secretbox.encrypt(get_thumb_nonce(), Payload::from(bytes.as_ref()))) + .transpose() + .map_err(|_| CryptoError::EncryptionFailed)?; - (EncryptedFileData { file, thumbnail }, key) + Ok((EncryptedFileData { file, thumbnail }, key)) } /// Decrypt file data and optional thumbnail data with the provided symmetric @@ -211,19 +276,18 @@ pub fn decrypt_file_data( data: &EncryptedFileData, encryption_key: &Key, ) -> Result { - // Make sure to init sodiumoxide library - sodiumoxide::init().unwrap(); + let secretbox = XSalsa20Poly1305::new(encryption_key.as_ref()); - let file = secretbox::open(&data.file, &FILE_NONCE, encryption_key) + let file = secretbox + .decrypt(get_file_nonce(), Payload::from(data.file.as_ref())) + .map_err(|_| CryptoError::DecryptionFailed)?; + + let thumbnail = data + .thumbnail + .as_ref() + .map(|bytes| secretbox.decrypt(get_thumb_nonce(), Payload::from(bytes.as_ref()))) + .transpose() .map_err(|_| CryptoError::DecryptionFailed)?; - let thumbnail = match data.thumbnail.as_ref() { - Some(t) => { - let decrypted = secretbox::open(t, &THUMB_NONCE, encryption_key) - .map_err(|_| CryptoError::DecryptionFailed)?; - Some(decrypted) - } - None => None, - }; Ok(FileData { file, thumbnail }) } @@ -236,7 +300,7 @@ mod test { api::ApiBuilder, types::{BlobId, MessageType}, }; - use sodiumoxide::crypto::box_::{self, Nonce, PublicKey, SecretKey}; + use crypto_box::{Nonce, PublicKey, SalsaBox, SecretKey}; use super::*; @@ -261,11 +325,11 @@ mod test { #[test] fn test_encrypt_image_msg() { // Set up keys - let own_sec = SecretKey([ + let own_sec = SecretKey::from([ 113, 146, 154, 1, 241, 143, 18, 181, 240, 174, 72, 16, 247, 83, 161, 29, 215, 123, 130, 243, 235, 222, 137, 151, 107, 162, 47, 119, 98, 145, 68, 146, ]); - let other_pub = PublicKey([ + let other_pub = PublicKey::from([ 153, 153, 204, 118, 225, 119, 78, 112, 88, 6, 167, 2, 67, 73, 254, 255, 96, 134, 225, 8, 36, 229, 124, 219, 43, 50, 241, 185, 244, 236, 55, 77, ]); @@ -278,20 +342,23 @@ mod test { // Fake a blob upload let blob_id = BlobId::from_str("00112233445566778899aabbccddeeff").unwrap(); - let blob_nonce = box_::gen_nonce(); + let blob_nonce: Nonce = SalsaBox::generate_nonce(&mut OsRng); // Encrypt let recipient_key = RecipientKey(other_pub); - let encrypted = api.encrypt_image_msg(&blob_id, 258, &blob_nonce.0, &recipient_key); + let encrypted = api + .encrypt_image_msg(&blob_id, 258, &blob_nonce, &recipient_key) + .unwrap(); + + let crypto_box: SalsaBox = SalsaBox::new(&recipient_key.0, &own_sec); // Decrypt - let decrypted = box_::open( - &encrypted.ciphertext, - &Nonce(encrypted.nonce), - &other_pub, - &own_sec, - ) - .unwrap(); + let decrypted = crypto_box + .decrypt( + &encrypted.nonce, + Payload::from(encrypted.ciphertext.as_ref()), + ) + .unwrap(); // Validate and remove padding let padding_bytes = decrypted[decrypted.len() - 1] as usize; @@ -306,7 +373,7 @@ mod test { assert_eq!(data.len(), 44 + 1); assert_eq!(&data[1..17], &blob_id.0); assert_eq!(&data[17..21], &[2, 1, 0, 0]); - assert_eq!(&data[21..45], &blob_nonce.0); + assert_eq!(&data[21..45], &blob_nonce[..]); } #[test] @@ -384,21 +451,22 @@ mod test { }; // Encrypt - let (encrypted, key) = encrypt_file_data(&data); + let (encrypted, key) = encrypt_file_data(&data).unwrap(); let encrypted_thumb = encrypted.thumbnail.expect("Thumbnail missing"); + let secretbox = XSalsa20Poly1305::new(key.as_ref()); + // Ensure that encrypted data is different from plaintext data assert_ne!(encrypted.file, file_data); assert_ne!(encrypted_thumb, thumb_data); - assert_eq!(encrypted.file.len(), file_data.len() + secretbox::MACBYTES); - assert_eq!( - encrypted_thumb.len(), - thumb_data.len() + secretbox::MACBYTES - ); // Test that data can be decrypted - let decrypted_file = secretbox::open(&encrypted.file, &FILE_NONCE, &key).unwrap(); - let decrypted_thumb = secretbox::open(&encrypted_thumb, &THUMB_NONCE, &key).unwrap(); + let decrypted_file = secretbox + .decrypt(get_file_nonce(), Payload::from(encrypted.file.as_ref())) + .unwrap(); + let decrypted_thumb = secretbox + .decrypt(get_thumb_nonce(), Payload::from(encrypted_thumb.as_ref())) + .unwrap(); assert_eq!(decrypted_file, &file_data); assert_eq!(decrypted_thumb, &thumb_data); } @@ -409,15 +477,18 @@ mod test { let (_, key1) = encrypt_file_data(&FileData { file: [1, 2, 3].to_vec(), thumbnail: None, - }); + }) + .unwrap(); let (_, key2) = encrypt_file_data(&FileData { file: [1, 2, 3].to_vec(), thumbnail: None, - }); + }) + .unwrap(); let (_, key3) = encrypt_file_data(&FileData { file: [1, 2, 3].to_vec(), thumbnail: None, - }); + }) + .unwrap(); assert_ne!(key1, key2); assert_ne!(key2, key3); assert_ne!(key1, key3); @@ -433,7 +504,7 @@ mod test { }; // Encrypt - let (encrypted, key) = encrypt_file_data(&data); + let (encrypted, key) = encrypt_file_data(&data).unwrap(); assert_ne!(encrypted.file, data.file); assert!(encrypted.thumbnail.is_some()); assert_ne!(encrypted.thumbnail, data.thumbnail); diff --git a/src/errors.rs b/src/errors.rs index b61f386df..39199f1d1 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -83,6 +83,10 @@ pub enum CryptoError { /// Decryption failed #[error("decryption failed")] DecryptionFailed, + + /// Encryption failed + #[error("encryption failed")] + EncryptionFailed, } /// Errors when interacting with the [`ApiBuilder`](../struct.ApiBuilder.html). diff --git a/src/lib.rs b/src/lib.rs index 2c5a136eb..158636f8f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -54,7 +54,8 @@ //! let public_key = api.lookup_pubkey(to).await.unwrap(); //! //! // Encrypt -//! let encrypted = api.encrypt_text_msg(text, &public_key.into()); +//! let encrypted = api.encrypt_text_msg(text, &public_key.into()) +//! .expect("Could not encrypt text msg"); //! //! // Send //! match api.send(&to, &encrypted, false).await { @@ -82,17 +83,15 @@ mod lookup; mod receive; mod types; -pub use sodiumoxide::crypto::{ - box_::{PublicKey, SecretKey}, - secretbox::Key, -}; +pub use crypto_box::{PublicKey, SecretKey}; +pub use crypto_secretbox::Nonce; pub use crate::{ api::{ApiBuilder, E2eApi, SimpleApi}, connection::Recipient, crypto::{ decrypt_file_data, encrypt, encrypt_file_data, encrypt_raw, EncryptedFileData, - EncryptedMessage, FileData, RecipientKey, + EncryptedMessage, FileData, Key, RecipientKey, }, lookup::{Capabilities, LookupCriterion}, types::{BlobId, FileMessage, FileMessageBuilder, MessageType, RenderingType}, diff --git a/src/lookup.rs b/src/lookup.rs index 24fd85bd3..743b666c7 100644 --- a/src/lookup.rs +++ b/src/lookup.rs @@ -2,9 +2,9 @@ use std::{fmt, str}; +use crypto_box::{PublicKey, KEY_SIZE}; use data_encoding::HEXLOWER_PERMISSIVE; use reqwest::Client; -use sodiumoxide::crypto::box_::PublicKey; use crate::{connection::map_response_code, errors::ApiError}; @@ -141,20 +141,20 @@ pub(crate) async fn lookup_pubkey( let pubkey_hex_bytes = res.bytes().await?; // Decode key - let mut pubkey = [0u8; 32]; + let mut pubkey = [0u8; KEY_SIZE]; let bytes_decoded = HEXLOWER_PERMISSIVE .decode_mut(&pubkey_hex_bytes, &mut pubkey) .map_err(|e| { warn!("Could not parse public key fetched from API: {:?}", e); ApiError::ParseError("Invalid hex bytes for public key".to_string()) })?; - if bytes_decoded != 32 { + if bytes_decoded != KEY_SIZE { return Err(ApiError::ParseError(format!( "Invalid public key: Length must be 32 bytes, but is {} bytes", bytes_decoded ))); } - Ok(PublicKey(pubkey)) + Ok(pubkey.into()) } /// Look up an ID in the Threema directory. diff --git a/src/receive.rs b/src/receive.rs index 4cbaf053a..6eece713b 100644 --- a/src/receive.rs +++ b/src/receive.rs @@ -2,14 +2,19 @@ use std::{borrow::Cow, collections::HashMap}; +use crypto_box::{aead::Aead, PublicKey, SalsaBox, SecretKey}; +use crypto_secretbox::{aead::Payload, Nonce}; use data_encoding::HEXLOWER_PERMISSIVE; +use hmac::{Hmac, Mac}; use serde::{Deserialize, Deserializer}; -use sodiumoxide::crypto::{ - auth::hmacsha256, - box_::{self, Nonce, PublicKey, SecretKey}, +use sha2::Sha256; + +use crate::{ + crypto::NONCE_SIZE, + errors::{ApiError, CryptoError}, }; -use crate::errors::{ApiError, CryptoError}; +type HmacSha256 = Hmac; /// Deserialize a hex string into a byte vector. fn deserialize_hex_string<'de, D>(deserializer: D) -> Result, D::Error> @@ -92,7 +97,8 @@ impl IncomingMessage { } // Validate MAC - let mut hmac_state = hmacsha256::State::init(api_secret.as_bytes()); + let mut hmac_state = HmacSha256::new_from_slice(api_secret.as_bytes()) + .map_err(|_| ApiError::Other("Invalid api_secret".to_string()))?; for field in &["from", "to", "messageId", "date", "nonce", "box"] { hmac_state.update( values @@ -103,9 +109,8 @@ impl IncomingMessage { .as_bytes(), ); } - let given_tag = hmacsha256::Tag(mac); - let calculated_tag = hmac_state.finalize(); - if given_tag != calculated_tag { + + if hmac_state.verify_slice(&mac).is_err() { return Err(ApiError::InvalidMac); } @@ -130,10 +135,14 @@ impl IncomingMessage { private_key: &SecretKey, ) -> Result, CryptoError> { // Decode nonce - let nonce: Nonce = Nonce::from_slice(&self.nonce).ok_or(CryptoError::BadNonce)?; + let nonce_bytes = + <[u8; NONCE_SIZE]>::try_from(&self.nonce[..]).map_err(|_| CryptoError::BadNonce)?; + let nonce: Nonce = Nonce::from(nonce_bytes); // Decrypt bytes - let mut decrypted = box_::open(&self.box_data, &nonce, public_key, private_key) + let crypto_box: SalsaBox = SalsaBox::new(public_key, private_key); + let mut decrypted = crypto_box + .decrypt(&nonce, Payload::from(self.box_data.as_ref())) .map_err(|_| CryptoError::DecryptionFailed)?; // Remove PKCS#7 style padding @@ -178,26 +187,38 @@ mod tests { } mod decrypt_box { + use crypto_secretbox::aead::OsRng; + + use crypto_box::aead::AeadCore; + use super::*; #[test] fn decrypt() { - let (a_pk, a_sk) = box_::gen_keypair(); - let (b_pk, b_sk) = box_::gen_keypair(); - let nonce = box_::gen_nonce(); + let a_sk = SecretKey::generate(&mut OsRng); + let a_pk = a_sk.public_key(); + + let b_sk = SecretKey::generate(&mut OsRng); + let b_pk = b_sk.public_key(); + + let a_box = SalsaBox::new(&b_pk, &a_sk); + + let nonce = SalsaBox::generate_nonce(&mut OsRng); + + let box_data = a_box + .encrypt( + &nonce, + Payload::from([/* data */ 1, 2, 42, /* padding */ 3, 3, 3].as_ref()), + ) + .expect("Failed to encrypt data"); let msg = IncomingMessage { from: "AAAAAAAA".into(), to: "*BBBBBBB".into(), message_id: "00112233".into(), date: 0, - nonce: nonce.0.to_vec(), - box_data: box_::seal( - &[/* data */ 1, 2, 42, /* padding */ 3, 3, 3], - &nonce, - &b_pk, - &a_sk, - ), + nonce: nonce.to_vec(), + box_data, nickname: None, }; @@ -212,7 +233,8 @@ mod tests { #[test] fn decrypt_bad_nonce() { - let (pk, sk) = box_::gen_keypair(); + let sk = SecretKey::generate(&mut OsRng); + let pk = sk.public_key(); let msg = IncomingMessage { from: "AAAAAAAA".into(), to: "*BBBBBBB".into(), @@ -229,22 +251,30 @@ mod tests { #[test] fn decrypt_bad_padding() { - let (a_pk, a_sk) = box_::gen_keypair(); - let (b_pk, b_sk) = box_::gen_keypair(); - let nonce = box_::gen_nonce(); + let a_sk = SecretKey::generate(&mut OsRng); + let a_pk = a_sk.public_key(); + + let b_sk = SecretKey::generate(&mut OsRng); + let b_pk = b_sk.public_key(); + + let nonce = SalsaBox::generate_nonce(&mut OsRng); + + let a_box = SalsaBox::new(&b_pk, &a_sk); + + let box_data = a_box + .encrypt( + &nonce, + Payload::from([/* data */ 1, 2, 42 /* no padding */].as_ref()), + ) + .expect("Failed to encrypt data"); let msg = IncomingMessage { from: "AAAAAAAA".into(), to: "*BBBBBBB".into(), message_id: "00112233".into(), date: 0, - nonce: nonce.0.to_vec(), - box_data: box_::seal( - &[/* data */ 1, 2, 42 /* no padding */], - &nonce, - &b_pk, - &a_sk, - ), + nonce: nonce.to_vec(), + box_data, nickname: None, }; diff --git a/src/types.rs b/src/types.rs index 4099641b8..28d4a7dc6 100644 --- a/src/types.rs +++ b/src/types.rs @@ -40,9 +40,10 @@ impl From for u8 { /// The rendering type influences how a file message is displayed on the device /// of the recipient. -#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)] pub enum RenderingType { /// Display as default file message + #[default] File, /// Display as media file message (e.g. image or audio message) Media, @@ -66,12 +67,6 @@ impl Serialize for RenderingType { } } -impl Default for RenderingType { - fn default() -> Self { - RenderingType::File - } -} - /// A file message. #[derive(Debug, Serialize)] pub struct FileMessage { @@ -88,7 +83,6 @@ pub struct FileMessage { thumbnail_media_type: Option, #[serde(rename = "k")] - #[serde(serialize_with = "key_to_hex")] blob_encryption_key: Key, #[serde(rename = "n")] @@ -397,10 +391,6 @@ impl Serialize for BlobId { } } -fn key_to_hex(val: &Key, serializer: S) -> Result { - serializer.serialize_str(&HEXLOWER.encode(&val.0)) -} - #[cfg(test)] mod test { use std::collections::HashMap; @@ -425,7 +415,7 @@ mod test { #[test] fn test_serialize_to_string_minimal() { - let pk = Key([ + let key = Key::from([ 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, ]); @@ -434,7 +424,7 @@ mod test { file_media_type: "application/pdf".parse().unwrap(), thumbnail_blob_id: None, thumbnail_media_type: None, - blob_encryption_key: pk, + blob_encryption_key: key, file_name: None, file_size_bytes: 2048, description: None, @@ -465,7 +455,7 @@ mod test { #[test] fn test_serialize_to_string_full() { - let pk = Key([ + let key = Key::from([ 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, ]); @@ -474,7 +464,7 @@ mod test { file_media_type: "application/pdf".parse().unwrap(), thumbnail_blob_id: Some(BlobId::from_str("abcdef0123456789abcdef0123456789").unwrap()), thumbnail_media_type: Some("image/jpeg".parse().unwrap()), - blob_encryption_key: pk, + blob_encryption_key: key, file_name: Some("secret.pdf".into()), file_size_bytes: 2048, description: Some("This is a fancy file".into()), @@ -518,13 +508,14 @@ mod test { #[test] fn test_builder() { - let key = Key([ + let key_bytes = [ 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, - ]); + ]; + let key: Key = key_bytes.into(); let file_blob_id = BlobId::from_str("0123456789abcdef0123456789abcdef").unwrap(); let thumb_blob_id = BlobId::from_str("abcdef0123456789abcdef0123456789").unwrap(); - let msg = FileMessage::builder(file_blob_id.clone(), key.clone(), "image/jpeg", 2048) + let msg = FileMessage::builder(file_blob_id.clone(), key, "image/jpeg", 2048) .thumbnail(thumb_blob_id.clone(), "image/png") .file_name("hello.jpg") .description(String::from("An image file")) @@ -536,7 +527,7 @@ mod test { assert_eq!(msg.file_media_type, "image/jpeg"); assert_eq!(msg.thumbnail_blob_id, Some(thumb_blob_id)); assert_eq!(msg.thumbnail_media_type, Some("image/png".into())); - assert_eq!(msg.blob_encryption_key, key); + assert_eq!(&msg.blob_encryption_key.as_ref()[..], key_bytes); assert_eq!(msg.file_name, Some("hello.jpg".to_string())); assert_eq!(msg.file_size_bytes, 2048); assert_eq!(msg.description, Some("An image file".to_string()));