threema_client/
blob_api.rs

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
12// in the java code, file nonce, photo nonce, ... are all the same
13const 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        // certificate validation failed. For now disable checking. TODO Think about it
81        // This is not a big problem, as the file is encrypted and authenticated anyway.
82        //
83        // TODO always limit response size!
84
85        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        // Download 
98        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}