working prototype minus attachments
This commit is contained in:
commit
a7c4ab7f93
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/target
|
1671
Cargo.lock
generated
Normal file
1671
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
21
Cargo.toml
Normal file
21
Cargo.toml
Normal 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
205
src/main.rs
Normal 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
|
||||
)
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user