Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
0ef9b29
Update
TheRustyPickle Feb 2, 2026
98635ab
Initial gui
TheRustyPickle Feb 2, 2026
ab87d22
Add rfd dep
TheRustyPickle Feb 3, 2026
31aa92b
Add send ui
TheRustyPickle Feb 3, 2026
44ef825
refine ui
TheRustyPickle Feb 4, 2026
bfb9d77
Update dep
TheRustyPickle Feb 6, 2026
8e240e1
Update sendui
TheRustyPickle Feb 6, 2026
5622cb2
Add animated input box
TheRustyPickle Feb 8, 2026
a504847
Remove authors field and format
TheRustyPickle Feb 9, 2026
01bfb74
Move config to shared
TheRustyPickle Feb 10, 2026
ab6d26c
Update
TheRustyPickle Feb 10, 2026
77c820d
Add dialog for password and radio
TheRustyPickle Feb 10, 2026
88277b8
Move config into a feature
TheRustyPickle Feb 11, 2026
7dae649
Add ui for view and day count
TheRustyPickle Feb 11, 2026
fe0d095
Unify config
TheRustyPickle Feb 11, 2026
3764e89
Add toast on fail
TheRustyPickle Feb 13, 2026
eac3b4e
Update dependencies
TheRustyPickle Feb 13, 2026
8c876d4
Finish submit option
TheRustyPickle Feb 13, 2026
b617310
Remove unused dep
TheRustyPickle Feb 15, 2026
9b72c1a
Unify config fetch
TheRustyPickle Feb 15, 2026
ee55449
Return early on invalid or empty secret id
TheRustyPickle Feb 22, 2026
c54dcb1
Fix incorrect function calls
TheRustyPickle Feb 22, 2026
803e5a4
Initial recv page
TheRustyPickle Feb 22, 2026
a7e568c
Rename
TheRustyPickle Feb 25, 2026
9e69ea0
Add decryption
TheRustyPickle Feb 25, 2026
0fd7d22
Complete recv tab
TheRustyPickle Feb 27, 2026
f1a5af2
Format
TheRustyPickle Feb 27, 2026
e7ea697
Clippy fix
TheRustyPickle Feb 28, 2026
b166f0d
Clippy fix
TheRustyPickle Feb 28, 2026
9c4c05b
Add close button
TheRustyPickle Feb 28, 2026
5544393
Set manual default config
TheRustyPickle Mar 7, 2026
8ce2874
Update
TheRustyPickle Mar 7, 2026
71098fa
Add config page
TheRustyPickle Mar 7, 2026
96ffacf
Update placeholders
TheRustyPickle Mar 7, 2026
13f31bc
Update config page
TheRustyPickle Mar 20, 2026
43f8182
Rename
TheRustyPickle Mar 29, 2026
1737b3f
Update
TheRustyPickle Mar 29, 2026
486d99d
Emit event and update tabs
TheRustyPickle Mar 29, 2026
43829c8
Handle unsaved when saving file
TheRustyPickle Mar 29, 2026
48b5bdc
Fix reset button
TheRustyPickle Mar 29, 2026
65d6435
Add missing config fields on cli
TheRustyPickle Apr 8, 2026
f6c3b2f
Use correct web ui link
TheRustyPickle Apr 8, 2026
fdfedc7
Fix size checker and add logs
TheRustyPickle Apr 8, 2026
6671317
Update versions
TheRustyPickle Apr 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9,752 changes: 7,745 additions & 2,007 deletions Cargo.lock

Large diffs are not rendered by default.

17 changes: 9 additions & 8 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
[workspace]
members = ["cli", "server", "srv-lib", "core", "shared"]

resolver = "2"
members = ["cli", "core", "gui", "server", "shared", "srv-lib"]

[workspace.dependencies]
vial-core = "0.1.0"
vial-srv = "0.1.0"
vial-shared = "0.1.0"
chrono = { version = "0.4.42", features = ["serde"] }
tokio = { version = "1.49.0", features = ["rt-multi-thread", "macros"] }
anyhow = "1.0.102"
base64 = "0.22.1"
chrono = { version = "0.4.44", features = ["serde"] }
reqwest = { version = "0.13.2", features = ["blocking", "json"] }
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.149"
anyhow = "1.0.100"
tokio = { version = "1.51.1", features = ["macros", "rt-multi-thread"] }
vial-core = { path = "core", version = "0.2.0" }
vial-shared = { path = "shared", version = "0.2.0" }
vial-srv = { path = "srv-lib", version = "0.2.0" }

