Skip to content

Commit 570a95f

Browse files
committed
Cleaner API for encrypt_file_data / decrypt_file_data
- Struct for encrypted and unencrypted data - Allow decrypting thumbnails as well, not just files - Symmetric APIs - Add unit test
1 parent 55b3e14 commit 570a95f

File tree

6 files changed

+145
-36
lines changed

6 files changed

+145
-36
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ Possible log types:
1515
### Unreleased
1616

1717
- [added] Support downloading of blobs (#65)
18+
- [added] New `decrypt_file_data` helper function (#67)
1819
- [changed] Remove `mime` dependency in favor of plain strings (#64)
20+
- [changed] The API of `encrypt_file_data` has changed (#67)
1921

2022
### v0.15.1 (2021-12-06)
2123

examples/download_blob.rs

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ use std::process;
22

33
use data_encoding::HEXLOWER_PERMISSIVE;
44
use docopt::Docopt;
5-
use threema_gateway::{ApiBuilder, BlobId};
5+
use threema_gateway::{decrypt_file_data, ApiBuilder, BlobId, EncryptedFileData, Key};
66

77
const USAGE: &str = "
8-
Usage: download_blob [options] <our-id> <secret> <private-key> <blob-id>
8+
Usage: download_blob [options] <our-id> <secret> <private-key> <blob-id> [<blob-key>]
99
1010
Options:
1111
-h, --help Show this help
@@ -28,6 +28,15 @@ async fn main() {
2828
process::exit(1);
2929
}
3030
};
31+
let blob_key_raw = args.get_str("<blob-key>");
32+
let blob_key = if !blob_key_raw.is_empty() {
33+
let bytes = HEXLOWER_PERMISSIVE
34+
.decode(blob_key_raw.as_bytes())
35+
.expect("Invalid blob key");
36+
Some(Key::from_slice(&bytes).expect("Invalid blob key bytes"))
37+
} else {
38+
None
39+
};
3140

3241
// Create E2eApi instance
3342
let api = ApiBuilder::new(our_id, secret)
@@ -37,14 +46,30 @@ async fn main() {
3746

3847
// Download blob
3948
println!("Downloading blob with ID {}...", blob_id);
40-
match api.blob_download(&blob_id).await {
49+
let bytes = match api.blob_download(&blob_id).await {
4150
Err(e) => {
4251
eprintln!("Could not download blob: {}", e);
4352
process::exit(1);
4453
}
4554
Ok(bytes) => {
4655
println!("Downloaded {} blob bytes:", bytes.len());
47-
println!("{}", HEXLOWER_PERMISSIVE.encode(&bytes));
56+
bytes
4857
}
58+
};
59+
if let Some(key) = blob_key {
60+
let decrypted = decrypt_file_data(
61+
&EncryptedFileData {
62+
file: bytes,
63+
thumbnail: None,
64+
},
65+
&key,
66+
)
67+
.expect("Could not decrypt file data");
68+
println!(
69+
"Decrypted bytes: {}",
70+
HEXLOWER_PERMISSIVE.encode(&decrypted.file)
71+
);
72+
} else {
73+
println!("Encrypted bytes: {}", HEXLOWER_PERMISSIVE.encode(&bytes));
4974
}
5075
}

examples/send_e2e_file.rs

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use std::{ffi::OsStr, fs::File, io::Read, path::Path, process};
22

33
use docopt::Docopt;
4-
use threema_gateway::{encrypt_file_data, ApiBuilder, FileMessage, RenderingType};
4+
use threema_gateway::{encrypt_file_data, ApiBuilder, FileData, FileMessage, RenderingType};
55

66
const USAGE: &str = "
77
Usage: send_e2e_file [options] <from> <to> <secret> <private-key> <path-to-file>
@@ -73,9 +73,9 @@ async fn main() {
7373

7474
// Read files
7575
let mut file = etry!(File::open(filepath), "Could not open file");
76-
let mut file_data: Vec<u8> = vec![];
77-
etry!(file.read_to_end(&mut file_data), "Could not read file");
78-
let thumb_data = match thumbpath {
76+
let mut file_bytes: Vec<u8> = vec![];
77+
etry!(file.read_to_end(&mut file_bytes), "Could not read file");
78+
let thumbnail_bytes = match thumbpath {
7979
Some(p) => {
8080
let mut thumb = etry!(File::open(p), format!("Could not open thumbnail {:?}", p));
8181
let mut thumb_data: Vec<u8> = vec![];
@@ -89,15 +89,18 @@ async fn main() {
8989
};
9090

9191
// Encrypt file data
92-
let (encrypted_file, encrypted_thumb, key) =
93-
encrypt_file_data(&file_data, thumb_data.as_deref());
92+
let file_data = FileData {
93+
file: file_bytes,
94+
thumbnail: thumbnail_bytes,
95+
};
96+
let (encrypted, key) = encrypt_file_data(&file_data);
9497

9598
// Upload files to blob server
9699
let file_blob_id = etry!(
97-
api.blob_upload_raw(&encrypted_file, false).await,
100+
api.blob_upload_raw(&encrypted.file, false).await,
98101
"Could not upload file to blob server"
99102
);
100-
let thumb_blob_id = if let Some(et) = encrypted_thumb {
103+
let thumb_blob_id = if let Some(et) = encrypted.thumbnail {
101104
let blob_id = etry!(
102105
api.blob_upload_raw(&et, false).await,
103106
"Could not upload thumbnail to blob server"
@@ -115,7 +118,8 @@ async fn main() {
115118
.first_or_octet_stream()
116119
.to_string();
117120
let file_name = filepath.file_name().and_then(OsStr::to_str);
118-
let msg = FileMessage::builder(file_blob_id, key, file_media_type, file_data.len() as u32)
121+
let file_size_bytes = file_data.file.len() as u32;
122+
let msg = FileMessage::builder(file_blob_id, key, file_media_type, file_size_bytes)
119123
.thumbnail_opt(thumb_blob_id)
120124
.file_name_opt(file_name)
121125
.description_opt(caption)

src/api.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -379,7 +379,9 @@ impl E2eApi {
379379
.await
380380
}
381381

382-
/// Download a blob from the blob server and return the bytes.
382+
/// Download a blob from the blob server and return the encrypted bytes.
383+
///
384+
/// Cost: 0 credits.
383385
pub async fn blob_download(&self, blob_id: &BlobId) -> Result<Vec<u8>, ApiError> {
384386
blob_download(
385387
&self.client,

src/crypto.rs

Lines changed: 96 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -161,14 +161,31 @@ static THUMB_NONCE: secretbox::Nonce = secretbox::Nonce([
161161
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2,
162162
]);
163163

164+
/// Raw unencrypted bytes of a file and optionally a thumbnail.
165+
///
166+
/// This struct is used as a parameter type for [`encrypt_file_data`] and
167+
/// returned by [`decrypt_file_data`].
168+
#[derive(Clone)]
169+
pub struct FileData {
170+
pub file: Vec<u8>,
171+
pub thumbnail: Option<Vec<u8>>,
172+
}
173+
174+
/// Encrypted bytes of a file and optionally a thumbnail.
175+
///
176+
/// This struct is used as a parameter type for [`decrypt_file_data`] and
177+
/// returned by [`encrypt_file_data`].
178+
#[derive(Clone)]
179+
pub struct EncryptedFileData {
180+
pub file: Vec<u8>,
181+
pub thumbnail: Option<Vec<u8>>,
182+
}
183+
164184
/// Encrypt file data and an optional thumbnail using a randomly generated
165185
/// symmetric key.
166186
///
167187
/// Return the encrypted bytes and the key.
168-
pub fn encrypt_file_data(
169-
file_data: &[u8],
170-
thumb_data: Option<&[u8]>,
171-
) -> (Vec<u8>, Option<Vec<u8>>, secretbox::Key) {
188+
pub fn encrypt_file_data(data: &FileData) -> (EncryptedFileData, Key) {
172189
// Make sure to init sodiumoxide library
173190
sodiumoxide::init().unwrap();
174191

@@ -177,23 +194,48 @@ pub fn encrypt_file_data(
177194

178195
// Encrypt data
179196
// Note: Since we generate a random key, we can safely re-use constant nonces.
180-
let encrypted_file = secretbox::seal(file_data, &FILE_NONCE, &key);
181-
let encrypted_thumb = thumb_data.map(|t| secretbox::seal(t, &THUMB_NONCE, &key));
197+
let file = secretbox::seal(&data.file, &FILE_NONCE, &key);
198+
let thumbnail = data
199+
.thumbnail
200+
.as_ref()
201+
.map(|t| secretbox::seal(t, &THUMB_NONCE, &key));
182202

183-
(encrypted_file, encrypted_thumb, key)
203+
(EncryptedFileData { file, thumbnail }, key)
184204
}
185205

186-
pub fn decrypt_file_data(encrypted_data: &[u8], blob_key: &Key) -> Result<Vec<u8>, CryptoError> {
187-
let decrypted_blob = secretbox::open(encrypted_data, &FILE_NONCE, blob_key);
188-
decrypted_blob.map_err(|_| CryptoError::DecryptionFailed)
206+
/// Decrypt file data and optional thumbnail data with the provided symmetric
207+
/// key.
208+
///
209+
/// Return the decrypted bytes.
210+
pub fn decrypt_file_data(
211+
data: &EncryptedFileData,
212+
encryption_key: &Key,
213+
) -> Result<FileData, CryptoError> {
214+
// Make sure to init sodiumoxide library
215+
sodiumoxide::init().unwrap();
216+
217+
let file = secretbox::open(&data.file, &FILE_NONCE, encryption_key)
218+
.map_err(|_| CryptoError::DecryptionFailed)?;
219+
let thumbnail = match data.thumbnail.as_ref() {
220+
Some(t) => {
221+
let decrypted = secretbox::open(t, &THUMB_NONCE, encryption_key)
222+
.map_err(|_| CryptoError::DecryptionFailed)?;
223+
Some(decrypted)
224+
}
225+
None => None,
226+
};
227+
228+
Ok(FileData { file, thumbnail })
189229
}
190230

191231
#[cfg(test)]
192232
mod test {
193233
use std::str::FromStr;
194234

195-
use crate::api::ApiBuilder;
196-
use crate::types::{BlobId, MessageType};
235+
use crate::{
236+
api::ApiBuilder,
237+
types::{BlobId, MessageType},
238+
};
197239
use sodiumoxide::crypto::box_::{self, Nonce, PublicKey, SecretKey};
198240

199241
use super::*;
@@ -336,23 +378,26 @@ mod test {
336378
fn test_encrypt_file_data() {
337379
let file_data = [1, 2, 3, 4];
338380
let thumb_data = [5, 6, 7];
381+
let data = FileData {
382+
file: file_data.to_vec(),
383+
thumbnail: Some(thumb_data.to_vec()),
384+
};
339385

340386
// Encrypt
341-
let (encrypted_file, encrypted_thumb, key) =
342-
encrypt_file_data(&file_data, Some(&thumb_data));
343-
let encrypted_thumb = encrypted_thumb.expect("Thumbnail missing");
387+
let (encrypted, key) = encrypt_file_data(&data);
388+
let encrypted_thumb = encrypted.thumbnail.expect("Thumbnail missing");
344389

345390
// Ensure that encrypted data is different from plaintext data
346-
assert_ne!(encrypted_file, file_data);
391+
assert_ne!(encrypted.file, file_data);
347392
assert_ne!(encrypted_thumb, thumb_data);
348-
assert_eq!(encrypted_file.len(), file_data.len() + secretbox::MACBYTES);
393+
assert_eq!(encrypted.file.len(), file_data.len() + secretbox::MACBYTES);
349394
assert_eq!(
350395
encrypted_thumb.len(),
351396
thumb_data.len() + secretbox::MACBYTES
352397
);
353398

354399
// Test that data can be decrypted
355-
let decrypted_file = secretbox::open(&encrypted_file, &FILE_NONCE, &key).unwrap();
400+
let decrypted_file = secretbox::open(&encrypted.file, &FILE_NONCE, &key).unwrap();
356401
let decrypted_thumb = secretbox::open(&encrypted_thumb, &THUMB_NONCE, &key).unwrap();
357402
assert_eq!(decrypted_file, &file_data);
358403
assert_eq!(decrypted_thumb, &thumb_data);
@@ -361,11 +406,41 @@ mod test {
361406
#[test]
362407
fn test_encrypt_file_data_random_key() {
363408
// Ensure that a different key is generated each time
364-
let (_, _, key1) = encrypt_file_data(&[1, 2, 3], None);
365-
let (_, _, key2) = encrypt_file_data(&[1, 2, 3], None);
366-
let (_, _, key3) = encrypt_file_data(&[1, 2, 3], None);
409+
let (_, key1) = encrypt_file_data(&FileData {
410+
file: [1, 2, 3].to_vec(),
411+
thumbnail: None,
412+
});
413+
let (_, key2) = encrypt_file_data(&FileData {
414+
file: [1, 2, 3].to_vec(),
415+
thumbnail: None,
416+
});
417+
let (_, key3) = encrypt_file_data(&FileData {
418+
file: [1, 2, 3].to_vec(),
419+
thumbnail: None,
420+
});
367421
assert_ne!(key1, key2);
368422
assert_ne!(key2, key3);
369423
assert_ne!(key1, key3);
370424
}
425+
426+
#[test]
427+
fn test_decrypt_file_data() {
428+
let file_data = [1, 2, 3, 4];
429+
let thumb_data = [5, 6, 7];
430+
let data = FileData {
431+
file: file_data.to_vec(),
432+
thumbnail: Some(thumb_data.to_vec()),
433+
};
434+
435+
// Encrypt
436+
let (encrypted, key) = encrypt_file_data(&data);
437+
assert_ne!(encrypted.file, data.file);
438+
assert!(encrypted.thumbnail.is_some());
439+
assert_ne!(encrypted.thumbnail, data.thumbnail);
440+
441+
// Decrypt
442+
let decrypted = decrypt_file_data(&encrypted, &key).unwrap();
443+
assert_eq!(decrypted.file, &file_data);
444+
assert_eq!(decrypted.thumbnail.unwrap(), &thumb_data);
445+
}
371446
}

src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,8 @@ pub use crate::{
9191
api::{ApiBuilder, E2eApi, SimpleApi},
9292
connection::Recipient,
9393
crypto::{
94-
decrypt_file_data, encrypt, encrypt_file_data, encrypt_raw, EncryptedMessage, RecipientKey,
94+
decrypt_file_data, encrypt, encrypt_file_data, encrypt_raw, EncryptedFileData,
95+
EncryptedMessage, FileData, RecipientKey,
9596
},
9697
lookup::{Capabilities, LookupCriterion},
9798
types::{BlobId, FileMessage, FileMessageBuilder, MessageType, RenderingType},

0 commit comments

Comments
 (0)