Skip to content

Commit

Permalink
Merge pull request dbrgn#49 from dbrgn/receive
Browse files Browse the repository at this point in the history
Support incoming messages
  • Loading branch information
dbrgn authored Apr 2, 2021
2 parents a909d0b + 1b44202 commit 304d316
Show file tree
Hide file tree
Showing 15 changed files with 461 additions and 49 deletions.
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ references:
command: |
export PKG_CONFIG_PATH=/opt/libsodium/lib/pkgconfig:$PKG_CONFIG_PATH
export LD_LIBRARY_PATH=/opt/libsodium/lib:$LD_LIBRARY_PATH
cargo update && cargo build && cargo test
cargo update && cargo build --all-targets && cargo build --all-targets --all-features && cargo test --all-features
- save_cache:
key: v3-{{ .Environment.CIRCLE_JOB }}-cargo-cache-{{ checksum "Cargo.toml" }}
paths:
Expand Down
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,18 @@ Possible log types:
- `[security]` to invite users to upgrade in case of vulnerabilities.


### Unreleased

- [added] New `IncomingMessage` type for parsing and decrypting incoming messages
- [added] Validate MAC for incoming messages
- [added] Derive `PartialEq` and `Clone` for most error types
- [added] New `to_hex_string` method on `RecipientKey`
- [removed] Removed `Into<String>` impl for `RecipientKey`, use `to_hex_string` method instead
- [changed] The `lookup_pubkey` function now returns a `PublicKey`, not a `String`
- [changed] `CryptoError` type has three new error variants: `BadNonce`, `BadPadding` and
`DecryptionFailed`
- [changed] `ApiError` type has one new error variant: `InvalidMac`

### v0.14.1 (2021-03-27)

