working prototype minus attachments

This commit is contained in:
winwinner 2023-11-22 05:42:17 +00:00
commit a7c4ab7f93
Signed by: winneratwin
GPG Key ID: F11B4D71AD94CFB8
4 changed files with 1898 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

1671
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

21
Cargo.toml Normal file
View File

@ -0,0 +1,21 @@
[package]
name = "bincli"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
aes-gcm = { version = "0.10.3", features = ["aes"] }
base64 = "0.21.5"
bs58 = "0.5.0"
clap = { version = "4.4.8", features = ["derive"] }
miniz_oxide = "0.7.1"
pbkdf2 = "0.12.2"
rand = "0.8.5"
reqwest = { version = "0.11.22", features = ["blocking", "json"] }
serde = "1.0.193"
serde_json = "1.0.108"
serde_with = "3.4.0"
sha2 = "0.10.8"
typenum = "1.17.0"

205
src/main.rs Normal file
View File

@ -0,0 +1,205 @@
use aes_gcm::aead::Aead;
use aes_gcm::KeyInit;
use aes_gcm::Nonce;
use clap::{ArgAction, Parser};
use pbkdf2::pbkdf2_hmac;
use rand::RngCore;
use serde::Deserialize;
use serde::Serialize;
use serde_with::skip_serializing_none;
use sha2::Sha256;
use std::{io::Read, path::PathBuf};
#[derive(Parser, Debug)]
#[command(author, about, version, long_about = None)]
struct Cli {
#[arg(short, long, help = "choose privatebin instance to use")]
url: Option<String>, // used
#[arg(short, long)]
expire: Option<String>, // used
#[arg(short, long)]
open_discussion: Option<bool>, // used
#[arg(long)]
burn_after_reading: Option<bool>, // used
#[arg(short, long, action=ArgAction::SetFalse, help = "Disable gzip compression")]
gzip: Option<bool>, // used
#[arg(short, long)]
formatter: Option<String>, // used
#[arg(short, long)]
password: Option<String>, // used
#[arg(long)]
filename: Option<String>,
#[arg(short, long)]
attachment: Option<PathBuf>,
}
#[skip_serializing_none]
#[derive(Deserialize, Debug, Serialize)]
struct PasteData {
paste: String, // text of paste
attachment: Option<String>, // data URI (rfc 2397)
attachment_name: Option<String>, // filename of attachment
chilren: Option<Vec<String>>, // idk what this is. will never use it probably
}
fn main() {
let cli = Cli::parse();
println!("{:?}", cli);
// read piped input
let mut input = String::new();
std::io::stdin().read_to_string(&mut input).unwrap();
//println!("{}", input);
let pasteurl = create_paste(cli, input);
println!("url: {}", pasteurl);
}
fn create_paste(cfg: Cli, input: String) -> String {
use base64::{engine::general_purpose::STANDARD as b64, Engine as _};
let mut rng = rand::thread_rng();
let paste_data = PasteData {
paste: input,
attachment: None,
attachment_name: None,
chilren: None,
};
// check if attachment exists
if cfg.attachment.is_some() {
// TODO read attachment into paste_data.attachment
}
// assume url is https://privatebin.codecoffin.com/ for debugging
let url = "https://privatebin.codecoffin.com/";
// more info at https://github.com/PrivateBin/PrivateBin/wiki/Encryption-format
// generate random 256 bit key
let mut paste_key = [0u8; 32]; // 256 bits
rng.fill_bytes(&mut paste_key);
// add password to key if it exists
let key_and_password: Vec<u8> = paste_key
.iter()
.chain(cfg.password.unwrap_or("".to_string()).as_bytes().iter())
.cloned()
.collect();
// generate random 128 bit salt
let mut kdf_salt = [0u8; 8]; // 8 bytes
rng.fill_bytes(&mut kdf_salt);
let kdf_iterations = 100000; // was 10000 before PrivateBin version 1.3
let mut kdf_derived = [0u8; 32]; // bits of resulting kdf_key
// build mac
//let mut mac = Hmac::new(Sha256::new(), &);
// run pbkdf2 to generate kdf_key
pbkdf2_hmac::<Sha256>(
&key_and_password,
&kdf_salt,
kdf_iterations,
&mut kdf_derived,
);
//crypto::pbkdf2::pbkdf2(&mut mac, &kdf_salt, kdf_iterations, &mut kdf_derived);
//println!("kdf_derived: {:?}", kdf_derived);
// TODO run aes_gcm with random 128 bit iv and kdf_key to encrypt input
// generate random 128 bit iv
let mut nonce = [0u8; 16]; // 16 bytes
rng.fill_bytes(&mut nonce);
let compression = match cfg.gzip {
Some(true) => "zlib",
_ => "none",
};
let mut post_body = serde_json::json!({
"v": 2,
"adata": [
[
b64.encode(&nonce),
b64.encode(&kdf_salt),
100000,
256,
128,
"aes",
"gcm",
compression
],
cfg.formatter.unwrap_or("plaintext".to_string()),
cfg.open_discussion.unwrap_or_default() as u8,
cfg.burn_after_reading.unwrap_or_default() as u8
],
"ct": "",
"meta": {
"expire": cfg.expire.unwrap_or("1week".to_string())
}
});
// aad is adata as string
let aad = post_body.get("adata").unwrap().to_string();
let key: &aes_gcm::Key<aes_gcm::Aes256Gcm> = &kdf_derived.into();
type Cipher = aes_gcm::AesGcm<aes_gcm::aes::Aes256, typenum::U16>;
let cipher = Cipher::new(key);
let paste_blob = match cfg.gzip {
Some(true) => miniz_oxide::deflate::compress_to_vec(
serde_json::to_string(&paste_data).unwrap().as_bytes(),
10,
),
_ => serde_json::to_vec(&paste_data).unwrap(),
};
let payload = aes_gcm::aead::Payload {
msg: &paste_blob,
aad: aad.as_bytes(),
};
let enc_data = cipher.encrypt(Nonce::from_slice(&nonce), payload).unwrap();
//println!("enc_data: {:?}", enc_data);
post_body["ct"] = b64.encode(&enc_data).into();
//println!("post_body: {}", post_body);
let post_key = bs58::encode(paste_key).into_string();
//println!("b58: {}", post_key);
// check if url is set and upload to that url
let mut paste_id = String::new();
if let Some(url) = &cfg.url {
use reqwest::{Method, Url};
//println!("url: {}", url);
let client = reqwest::blocking::Client::builder().build().unwrap();
let mut request = client.request(Method::POST, url);
request = request.header("X-Requested-With", "JSONHttpRequest");
let res = request.body::<String>(serde_json::to_string(&post_body).unwrap()).send().unwrap();
let rsv: serde_json::Value = match res.json() {
Ok(v) => {
v
},
Err(e) => {
println!("Error: {}", e);
return String::new();
}
};
paste_id = rsv.get("id").unwrap().as_str().unwrap().to_string();
}
format!(
"{}?{}#{}",
cfg.url.unwrap_or("https://EXAMPLE.com/".to_string()),
paste_id,
post_key
)
}