This commit is contained in:
trisua 2025-06-07 20:58:10 -04:00
commit 232a2fc2d9
10 changed files with 3315 additions and 0 deletions

74
src/engine/fs.rs Normal file
View file

@ -0,0 +1,74 @@
use super::Engine;
use crate::{FileDescriptor, decrypt_deflate, encrypt_compress, salt};
use pathbufd::PathBufD;
use std::{
collections::HashMap,
fs::{read_to_string, remove_file, write},
io::Result,
};
pub struct FsEngine;
impl Engine for FsEngine {
const CHUNK_SIZE: usize = 200_000;
async fn auth(&mut self) -> Result<()> {
unreachable!("Not needed");
}
async fn process(&self, name: String, data: Vec<u8>) -> Result<FileDescriptor> {
let (seed, key, data) = encrypt_compress(data);
let mut descriptor = FileDescriptor {
name,
key,
seed,
engine_data: HashMap::new(),
chunks: Vec::new(),
};
for chunk in data.as_bytes().chunks(Self::CHUNK_SIZE) {
let id = salt();
self.create_chunk(&id, String::from_utf8(chunk.to_vec()).unwrap())
.await?;
descriptor.chunks.push(id);
}
descriptor.write(PathBufD::current().join(format!("{}.toml", descriptor.name)))?;
Ok(descriptor)
}
async fn reconstruct(&self, descriptor: FileDescriptor) -> Result<()> {
let mut encoded_string: String = String::new();
for chunk in descriptor.chunks {
encoded_string += &self.get_chunk(&chunk).await?;
}
let decoded = decrypt_deflate(descriptor.seed, descriptor.key, encoded_string);
write(PathBufD::current().join(descriptor.name), decoded)?;
Ok(())
}
async fn delete(&mut self, descriptor: FileDescriptor) -> Result<()> {
for chunk in descriptor.chunks {
self.delete_chunk(&chunk).await?;
}
Ok(())
}
async fn get_chunk(&self, id: &str) -> Result<String> {
read_to_string(PathBufD::new().join(id))
}
async fn create_chunk(&self, id: &str, data: String) -> Result<String> {
write(PathBufD::new().join(id), data)?;
Ok(String::new())
}
async fn delete_chunk(&self, id: &str) -> Result<()> {
remove_file(PathBufD::new().join(id))
}
}

29
src/engine/mod.rs Normal file
View file

@ -0,0 +1,29 @@
#![allow(async_fn_in_trait)]
pub mod fs;
pub mod rentry;
use std::io::Result;
use crate::FileDescriptor;
/// An engine that drives remote storage.
pub trait Engine {
const CHUNK_SIZE: usize;
/// Returns authentication details (CSRF token, session token, etc).
async fn auth(&mut self) -> Result<()>;
/// Processes a file and returns a [`FileDescriptor`].
async fn process(&self, name: String, data: Vec<u8>) -> Result<FileDescriptor>;
/// Reads a [`FileDescriptor`] and recreates the file.
async fn reconstruct(&self, descriptor: FileDescriptor) -> Result<()>;
/// Reads a [`FileDescriptor`] and deletes the file.
async fn delete(&mut self, descriptor: FileDescriptor) -> Result<()>;
/// Gets the data in a chunk by its ID.
async fn get_chunk(&self, id: &str) -> Result<String>;
/// Creates a new chunk and sends it to remote.
async fn create_chunk(&self, id: &str, data: String) -> Result<String>;
/// Deletes a specific chunk from the remote. Used to clean up remote data
/// when deleting a file.
async fn delete_chunk(&self, id: &str) -> Result<()>;
}

309
src/engine/rentry.rs Normal file
View file

