Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support comments #13

Merged
merged 13 commits into from
Apr 26, 2024
Merged
2 changes: 1 addition & 1 deletion src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ impl API {
let status: u32 = value.get("status").unwrap().as_u64().unwrap() as u32;

match status {
0 => Ok(value.try_into()?),
0 => Ok(serde_json::from_value(value)?),
1 => Err(PasteError::PasteNotFound),
s => Err(PasteError::UnknownPasteStatus(s)),
}
Expand Down
60 changes: 36 additions & 24 deletions src/crypto.rs
Original file line number Diff line number Diff line change
@@ -1,35 +1,52 @@
use crate::error::{PasteError, PbResult};
use crate::privatebin::{DecryptedPaste, Paste};
use crate::privatebin::Cipher;
use aes_gcm::aead::{Aead, NewAead};
use aes_gcm::{Key, Nonce};

/// Trait implemented by any decrypt-able type (paste or comment)
pub trait Decryptable {
/// Get ciphertext.
/// We prefer to borrow this and not copy, because ct may be large.
fn get_ct(&self) -> &str;
/// Additional authenticated (but not encrypted) data.
/// Sensitive to formatting changes.
fn get_adata_str(&self) -> String;
/// Cipher parameters
fn get_cipher(&self) -> &Cipher;
}

fn derive_key(iterations: std::num::NonZeroU32, salt: &[u8], key: &[u8], out: &mut [u8]) {
ring::pbkdf2::derive(ring::pbkdf2::PBKDF2_HMAC_SHA256, iterations, salt, key, out);
}

pub fn decrypt_with_password(
paste: &Paste,
/// Decrypt decryptable, then attempt deserialize to requested type (DecryptedT)
pub fn decrypt_with_password<DecryptedT: serde::de::DeserializeOwned>(
decryptable: &impl Decryptable,
key: &[u8],
password: &str,
) -> PbResult<DecryptedPaste> {
let cipher_algo = &paste.adata.cipher.cipher_algo;
let cipher_mode = &paste.adata.cipher.cipher_mode;
let kdf_keysize = paste.adata.cipher.kdf_keysize;
) -> PbResult<DecryptedT> {
let cipher_algo = &decryptable.get_cipher().cipher_algo;
let cipher_mode = &decryptable.get_cipher().cipher_mode;
let kdf_keysize = decryptable.get_cipher().kdf_keysize;

let salt = base64::decode(&paste.adata.cipher.kdf_salt)?;
let iterations = std::num::NonZeroU32::new(paste.adata.cipher.kdf_iterations).unwrap();
let salt = base64::decode(&decryptable.get_cipher().kdf_salt)?;
let iterations = std::num::NonZeroU32::new(decryptable.get_cipher().kdf_iterations).unwrap();

let key = [key, password.as_bytes()].concat();

let mut derived_key = [0u8; 32];
derive_key(iterations, &salt, &key, &mut derived_key);

match (&cipher_algo[..], &cipher_mode[..], kdf_keysize) {
("aes", "gcm", 256) => decrypt_aes_256_gcm(paste, &derived_key),
("aes", "gcm", 256) => {
let data = decrypt_aes_256_gcm(decryptable, &derived_key)?;
let value: serde_json::Value = serde_json::from_slice(&data)?;
Ok(serde_json::from_value(value)?)
}
_ => Err(PasteError::CipherNotImplemented {
cipher_mode: paste.adata.cipher.cipher_mode.clone(),
cipher_algo: paste.adata.cipher.cipher_algo.clone(),
keysize: paste.adata.cipher.kdf_keysize,
cipher_mode: decryptable.get_cipher().cipher_mode.clone(),
cipher_algo: decryptable.get_cipher().cipher_algo.clone(),
keysize: decryptable.get_cipher().kdf_keysize,
}),
}
}
Expand Down Expand Up @@ -66,23 +83,18 @@ pub fn encrypt(
Ok(encrypted_data)
}

fn convert_to_decrypted_paste(data: &[u8]) -> PbResult<DecryptedPaste> {
let value: serde_json::Value = serde_json::from_slice(data)?;
Ok(serde_json::from_value(value)?)
}

fn decrypt_aes_256_gcm(paste: &Paste, derived_key: &[u8]) -> PbResult<DecryptedPaste> {
fn decrypt_aes_256_gcm(decryptable: &impl Decryptable, derived_key: &[u8]) -> PbResult<Vec<u8>> {
type Cipher = aes_gcm::AesGcm<aes_gcm::aes::Aes256, typenum::U16>;
let ciphertext = base64::decode(&paste.ct)?;
let nonce = base64::decode(&paste.adata.cipher.cipher_iv)?;
let ciphertext = base64::decode(decryptable.get_ct())?;
let nonce = base64::decode(&decryptable.get_cipher().cipher_iv)?;

let cipher = Cipher::new(Key::from_slice(derived_key));
let adata_str = decryptable.get_adata_str();
let payload = aes_gcm::aead::Payload {
msg: &ciphertext,
aad: paste.adata_str.as_bytes(),
aad: adata_str.as_bytes(),
};
let data = cipher.decrypt(Nonce::from_slice(&nonce), payload)?;
let decompressed = miniz_oxide::inflate::decompress_to_vec(&data)?;

convert_to_decrypted_paste(&decompressed)
Ok(decompressed)
}
111 changes: 93 additions & 18 deletions src/privatebin.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
use crate::crypto::Decryptable;
use crate::error::PbResult;
use serde::ser::{SerializeTuple, Serializer};
use serde::Deserialize;
use serde::Serialize;
use serde_json::Value;
use serde_with::skip_serializing_none;
use std::io::ErrorKind;
use url::Url;

#[derive(Deserialize, Debug, Serialize)]
Expand Down Expand Up @@ -36,25 +36,60 @@ pub struct Paste {
pub ct: String,
pub meta: Meta,
pub adata: Data,
pub comments: Option<Vec<Comment>>,
}

impl Decryptable for Paste {
fn get_ct(&self) -> &str {
&self.ct
}
fn get_cipher(&self) -> &Cipher {
&self.adata.cipher
}
fn get_adata_str(&self) -> String {
serde_json::to_string(&self.adata).unwrap()
}
}

#[derive(Deserialize, Debug, Serialize)]
pub struct Comment {
pub status: Option<i32>,
pub id: String,
pub pasteid: String,
pub parentid: String,
pub url: Option<String>,
pub v: i32,
pub ct: String,
pub meta: Meta,
pub adata: Cipher,
}

#[serde(skip)]
pub adata_str: String,
impl Decryptable for Comment {
fn get_ct(&self) -> &str {
&self.ct
}
fn get_cipher(&self) -> &Cipher {
&self.adata
}
fn get_adata_str(&self) -> String {
serde_json::to_string(&self.adata).unwrap()
}
}

#[derive(Deserialize, Debug, Serialize)]
pub struct Meta {
pub created: i32,
}

#[derive(Deserialize, Debug, Serialize)]
#[derive(Deserialize, Debug)]
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did we remove Serialize here and implement it ourselves?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Normal serde serialisation results in a JSON object { .. }. But Privatebin expects data (adata) to be a JSON array [ .. ].

PBCLI currently stores adata_str as a separate property in paste struct. This was required because direct serialisation of data outputs a JSON object string, which is not the original adata string (which is array).

By using this custom serialisation, we get a json array string identical to the original adata string.

With this, we can do away with adata_str field too, and the custom TryFrom<serde_json::Value> for Paste which sets it.

Copy link
Contributor Author

@nain-F49FF806 nain-F49FF806 Apr 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a demonstration, you can see we haven't added a TryFrom for comment. So adata_str value remains "". Line84 is sufficient to reconstruct the correct adata string because of this custom array serializer.

I was going to propose this way of handling adata, and removal of adata_str field in another PR. But now maybe we can discuss this here itself.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested patch, using custom serialize.

diff --git a/src/api.rs b/src/api.rs
index e41d033..c2b3d52 100644
--- a/src/api.rs
+++ b/src/api.rs
@@ -95,7 +95,7 @@ impl API {
         let status: u32 = value.get("status").unwrap().as_u64().unwrap() as u32;
 
         match status {
-            0 => Ok(value.try_into()?),
+            0 => Ok(serde_json::from_value(value)?),
             1 => Err(PasteError::PasteNotFound),
             s => Err(PasteError::UnknownPasteStatus(s)),
         }
diff --git a/src/privatebin.rs b/src/privatebin.rs
index 90fdb1f..eb00b03 100644
--- a/src/privatebin.rs
+++ b/src/privatebin.rs
@@ -3,9 +3,7 @@ use crate::error::PbResult;
 use serde::ser::{SerializeTuple, Serializer};
 use serde::Deserialize;
 use serde::Serialize;
-use serde_json::Value;
 use serde_with::skip_serializing_none;
-use std::io::ErrorKind;
 use url::Url;
 
 #[derive(Deserialize, Debug, Serialize)]
@@ -39,9 +37,6 @@ pub struct Paste {
     pub meta: Meta,
     pub adata: Data,
     pub comments: Option<Vec<Comment>>,
-
-    #[serde(skip)]
-    pub adata_str: String,
 }
 
 impl<'a> Decryptable<'a> for Paste {
@@ -52,7 +47,7 @@ impl<'a> Decryptable<'a> for Paste {
         &self.adata.cipher
     }
     fn get_adata_str(&self) -> String {
-        self.adata_str.clone()
+        serde_json::to_string(&self.adata).unwrap()
     }
 }
 
@@ -67,9 +62,6 @@ pub struct Comment {
     pub ct: String,
     pub meta: Meta,
     pub adata: Cipher,
-
-    #[serde(skip)]
-    pub adata_str: String,
 }
 
 impl<'a> Decryptable<'a> for Comment {
@@ -80,11 +72,7 @@ impl<'a> Decryptable<'a> for Comment {
         &self.adata
     }
     fn get_adata_str(&self) -> String {
-        if self.adata_str.is_empty() {
-            serde_json::to_string(&self.adata).unwrap()
-        } else {
-            self.adata_str.clone()
-        }
+        serde_json::to_string(&self.adata).unwrap()
     }
 }
 
@@ -229,21 +217,3 @@ impl Serialize for Cipher {
         s.end()
     }
 }
-
-impl TryFrom<serde_json::Value> for Paste {
-    type Error = crate::error::PbError;
-
-    fn try_from(value: Value) -> PbResult<Self> {
-        let adata = value.get("adata").ok_or(std::io::Error::new(
-            ErrorKind::InvalidData,
-            "Cannot get adata in try_from",
-        ))?;
-        let adata_str = serde_json::to_string(adata)?;
-
-        let mut paste = serde_json::from_value::<Paste>(value)?;
-
-        paste.adata_str = adata_str;
-
-        Ok(paste)
-    }
-}

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey,

yea - with the removal of adata_str that makes a lot more sense now :), and I like the idea.

Do you want to add that change as a commit in this PR?

In addition, did you see the lifetime comment I made above?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In addition, did you see the lifetime comment I made above?

I saw it only now, sorry for missing it earlier. Didn't realise the compiler would figure it out, so I started explicit, and then just carried on. I'll remove them :)

Do you want to add that change as a commit in this PR?

Sure, I can it here, unless you want it as separate PR. If so, let me know.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I saw it only now, sorry for missing it earlier. Didn't realise the compiler would figure it out, so I started explicit, and then just carried on. I'll remove them :)