- [added] New helper function `encrypt_file_data` for encrypting the data and
Expand Down
5 changes: 4 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,20 @@ include = [
edition = "2018"

[features]
dev = []
default = ["receive"]
receive = ["form_urlencoded", "serde_urlencoded"] # Support for receiving and decrypting incoming messages

[dependencies]
byteorder = "1.0"
data-encoding = "2.1"
form_urlencoded = { version = "1", optional = true }
log = "0.4"
mime = "0.3"
thiserror = "1"
reqwest = { version = "0.11", features = ["rustls-tls-native-roots", "multipart"], default-features = false }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_urlencoded = { version = "0.7", optional = true }
sodiumoxide = "0.2.0"

[dev-dependencies]
Expand Down
17 changes: 15 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,10 @@ client library in Rust. For implementation status, see feature list below.

**Receiving**

- [ ] Verify MAC of incoming message
- [ ] Decrypt incoming message
- [x] Decode incoming request body
- [x] Verify MAC of incoming message
- [x] Decrypt incoming message
- [ ] Decode incoming message

**Files**

Expand Down Expand Up @@ -77,6 +79,17 @@ Look up Threema ID by email hash:

cargo run --example lookup_id -- by_email_hash <from> <secret> 1ea093239cc5f0e1b6ec81b866265b921f26dc4033025410063309f4d1a8ee2c

Decode and decrypt an incoming message payload:

cargo run --example receive -- <our-id> <secret> <private-key> <request-body>


## Cargo Features

This library offers the following optional features:

- `receive`: Add support for processing incoming messages. Enabled by default.


## Rust Version Requirements (MSRV)

Expand Down
8 changes: 6 additions & 2 deletions examples/lookup_pubkey.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use docopt::Docopt;
use threema_gateway::ApiBuilder;
use threema_gateway::{ApiBuilder, RecipientKey};

const USAGE: &str = "
Usage: lookup_pubkey [options] <our_id> <secret> <their_id>
Expand All @@ -25,7 +25,11 @@ async fn main() {

// Show result
match pubkey {
Ok(pk) => println!("Public key for {} is {}.", their_id, pk),
Ok(pk) => println!(
"Public key for {} is {}.",
their_id,
RecipientKey::from(pk).to_hex_string()
),
Err(e) => println!("Could not fetch public key: {}", e),
}
}
68 changes: 68 additions & 0 deletions examples/receive.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
use data_encoding::HEXLOWER_PERMISSIVE;
use docopt::Docopt;
use threema_gateway::{ApiBuilder, SecretKey};

const USAGE: &str = "
Usage: receive [options] <our-id> <secret> <private-key> <request-body>
Options:
-h, --help Show this help
";

#[tokio::main(flavor = "current_thread")]
async fn main() {
let args = Docopt::new(USAGE)
.and_then(|docopt| docopt.parse())
.unwrap_or_else(|e| e.exit());

// Command line arguments
let our_id = args.get_str("<our-id>");
let secret = args.get_str("<secret>");
let private_key = HEXLOWER_PERMISSIVE
.decode(args.get_str("<private-key>").as_bytes())
.ok()
.and_then(|bytes| SecretKey::from_slice(&bytes))
.unwrap_or_else(|| {
eprintln!("Invalid private key");
std::process::exit(1);
});
let request_body = args.get_str("<request-body>");

// Create E2eApi instance
let api = ApiBuilder::new(our_id, secret)
.with_private_key_bytes(private_key.as_ref())
.and_then(|builder| builder.into_e2e())
.unwrap();

// Parse request body
let msg = api
.decode_incoming_message(request_body)
.unwrap_or_else(|e| {
eprintln!("Could not decode incoming message: {}", e);
std::process::exit(1);
});

println!("Parsed and validated message from request:");
println!(" From: {}", msg.from);
println!(" To: {}", msg.to);
println!(" Message ID: {}", msg.message_id);
println!(" Timestamp: {}", msg.date);
println!(" Sender nickname: {:?}", msg.nickname);

// Fetch sender public key
let pubkey = api.lookup_pubkey(&msg.from).await.unwrap_or_else(|e| {
eprintln!("Could not fetch public key for {}: {}", &msg.from, e);
std::process::exit(1);
});

// Decrypt
let data = api
.decrypt_incoming_message(&msg, &pubkey)
.unwrap_or_else(|e| {
println!("Could not decrypt box: {}", e);
std::process::exit(1);
});

// Show result
println!("Decrypted box: {:?}", data);
}
5 changes: 2 additions & 3 deletions examples/send_e2e_file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use std::path::Path;
use std::process;

use docopt::Docopt;
use threema_gateway::{encrypt_file_data, ApiBuilder, FileMessage, RecipientKey, RenderingType};
use threema_gateway::{encrypt_file_data, ApiBuilder, FileMessage, RenderingType};

const USAGE: &str = "
Usage: send_e2e_file [options] <from> <to> <secret> <private-key> <path-to-file> [<path-to-thumbnail>]
Expand Down Expand Up @@ -58,7 +58,6 @@ async fn main() {
// Fetch public key
// Note: In a real application, you should cache the public key
let public_key = etry!(api.lookup_pubkey(to).await, "Could not fetch public key");
let recipient_key: RecipientKey = etry!(public_key.parse(), "Error");

// Read files
let mut file = etry!(File::open(filepath), "Could not open file");
Expand Down Expand Up @@ -108,7 +107,7 @@ async fn main() {
.rendering_type(RenderingType::File)
.build()
.expect("Could not build FileMessage");
let encrypted = api.encrypt_file_msg(&msg, &recipient_key);
let encrypted = api.encrypt_file_msg(&msg, &public_key.into());

// Send
let msg_id = api.send(&to, &encrypted, false).await;
Expand Down
5 changes: 1 addition & 4 deletions examples/send_e2e_image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,7 @@ async fn main() {
println!("Could not fetch public key: {}", e);
process::exit(1);
});
let recipient_key: RecipientKey = public_key.parse().unwrap_or_else(|e| {
println!("{}", e);
process::exit(1);
});
let recipient_key: RecipientKey = public_key.into();

// Encrypt image
let mut file = File::open(path).unwrap_or_else(|e| {
Expand Down
8 changes: 2 additions & 6 deletions examples/send_e2e_text.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::process;

use docopt::Docopt;
use threema_gateway::{ApiBuilder, RecipientKey};
use threema_gateway::ApiBuilder;

const USAGE: &str = "
Usage: send_e2e_text [options] <from> <to> <secret> <private-key> <text>...
Expand Down Expand Up @@ -37,11 +37,7 @@ async fn main() {
});

// Encrypt and send
let recipient_key: RecipientKey = public_key.parse().unwrap_or_else(|e| {
println!("{}", e);
process::exit(1);
});
let encrypted = api.encrypt_text_msg(&text, &recipient_key);
let encrypted = api.encrypt_text_msg(&text, &public_key.into());
let msg_id = api.send(&to, &encrypted, false).await;

match msg_id {
Expand Down
38 changes: 32 additions & 6 deletions src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,19 @@ use std::{

use data_encoding::HEXLOWER_PERMISSIVE;
use reqwest::Client;
use sodiumoxide::crypto::box_::PublicKey;

use crate::{
connection::{blob_upload, send_e2e, send_simple, Recipient},
crypto::{
encrypt, encrypt_file_msg, encrypt_image_msg, encrypt_raw, EncryptedMessage, RecipientKey,
},
errors::{ApiBuilderError, ApiError},
errors::{ApiBuilderError, ApiError, CryptoError},
lookup::{
lookup_capabilities, lookup_credits, lookup_id, lookup_pubkey, Capabilities,
LookupCriterion,
},
receive::IncomingMessage,
types::{BlobId, FileMessage, MessageType},
SecretKey, MSGAPI_URL,
};
Expand All @@ -33,7 +35,7 @@ macro_rules! impl_common_functionality {
///
/// It is strongly recommended that you cache the public keys to avoid querying
/// the API for each message.
pub async fn lookup_pubkey(&self, id: &str) -> Result<String, ApiError> {
pub async fn lookup_pubkey(&self, id: &str) -> Result<PublicKey, ApiError> {
lookup_pubkey(
&self.client,
self.endpoint.borrow(),
Expand Down Expand Up @@ -349,6 +351,28 @@ impl E2eApi {
)
.await
}

/// Deserialize an incoming Threema Gateway message in
/// `application/x-www-form-urlencoded` format.
///
/// This will validate the MAC. If the MAC is invalid,
/// [`ApiError::InvalidMac`] will be returned.
pub fn decode_incoming_message(
&self,
bytes: impl AsRef<[u8]>,
) -> Result<IncomingMessage, ApiError> {
IncomingMessage::from_urlencoded_bytes(bytes, &self.secret)
}

/// Decrypt an [`IncomingMessage`] using the provided public key and our
/// own private key.
pub fn decrypt_incoming_message(
&self,
message: &IncomingMessage,
public_key: &PublicKey,
) -> Result<Vec<u8>, CryptoError> {
message.decrypt_box(public_key, &self.private_key)
}
}

/// A convenient way to set up the API object.
Expand All @@ -361,9 +385,9 @@ impl E2eApi {
/// use threema_gateway::{ApiBuilder, SimpleApi};
///
/// let gateway_id = "*3MAGWID";
/// let gateway_secret = "hihghrg98h00ghrg";
/// let api_secret = "hihghrg98h00ghrg";
///
/// let api: SimpleApi = ApiBuilder::new(gateway_id, gateway_secret).into_simple();
/// let api: SimpleApi = ApiBuilder::new(gateway_id, api_secret).into_simple();
/// ```
///
/// ## E2E API
Expand All @@ -372,10 +396,10 @@ impl E2eApi {
/// use threema_gateway::{ApiBuilder, E2eApi};
///
/// let gateway_id = "*3MAGWID";
/// let gateway_secret = "hihghrg98h00ghrg";
/// let api_secret = "hihghrg98h00ghrg";
/// let private_key = "998730fbcac1c57dbb181139de41d12835b3fae6af6acdf6ce91670262e88453";
///
/// let api: E2eApi = ApiBuilder::new(gateway_id, gateway_secret)
/// let api: E2eApi = ApiBuilder::new(gateway_id, api_secret)
/// .with_private_key_str(private_key)
/// .and_then(|builder| builder.into_e2e())
/// .unwrap();
Expand Down Expand Up @@ -459,6 +483,8 @@ impl ApiBuilder {
}

/// Return a [`E2eAPI`](struct.SimpleApi.html) instance.
///
/// This will fail if no private key was set.
pub fn into_e2e(self) -> Result<E2eApi, ApiBuilderError> {
match self.private_key {
Some(key) => Ok(E2eApi::new(
Expand Down
17 changes: 8 additions & 9 deletions src/crypto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,6 @@ impl From<[u8; 32]> for RecipientKey {
}
}

impl Into<String> for RecipientKey {
/// Encode the key bytes as lowercase hex string.
fn into(self) -> String {
HEXLOWER.encode(&(self.0).0)
}
}

impl RecipientKey {
/// Create a `RecipientKey` from a byte slice. It must contain 32 bytes.
pub fn from_bytes(val: &[u8]) -> Result<Self, CryptoError> {
Expand All @@ -69,6 +62,11 @@ impl RecipientKey {
pub fn as_bytes(&self) -> &[u8] {
&(self.0).0
}

/// Encode the key bytes as lowercase hex string.
pub fn to_hex_string(&self) -> String {
HEXLOWER.encode(&(self.0).0)
}
}

impl FromStr for RecipientKey {
Expand Down Expand Up @@ -99,6 +97,8 @@ pub fn encrypt_raw(
}

/// Encrypt a message for the recipient.
///
/// The encrypted data will include PKCS#7 style random padding.
pub fn encrypt(
data: &[u8],
msgtype: MessageType,
Expand Down Expand Up @@ -320,9 +320,8 @@ mod test {
bytes[0] = 0xff;
bytes[31] = 0xee;
let recipient = RecipientKey::from_bytes(&bytes).unwrap();
let string: String = recipient.into();
assert_eq!(
string,
recipient.to_hex_string(),
"ff000000000000000000000000000000000000000000000000000000000000ee"
);
}
Expand Down
Loading

0 comments on commit 304d316

Please sign in to comment.