@ -0,0 +1,309 @@
use super::Engine;
use crate::{FileDescriptor, decrypt_deflate, encrypt_compress, salt};
use pathbufd::PathBufD;
use reqwest::{Client, StatusCode};
use std::{
collections::HashMap,
fs::write,
io::{Error, Result},
time::Duration,
};
use tokio::task::JoinHandle;
pub struct RentryEngine {
pub http: Client,
pub csrf_token: Option<String>,
}
impl RentryEngine {
pub fn new() -> Self {
Self {
http: Client::new(),
csrf_token: None,
}
}
pub async fn get_csrf(&self, url: &str) -> String {
println!("start: extract token {}", url);
let body = self
.http
.get(url)
.send()
.await
.unwrap()
.text()
.await
.unwrap();
let token = body
.split("name=\"csrfmiddlewaretoken\" value=\"")
.skip(1)
.next()
.unwrap()
.split("\"")
.next()
.unwrap()
.to_string();
println!("start: token obtained");
token
}
}
impl Engine for RentryEngine {
const CHUNK_SIZE: usize = 150_000; // chunks are given 8 bytes for extra characters
async fn auth(&mut self) -> Result<()> {
self.csrf_token = Some(self.get_csrf("https://rentry.co").await);
Ok(())
}
async fn process(&self, name: String, data: Vec<u8>) -> Result<FileDescriptor> {
let token = if let Some(ref token) = self.csrf_token {
token.to_owned()
} else {
return Err(Error::other("no csrf token extracted"));
};
let (seed, key, data) = encrypt_compress(data);
let mut descriptor = FileDescriptor {
name,
key,
seed,
engine_data: {
let mut data = HashMap::new();
data.insert("token".to_string(), token);
data
},
chunks: Vec::new(),
};
let chars = data.chars().collect::<Vec<char>>();
let chunks = chars.chunks(Self::CHUNK_SIZE);
let chunks_num = chunks.len();
let mut handles: Vec<JoinHandle<()>> = Vec::new();
for (i, chunk) in chunks.enumerate() {
let id = salt();
println!("cnksv: start chunk save ({}/{chunks_num})", i + 1);
// spawn worker thread
let c = chunk.to_vec();
let mut worker_engine = Self::new();
worker_engine.csrf_token = self.csrf_token.clone();
let id_c = id.clone();
tokio::time::sleep(Duration::from_millis(750)).await; // sleep to avoid rate limit
let handle = tokio::spawn(async move {
let mut b = worker_engine
.create_chunk(&id_c, c.iter().collect())
.await
.expect("failed to create chunk");
while b.contains("e436") {
println!(
"\x1b[91mcnksv: upload failed (hit limit on {})\x1b[0m",
i + 1
);
println!("cnksv: creation limit reached, waiting to try again");
tokio::time::sleep(Duration::from_millis(10000)).await;
b = worker_engine
.create_chunk(&id_c, c.iter().collect())
.await
.expect("failed to create chunk");
}
});
handles.push(handle);
descriptor.chunks.push(id.clone());
}
// wait for all handles to finish
println!("cnksv: waiting for all handles to finish");
loop {
let mut all_finished = true;
for handle in &handles {
if !handle.is_finished() {
all_finished = false;
break;
}
}
if all_finished {
break;
}
// sleep a little to save cpu
tokio::time::sleep(Duration::from_millis(150)).await;
}
// ...
descriptor.write(PathBufD::current().join(format!("{}.toml", descriptor.name)))?;
Ok(descriptor)
}
async fn reconstruct(&self, descriptor: FileDescriptor) -> Result<()> {
let mut encoded_string: String = String::new();
let chunks_num = descriptor.chunks.len();
for (i, chunk) in descriptor.chunks.iter().enumerate() {
tokio::time::sleep(Duration::from_millis(250)).await;
println!("cnkrd: start chunk read ({}/{chunks_num})", i + 1);
if let Ok(d) = self.get_chunk(&chunk).await {
println!("cnkrd: read {} bytes", d.len());
encoded_string += &d;
} else {
println!("cnkrd: waiting to retry chunk read");
tokio::time::sleep(Duration::from_millis(1000)).await;
encoded_string += &self.get_chunk(&chunk).await?;
}
}
let decoded = decrypt_deflate(descriptor.seed, descriptor.key, encoded_string);
write(PathBufD::current().join(descriptor.name), decoded)?;
Ok(())
}
async fn delete(&mut self, descriptor: FileDescriptor) -> Result<()> {
self.csrf_token = Some(
descriptor
.engine_data
.get("token")
.expect("rentry engine requires engine_data.token")
.to_owned(),
);
for (i, chunk) in descriptor.chunks.iter().enumerate() {
println!(
"cnkde: start chunk delete ({}/{})",
i + 1,
descriptor.chunks.len()
);
if self.delete_chunk(&chunk).await.is_err() {
// wait and then retry
println!("cnkde: waiting to retry chunk deletion");
tokio::time::sleep(Duration::from_millis(750)).await;
if self.delete_chunk(&chunk).await.is_err() {
println!("cnkde: failed to delete chunk {} (skipping)", i + 1);
continue;
};
}
}
Ok(())
}
async fn get_chunk(&self, id: &str) -> Result<String> {
println!("cnkrq: chunk request");
let res = self
.http
.get(format!("https://rentry.co/renbin_{id}"))
.send()
.await
.unwrap();
if res.status() != StatusCode::OK {
return Err(Error::other("remote error"));
}
let body = res.text().await.unwrap();
// this shows how useful that raw key thing is for you
let content = body
.split("<div><p>")
.skip(1)
.next()
.unwrap()
.split("</p></div>")
.next()
.unwrap()
.to_string();
println!("cnkrd: chunk read");
Ok(content)
}
async fn create_chunk(&self, id: &str, data: String) -> Result<String> {
let token = if let Some(ref token) = self.csrf_token {
token
} else {
return Err(Error::other("no csrf token extracted"));
};
// create body
let mut body_map: HashMap<&str, String> = HashMap::new();
body_map.insert("csrfmiddlewaretoken", token.to_owned());
body_map.insert("text", data);
body_map.insert("metadata", String::new());
body_map.insert("edit_code", token.to_owned());
body_map.insert("url", format!("renbin_{id}"));
// ...
let res = self
.http
.post("https://rentry.co")
.form(&body_map)
.header("Cookie", format!("csrftoken={token}"))
.header("Referer", "https://rentry.co/")
.send()
.await
.expect("failed to create chunk");
if res.status() != StatusCode::OK {
return Err(Error::other("remote error"));
}
println!("cnksv: chunk saved");
Ok(res.text().await.unwrap())
}
async fn delete_chunk(&self, id: &str) -> Result<()> {
let token = if let Some(ref token) = self.csrf_token {
token
} else {
return Err(Error::other("no csrf token extracted"));
};
// create body
let mut body_map: HashMap<&str, String> = HashMap::new();
body_map.insert("csrfmiddlewaretoken", token.to_owned());
body_map.insert("text", "renbin_delete".to_string());
body_map.insert("metadata", String::new());
body_map.insert("edit_code", token.to_owned());
body_map.insert("new_edit_code", String::new());
body_map.insert("new_url", String::new());
body_map.insert("new_modify_code", String::new());
body_map.insert("delete", "delete".to_string());
// the rentry api is one of the worst i've ever seen
let url = format!("https://rentry.co/renbin_{id}/edit");
let res = self
.http
.post(&url)
.form(&body_map)
.header("Cookie", format!("csrftoken={token}"))
.header("Referer", &url)
.send()
.await
.expect("failed to delete chunk");
if res.status() != StatusCode::OK {
return Err(Error::other("remote error"));
}
println!("cnkde: chunk deleted");
Ok(())
}
}

