fitgirl_decrypt/
lib.rs

1#![doc = include_str!("../../README.md")]
2#![cfg_attr(feature = "nightly", feature(doc_cfg))]
3
4mod types;
5pub use types::{Attachment, Cipher, CipherInfo, CompressionType};
6
7mod crypto;
8pub use crypto::decrypt_with_key;
9
10mod error;
11pub use error::Error;
12
13/// [`Paste`] stores pasteid and key.
14#[allow(unused)]
15#[derive(Debug, Clone)]
16pub struct Paste<'a> {
17    key: Vec<u8>,
18    key_base58: &'a str,
19    pasteid: &'a str,
20    base_url: &'a str,
21}
22
23/// Alias of Result with [`Error`] as E.
24pub type Result<T> = std::result::Result<T, Error>;
25
26impl Paste<'_> {
27    /// Parse paste info from an URL.
28    ///
29    /// ```rust
30    /// use fitgirl_decrypt::{Paste, Error};
31    ///
32    /// let url1 = "https://paste.fitgirl-repacks.site/?225484ced69df1d1#SKYwGaZwZmRbN2fR4R9QQJzLTmzpctbDE7kZNpwesRW";
33    /// let url2 = "https://paste.fitgirl-repacks.site/?225484ced69df1d1#kWYCcn3qmpehWMMBmZ1NJciKNA6eXfK6LPzwgGXFdJ";
34    ///
35    /// assert!(Paste::parse_url(url1).is_ok());
36    /// assert!(matches!(Paste::parse_url(url2), Err(Error::KeyLengthMismatch(31))));
37    /// ```
38    pub fn parse_url<'a>(url: &'a str) -> Result<Paste<'a>> {
39        let (base_url, pasteinfo) = url.split_once('?').ok_or(Error::IllFormedURL)?;
40        let (pasteid, key_base58) = pasteinfo.split_once("#").ok_or(Error::IllFormedURL)?;
41
42        let paste = Self::try_from_key_and_pasteid(key_base58, pasteid)?;
43        Ok(Paste { base_url, ..paste })
44    }
45
46    /// Parse paste info from key_base58 (the url segment after '#') and pasteid.
47    ///
48    /// ```rust
49    /// use fitgirl_decrypt::{Paste, Error};
50    ///
51    /// assert!(
52    ///     Paste::try_from_key_and_pasteid(
53    ///         "SKYwGaZwZmRbN2fR4R9QQJzLTmzpctbDE7kZNpwesRW",
54    ///         "",
55    ///     )
56    ///     .is_ok()
57    /// );
58    /// assert!(
59    ///     matches!(
60    ///         Paste::try_from_key_and_pasteid(
61    ///             "kWYCcn3qmpehWMMBmZ1NJciKNA6eXfK6LPzwgGXFdJ",
62    ///             "225484ced69df1d1",
63    ///         ),
64    ///         Err(Error::KeyLengthMismatch(31)),
65    ///     )
66    /// );
67    /// ```
68    pub fn try_from_key_and_pasteid<'a>(
69        key_base58: &'a str,
70        pasteid: &'a str,
71    ) -> Result<Paste<'a>> {
72        let key = bs58::decode(key_base58).into_vec()?;
73
74        if key.len() != 32 {
75            return Err(Error::KeyLengthMismatch(key.len()));
76        }
77
78        let base_url = "https://paste.fitgirl-repacks.site/";
79
80        Ok(Paste {
81            key,
82            key_base58,
83            pasteid,
84            base_url,
85        })
86    }
87
88    /// Decrypt paste from [`CipherInfo`].
89    pub fn decrypt(&self, cipher: impl AsRef<CipherInfo>) -> Result<Attachment> {
90        decrypt_with_key(&self.master_key(), cipher.as_ref())
91    }
92
93    pub fn master_key(&self) -> &Vec<u8> {
94        &self.key
95    }
96
97    /// Get [`CipherInfo`] to decrypt synchronously, with [`ureq`].
98    #[cfg_attr(feature = "nightly", doc(cfg(feature = "ureq")))]
99    #[cfg(feature = "ureq")]
100    pub fn request(&self) -> Result<CipherInfo> {
101        use ureq::{http::header::ACCEPT, Agent};
102
103        let pasteid = self.pasteid;
104        let key_base58 = self.key_base58;
105
106        let base = self.base_url;
107        let init_cookies = format!("{base}?{pasteid}#{key_base58}");
108        let cipher_info = format!("{base}?pasteid={pasteid}");
109
110        let agent = Agent::new_with_defaults();
111
112        agent.get(init_cookies).call()?;
113
114        let resp = agent
115            .get(cipher_info)
116            .header(ACCEPT, "application/json")
117            .call()?
118            .body_mut()
119            .read_json()?;
120        Ok(resp)
121    }
122
123    /// Get [`CipherInfo`] to decrypt asynchronously, with [`reqwest`].
124    ///
125    /// ## NOTE
126    ///
127    /// Because reqwest depends on tokio types, if you are using an async runtime
128    ///
129    /// other than tauri and tokio, try to spawn this inside tokio context.
130    ///
131    /// Also see [async_compat](https://docs.rs/async-compat/latest/async_compat/)
132    #[cfg_attr(feature = "nightly", doc(cfg(feature = "reqwest")))]
133    #[cfg(feature = "reqwest")]
134    pub async fn request_async(&self) -> Result<CipherInfo> {
135        use reqwest::{header::ACCEPT, ClientBuilder};
136
137        let pasteid = self.pasteid;
138        let key_base58 = self.key_base58;
139
140        let base = self.base_url;
141        let init_cookies = format!("{base}?{pasteid}#{key_base58}");
142        let cipher_info = format!("{base}?pasteid={pasteid}");
143
144        let client = ClientBuilder::new().gzip(true).build()?;
145        client.get(init_cookies).send().await?;
146
147        let resp = client
148            .get(cipher_info)
149            .header(ACCEPT, "application/json")
150            .send()
151            .await?
152            .json()
153            .await?;
154        Ok(resp)
155    }
156
157    /// Get [`CipherInfo`] to decrypt asynchronously, with [`nyquest`].
158    ///
159    /// Nyquest can be much more lightweight than [`reqwest`], though it's experimental.
160    #[cfg_attr(feature = "nightly", doc(cfg(feature = "nyquest")))]
161    #[cfg(feature = "nyquest")]
162    pub async fn request_async_ny(&self) -> Result<CipherInfo> {
163        use nyquest::{r#async::Request, ClientBuilder};
164
165        let pasteid = self.pasteid;
166        let key_base58 = self.key_base58;
167
168        let base = self.base_url;
169        let init_cookies = format!("/?{pasteid}#{key_base58}");
170        let cipher_info = format!("/?pasteid={pasteid}");
171
172        let client = ClientBuilder::default()
173            .base_url(base)
174            .build_async()
175            .await?;
176        client.request(Request::get(init_cookies)).await?;
177
178        let resp = client
179            .request(Request::get(cipher_info).with_header("Accept", "application/json"))
180            .await?
181            .json()
182            .await?;
183        Ok(resp)
184    }
185}
186
187/// re-export of base64 for torrent decoding.
188pub use base64;