# The profile that 'dist' will build with
[profile.dist]
Expand Down
23 changes: 10 additions & 13 deletions cli/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,32 +1,29 @@
[package]
name = "vial-cli"
version = "0.1.0"
version = "0.2.0"
edition = "2024"
authors = ["TheRustyPickle <rusty.pickle94@gmail.com>"]
readme = "../README.md"
description = """
CLI for creating and retrieving end-to-end encrypted secrets
"""
readme = "../README.md"
homepage = "https://github.com/TheRustyPickle/Vial"
repository = "https://github.com/TheRustyPickle/Vial"
license = "MIT"
keywords = ["cli", "e2e", "secret", "vault", "secure"]
keywords = ["cli", "e2ee", "secret", "secure", "vault"]
categories = ["command-line-utilities"]

[[bin]]
name = "vial"
path = "src/main.rs"

[dependencies]
vial-core.workspace = true
vial-shared.workspace = true
anyhow.workspace = true
base64.workspace = true
chrono.workspace = true
clap = { version = "4.6.0", features = ["derive"] }
reqwest.workspace = true
rpassword = "7.4.0"
serde.workspace = true
serde_json.workspace = true
anyhow.workspace = true

dirs = "6.0.0"
reqwest = { version = "0.13.1", features = ["json", "blocking"] }
clap = { version = "4.5.54", features = ["derive"] }
base64 = "0.22.1"
rpassword = "7.4.0"
vial-core.workspace = true
vial-shared = { workspace = true, features = ["config"] }
224 changes: 98 additions & 126 deletions cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,110 +2,25 @@ use anyhow::{Context, Result, anyhow};
use base64::{Engine as _, engine::general_purpose::URL_SAFE};
use chrono::{Days, Utc};
use clap::{Args, Parser, Subcommand};
use dirs::config_dir;
use serde::{Deserialize, Serialize};
use std::env::set_current_dir;
use std::fs::{File, create_dir_all, read};
use std::fs::read;
use std::io::{Write as _, stdin, stdout};
use std::path::{Path, PathBuf};
use vial_core::crypto::{
decrypt_with_password, decrypt_with_random_key, encrypt_with_password, encrypt_with_random_key,
};
use vial_shared::config::Config;
use vial_shared::{
CreateSecretRequest, EncryptedPayload, FullSecretV1, Payload, SecretFile, SecretFileV1,
SecretId, sanitize_filename,
};

const MAX_SIZE: usize = 1024 * 1024 * 5 + 200;
const DEFAULT_SERVER_URL: &str = "https://rustypickle.onrender.com/api/secrets";
const DEFAULT_WEB_URL: &str = "https://rustypickle.onrender.com/secrets";

#[derive(Parser, Debug)]
struct Cli {
#[command(subcommand)]
command: Command,
}

#[derive(Serialize, Deserialize, Default)]
struct Config {
download_path: Option<PathBuf>,
server_url: Option<String>,
max_size: Option<usize>,
web_ui_url: Option<String>,
}

impl Config {
fn get_config() -> Result<Self> {
let mut target_path = config_dir().unwrap();

target_path.push("Vial");

create_dir_all(&target_path)?;

target_path.push("vial.json");

if target_path.exists() {
let contents = read(target_path)?;
Ok(serde_json::from_slice(&contents)?)
} else {
let config = Config {
download_path: None,
server_url: None,
max_size: None,
web_ui_url: None,
};

config.save_config()?;

Ok(config)
}
}

fn set_download_path(&mut self, path: PathBuf) -> Result<()> {
self.download_path = Some(path);

self.save_config()?;

Ok(())
}

fn set_server_url(&mut self, url: String) -> Result<()> {
self.server_url = Some(url);

self.save_config()?;

Ok(())
}

fn set_web_ui_url(&mut self, url: String) -> Result<()> {
self.web_ui_url = Some(url);

self.save_config()?;

Ok(())
}

fn set_max_size(&mut self, size: usize) -> Result<()> {
self.max_size = Some(size);

self.save_config()?;

Ok(())
}

fn save_config(&self) -> Result<()> {
let mut target_path = config_dir().unwrap();

target_path.push("Vial");

target_path.push("vial.json");

let mut file = File::create(target_path)?;
serde_json::to_writer(&mut file, self)?;
Ok(())
}
}