127
src/lib.rs Normal file
View file

@ -0,0 +1,127 @@
pub mod engine;
use aes_gcm::{
Aes256Gcm, Nonce,
aead::{AeadCore, AeadInPlace, KeyInit, OsRng},
aes::cipher::typenum,
};
use base64::{Engine, engine::general_purpose::STANDARD};
use flate2::{Compression, read::GzDecoder, write::GzEncoder};
use pathbufd::PathBufD as PathBuf;
use rand::{Rng, distr::Alphanumeric, rng};
use serde::{Deserialize, Serialize};
use std::{
collections::HashMap,
fs::{read_to_string, write},
io::{Result, prelude::*},
};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct FileDescriptor {
/// The name of the file.
pub name: String,
/// The encryption key used for the file.
pub key: String,
/// The seed (nonce) used for the file.
pub seed: String,
/// Data needed by the engine to read this file.
pub engine_data: HashMap<String, String>,
/// A list of all locations to retrieve chunks from.
/// Each chunk **must** be in the correct order.
pub chunks: Vec<String>,
}
impl FileDescriptor {
/// Read a [`FileDescriptor`] from the given `path`.
pub fn read(path: PathBuf) -> Self {
toml::from_str(&read_to_string(path).expect("failed to read file"))
.expect("failed to deserialize file")
}
/// Write a [`FileDescriptor`] into the given `path`.
pub fn write(&self, path: PathBuf) -> Result<()> {
write(
path,
toml::to_string_pretty(&self).expect("failed to serialize file"),
)
}
}
/// Create a random (not necessarily unique) 16-byte long string.
pub fn salt() -> String {
rng()
.sample_iter(&Alphanumeric)
.take(16)
.map(char::from)
.collect()
}
/// Encrypt and compress the given `data` using the given `key`.
///
/// # Returns
/// `(nonce, key, encrypted as base64)`
pub fn encrypt_compress(data: Vec<u8>) -> (String, String, String) {
// compress
let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
encoder.write_all(&data).expect("failed to compress");
println!("encod: data compressed");
let mut compressed_buffer: Vec<u8> = encoder.finish().unwrap();
// encrypt
let key = Aes256Gcm::generate_key(&mut OsRng);
let cipher = Aes256Gcm::new(&key);
let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
cipher
.encrypt_in_place(&nonce, b"", &mut compressed_buffer)
.expect("failed to encrypt");
println!("encod: data encrypted");
// return
(
STANDARD.encode(nonce.to_vec()),
STANDARD.encode(key.to_vec()),
STANDARD.encode(compressed_buffer.to_vec()),
)
}
/// Decrypt and uncompress the given `data` using the given `key`.
///
/// # Returns
/// `(key, encrypted)`
pub fn decrypt_deflate(nonce: String, key: String, data: String) -> Vec<u8> {
// decrypt
// decryption must happen first since we must undo what was done previously
let key = STANDARD.decode(key).expect("recieved invalid base64 key");
let cipher = Aes256Gcm::new_from_slice(&key.as_slice()).unwrap();
let nonce = Nonce::<typenum::U12>::clone_from_slice(
STANDARD
.decode(nonce)
.expect("received invalid base64 nonce")
.as_slice(),
);
let data = STANDARD.decode(data).expect("received invalid base64 data");
let mut decrypted_buffer: Vec<u8> = data;
cipher
.decrypt_in_place(&nonce, b"", &mut decrypted_buffer)
.expect("failed to decrypt");
println!("decod: data decrypted");
// compress
let mut decoder = GzDecoder::new(decrypted_buffer.as_slice());
let mut decompressed_buffer: Vec<u8> = Vec::new();
decoder
.read_to_end(&mut decompressed_buffer)
.expect("failed to decompress");
println!("decod: data decompressed");
// return
decompressed_buffer
}