Cool!

Sure, I can it here, unless you want it as separate PR. If so, let me know.

Yeah, lets add it here, so its coupled together with the added custom serialization. Apart from that it LGTM. When the Lifetime fix is made and the removal of adata_str commit is in, I would proceed to merge it.

Thanks for the contribution!

Best Regards
Mydayyy

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have added commits for both removal of lifetimes and removal of adata_str.

Thanks for the review!

Best regards.

pub struct Data {
pub cipher: Cipher,
pub format: PasteFormat,
pub discuss: i8,
pub burn: i8,
}

#[derive(Deserialize, Debug, Serialize)]
#[derive(Deserialize, Debug)]
nain-F49FF806 marked this conversation as resolved.
Show resolved Hide resolved
pub struct Cipher {
pub cipher_iv: String,
pub kdf_salt: String,
Expand All @@ -75,6 +110,13 @@ pub struct DecryptedPaste {
pub attachment_name: Option<String>,
}

#[skip_serializing_none]
#[derive(Deserialize, Debug, Serialize)]
pub struct DecryptedComment {
pub comment: String,
pub nickname: Option<String>,
}

#[derive(Deserialize, Debug, Serialize, Clone)]
pub struct PostPasteResponse {
pub deletetoken: String,
Expand Down Expand Up @@ -125,20 +167,53 @@ impl Paste {
}
}

impl TryFrom<serde_json::Value> for Paste {
type Error = crate::error::PbError;

fn try_from(value: Value) -> PbResult<Self> {
let adata = value.get("adata").ok_or(std::io::Error::new(
ErrorKind::InvalidData,
"Cannot get adata in try_from",
))?;
let adata_str = serde_json::to_string(adata)?;
impl Comment {
pub fn decrypt(&self, bs58_key: &str) -> PbResult<DecryptedComment> {
self.decrypt_with_password(bs58_key, "")
}

let mut paste = serde_json::from_value::<Paste>(value)?;
pub fn decrypt_with_password(
&self,
bs58_key: &str,
password: &str,
) -> PbResult<DecryptedComment> {
let key = bs58::decode(bs58_key).into_vec()?;
crate::crypto::decrypt_with_password(self, &key, password)
}
}

paste.adata_str = adata_str;
/// Data struct needs to be serialized as an ordered array (not object),
/// so we implement custom serialization.
impl Serialize for Data {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut s = serializer.serialize_tuple(4)?;
s.serialize_element(&self.cipher)?;
s.serialize_element(&self.format)?;
s.serialize_element(&self.discuss)?;
s.serialize_element(&self.burn)?;
s.end()
}
}

Ok(paste)
/// Cipher struct needs to be serialized as an ordered array (not object),
/// so we implement custom serialization.
impl Serialize for Cipher {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut s = serializer.serialize_tuple(8)?;
s.serialize_element(&self.cipher_iv)?;
s.serialize_element(&self.kdf_salt)?;
s.serialize_element(&self.kdf_iterations)?;
s.serialize_element(&self.kdf_keysize)?;
s.serialize_element(&self.cipher_tag_size)?;
s.serialize_element(&self.cipher_algo)?;
s.serialize_element(&self.cipher_mode)?;
s.serialize_element(&self.compression_type)?;
s.end()
}
}
Loading