#[derive(Subcommand, Debug)]
enum Command {
/// Send a new secret to the server
Expand Down Expand Up @@ -243,6 +158,57 @@ pub struct ConfigArgs {
/// 10485760 (10 MB)
#[arg(long, value_name = "BYTES")]
pub set_max_size: Option<usize>,

/// Set the maximum views allowed for a secret
///
/// Unless a different server is used than the default one, this value is ignored.
///
/// Defaults to 1000 views.
///
/// Example values:
/// 1000
/// 9999
#[arg(long, value_name = "VIEW COUNT")]
pub set_max_views: Option<usize>,

/// Set the maximum days a secret is allowed to exist
///
/// Unless a different server is used than the default one, this value is ignored.
///
/// Defaults to 30 days.
///
/// Example values:
/// 100
/// 365
#[arg(long, value_name = "DAYS COUNT")]
pub set_max_days: Option<usize>,

/// Set the database URL to use when starting the server bin (vial-server)
///
/// Defaults nothing
///
/// Example value:
/// postgresql://postgres:asdf@127.0.0.1:5432/asdf
#[arg(long, value_name = "POSTGRES URL")]
pub set_database_url: Option<String>,

/// Set the port to bind to when starting the server bin (vial-server)
///
/// Defaults to 8080.
///
/// Example value:
/// 8080
#[arg(long, value_name = "PORT")]
pub set_port: Option<u16>,

/// Set the address to bind to when starting the server bin (vial-server)
///
/// Defaults to 127.0.0.1.
///
/// Example value:
/// 127.0.0.1
#[arg(long, value_name = "ADDRESS")]
pub set_address: Option<String>,
}

fn main() -> Result<()> {
Expand Down Expand Up @@ -300,9 +266,7 @@ fn send(
expires_at = Some(Utc::now().naive_utc() + Days::new(expire as u64));
}

let config = Config::get_config()
.context("Failed to read config")
.unwrap_or_default();
let config = Config::get_config();

let mut files = Vec::with_capacity(attachments.len());

Expand Down Expand Up @@ -342,39 +306,18 @@ fn send(
};

// Only accept the size in the config if a different server is used than the default one
let max_size = if let Some(max_size) = config.max_size
&& let Some(url) = &config.server_url
&& url != DEFAULT_SERVER_URL
{
max_size
} else {
MAX_SIZE
};
let max_size = config.get_max_size_verified();

let post_url = if let Some(url) = config.server_url {
url
} else {
DEFAULT_SERVER_URL.to_string()
};
let post_url = config.get_server_url();

let web_ui_url = if let Some(url) = config.web_ui_url {
url
} else {
DEFAULT_WEB_URL.to_string()
};
let web_ui_url = config.get_web_ui_url();

if blob.len() > max_size {
return Err(anyhow!(
"The secret is too large to be sent. Try breaking it up. Max limit is {max_size} bytes."
));
}

if blob.len() > MAX_SIZE {
return Err(anyhow!(
"The secret is too large to be sent. Try breaking it up. Max limit is {MAX_SIZE} bytes."
));
}

let secret_request = CreateSecretRequest {
ciphertext: blob,
expires_at,
Expand Down Expand Up @@ -404,29 +347,28 @@ fn receive(source: String, password: bool, random_key: bool) -> Result<()> {
return Err(anyhow!("Could not find the secret id in the secret link."));
};

let config = Config::get_config()
.context("Failed to read config")
.unwrap_or_default();
if secret_id.is_empty() || secret_id.contains(' ') {
return Err(anyhow!("Could not find the secret id in the secret link."));
}

let post_url = if let Some(url) = config.server_url {
url
} else {
DEFAULT_SERVER_URL.to_string()
};
let config = Config::get_config();

let server_url = config.get_server_url();

let key = secret_id.split_once('#');

let client = reqwest::blocking::Client::new();

let decrypted = if let Some((id, key)) = key {
let payload: EncryptedPayload = reqwest_json(client.get(format!("{post_url}/{id}")))
let payload: EncryptedPayload = reqwest_json(client.get(format!("{server_url}/{id}")))
.context("Failed to fetch the secret")?;

decrypt_random_key(key, &payload.payload)
.context("Failed to decrypt using random key schema")?
} else {
let payload: EncryptedPayload = reqwest_json(client.get(format!("{post_url}/{secret_id}")))
.context("Failed to fetch the secret")?;
let payload: EncryptedPayload =
reqwest_json(client.get(format!("{server_url}/{secret_id}")))
.context("Failed to fetch the secret")?;

let key = rpassword::prompt_password("Enter key/password: ")
.context("Failed to read the password")?;
Expand Down Expand Up @@ -456,7 +398,7 @@ fn receive(source: String, password: bool, random_key: bool) -> Result<()> {
}

fn config(args: ConfigArgs) -> Result<()> {
let mut config = Config::get_config().context("Failed to get config")?;
let mut config = Config::get_config();

if let Some(dl_path) = args.set_download_path {
config
Expand All @@ -482,6 +424,36 @@ fn config(args: ConfigArgs) -> Result<()> {
.with_context(|| format!("Failed to set new server url {url}"))?;
}

if let Some(days) = args.set_max_days {
config
.set_max_days(days)
.with_context(|| format!("Failed to set new max days {days}"))?;
}

if let Some(views) = args.set_max_views {
config
.set_max_views(views)
.with_context(|| format!("Failed to set new max views {views}"))?;
}

if let Some(url) = args.set_database_url {
config
.set_database_url(url.clone())
.with_context(|| format!("Failed to set new database url {url}"))?;
}

if let Some(port) = args.set_port {
config
.set_port(port)
.with_context(|| format!("Failed to set new port {port}"))?;
}

if let Some(address) = args.set_address {
config
.set_address(address.clone())
.with_context(|| format!("Failed to set new address {address}"))?;
}

if args.show {
println!(
"{}",
Expand Down
Loading
Loading