101
src/main.rs Normal file
View file

@ -0,0 +1,101 @@
extern crate renbin;
use clap::Parser;
use renbin::{
FileDescriptor,
engine::{Engine, fs::FsEngine, rentry::RentryEngine},
};
use std::{
fs::{read, remove_file},
path::PathBuf,
};
#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
struct Args {
#[arg(short = 'd', long = "decode", action)]
decode: bool,
#[arg(short = 'x', long = "delete", action)]
delete: bool,
#[arg(short = 'i', long = "input")]
path: String,
#[arg(short = 'e', long = "engine")]
#[clap(default_value = "fs")]
engine: String,
}
#[tokio::main]
async fn main() {
let args = Args::parse();
if args.delete {
// delete
let path = PathBuf::from(args.path);
if args.engine == "fs" {
FsEngine
.delete(FileDescriptor::read(path.clone().into()))
.await
.expect("failed to reconstruct file");
} else if args.engine == "rentry" {
let mut engine = RentryEngine::new();
engine.auth().await.expect("failed to extract csrf token");
engine
.delete(FileDescriptor::read(path.clone().into()))
.await
.expect("failed to delete file");
} else {
panic!("unknown engine type");
};
remove_file(path).expect("failed to delete file descriptor");
return;
}
if !args.decode {
// encode
let pathbuf = PathBuf::from(args.path);
if args.engine == "fs" {
FsEngine
.process(
pathbuf.file_name().unwrap().to_str().unwrap().to_string(),
read(pathbuf).expect("failed to read file"),
)
.await
.expect("failed to process file")
} else if args.engine == "rentry" {
let mut engine = RentryEngine::new();
engine.auth().await.expect("failed to extract csrf token");
engine
.process(
pathbuf.file_name().unwrap().to_str().unwrap().to_string(),
read(pathbuf).expect("failed to read file"),
)
.await
.expect("failed to process file")
} else {
panic!("unknown engine type");
};
} else {
// decode
if args.engine == "fs" {
FsEngine
.reconstruct(FileDescriptor::read(PathBuf::from(args.path).into()))
.await
.expect("failed to reconstruct file");
} else if args.engine == "rentry" {
let mut engine = RentryEngine::new();
engine.auth().await.expect("failed to extract csrf token");
engine
.reconstruct(FileDescriptor::read(PathBuf::from(args.path).into()))
.await
.expect("failed to reconstruct file");
} else {
panic!("unknown engine type");
};
}
}