1use sodiumoxide::crypto::secretbox::xsalsa20poly1305 as secretbox;
2pub use secretbox::Key as BlobKey;
3
4use std::convert::TryInto;
5use reqwest;
6use hex;
7
8use thiserror;
9
10pub type BlobId = [u8; 16];
11
12const BLOB_NONCE: [u8;24] = [0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01];
14const THUMBNAIL_NONCE: [u8;24] = [0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02];
15const UPLOAD_URL: &str = "https://upload.blob.threema.ch/upload";
16
17#[derive(Debug, thiserror::Error)]
18pub enum InvalidBlob {
19 #[error("Blob server sent too many bytes")]
20 TooLarge,
21 #[error("Blob server sent too few bytes")]
22 TooSmallDownload,
23 #[error("Size in Blobref is too small for encrypted data")]
24 TooSmallSize,
25 #[error("Validation failed")]
26 ValidationFailed,
27 #[error("HTTP request unsuccessfull for {0}: {1}")]
28 HttpError(String, reqwest::StatusCode),
29}
30
31#[derive(Debug, thiserror::Error)]
32pub enum ParseError {
33 #[error("Hex decoding Failed")]
34 HexFailed,
35 #[error("wrong size")]
36 Size,
37}
38
39#[derive(Debug, Clone)]
40pub struct BlobRef {
41 pub id: BlobId,
42 pub size: u32,
43 pub key: BlobKey,
44}
45
46
47impl BlobRef {
48 const SIZE: usize = 52;
49 pub fn from_slice(data: &[u8]) -> Self{
50 let id = data[0..16].try_into().unwrap();
51 let size = u32::from_le_bytes(data[16..20].try_into().unwrap());
52 let key = BlobKey::from_slice(&data[20..52]).unwrap();
53 return BlobRef{id, size, key};
54 }
55 pub fn from_hex(data: &str) -> Result<Self, ParseError>{
56 let buf = hex::decode(data).map_err(|_| ParseError::HexFailed)?;
57 if buf.len() != Self::SIZE {
58 return Err(ParseError::Size);
59 }
60 return Ok(Self::from_slice(&buf))
61 }
62 pub fn to_slice(&self, out: &mut [u8]){
63 out[0..16].clone_from_slice(&self.id);
64 out[16..20].clone_from_slice(&self.size.to_le_bytes());
65 out[20..52].clone_from_slice(self.key.as_ref());
66 }
67 pub fn hex(&self) -> String {
68 let mut buf = [0; Self::SIZE];
69 self.to_slice(&mut buf);
70 return hex::encode(&buf);
71 }
72}
73
74pub struct Client{
75 http: reqwest::Client,
76}
77impl Client{
78
79 pub fn new() -> Self{
80 let http = reqwest::Client::builder()
86 .danger_accept_invalid_certs(true)
87 .user_agent("Threema ist cool. (https://github.com/TheJonny/threema-rs)")
88 .build().expect("HTTP Client creation failed");
89 Client{http}
90 }
91
92 pub async fn download(&self, blobref: &BlobRef) -> Result<Vec<u8>, anyhow::Error>{
93 let size = blobref.size as usize;
94 let id_hex = hex::encode(&blobref.id);
95 let url = format!("https://{}.blob.threema.ch/{}", &id_hex[0..2], id_hex);
96
97 let mut res = self.http.get(&url).send().await?;
99 if !res.status().is_success() {
100 return Err(InvalidBlob::HttpError(url, res.status()).into());
101 }
102 let mut buf = Vec::<u8>::with_capacity(size);
103 while let Some(b) = res.chunk().await? {
104 if b.len() + buf.len() > size {
105 return Err(InvalidBlob::TooLarge.into());
106 }
107 buf.extend_from_slice(&b);
108 }
109 if buf.len() != size {
110 return Err(InvalidBlob::TooSmallDownload.into());
111 }
112 log::trace!("downloaded {} bytes for blobref {:?}", buf.len(), blobref);
113
114 if size < secretbox::NONCEBYTES + secretbox::MACBYTES {
115 return Err(InvalidBlob::TooSmallSize.into());
116 }
117 let nonce = secretbox::Nonce::from_slice(&BLOB_NONCE).unwrap();
118
119 Ok(secretbox::open(&buf, &nonce, &blobref.key).map_err(|_| InvalidBlob::ValidationFailed)?)
120 }
121 pub async fn mark_done(&self, blobid: &BlobId) -> Result<(), anyhow::Error>{
122 let id_hex = hex::encode(&blobid);
123 let url = format!("https://{}.blob.threema.ch/{}/done", &id_hex[0..2], id_hex);
124 let res = self.http.post(&url).send().await?;
125 if !res.status().is_success() {
126 return Err(InvalidBlob::HttpError(url, res.status()).into());
127 }
128 Ok(())
129 }
130
131 pub async fn upload(&self, plainblob: &[u8]) -> anyhow::Result<BlobRef> {
132 if plainblob.len() + secretbox::MACBYTES > u32::MAX as usize {
133 return Err(InvalidBlob::TooLarge.into());
134 }
135 let key = secretbox::gen_key();
136 let nonce = secretbox::Nonce::from_slice(&BLOB_NONCE).unwrap();
137 let enc = secretbox::seal(plainblob, &nonce, &key);
138 let length = enc.len() as u64;
139 let bodypart = reqwest::multipart::Part::stream_with_length(enc, length)
140 .file_name("blob.bin");
141 let body = reqwest::multipart::Form::new()
142 .part("blob", bodypart);
143
144
145 let res = self.http.post(UPLOAD_URL).multipart(body).send().await?;
146 let id_hex = res.text().await?;
147 let id_vec = hex::decode(&id_hex).map_err(|_| ParseError::HexFailed)?;
148 let id: BlobId = id_vec.try_into().map_err(|_| ParseError::Size)?;
149
150 return Ok(BlobRef{id, key, size: length as u32});
151 }
152}