From ae4b65863e0c97889d0b12adc3bb317889b60577 Mon Sep 17 00:00:00 2001 From: Tyr Chen Date: Sun, 24 Mar 2024 11:15:07 -0700 Subject: [PATCH] feature: add sign/verify cli for text --- Cargo.lock | 253 ++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 2 + deny.toml | 2 + fixtures/blake3.txt | 1 + fixtures/ed25519.pk | Bin 0 -> 32 bytes fixtures/ed25519.sk | 1 + src/cli/base64.rs | 6 +- src/cli/csv.rs | 4 +- src/cli/mod.rs | 26 ++++- src/cli/text.rs | 82 +++++++++++++ src/lib.rs | 4 +- src/main.rs | 53 ++++++++- src/process/b64.rs | 39 +++---- src/process/gen_pass.rs | 12 +- src/process/mod.rs | 2 + src/process/text.rs | 180 ++++++++++++++++++++++++++++ src/utils.rs | 18 +++ 17 files changed, 634 insertions(+), 51 deletions(-) create mode 100644 fixtures/blake3.txt create mode 100644 fixtures/ed25519.pk create mode 100644 fixtures/ed25519.sk create mode 100644 src/cli/text.rs create mode 100644 src/process/text.rs create mode 100644 src/utils.rs diff --git a/Cargo.lock b/Cargo.lock index a06f722..12facd7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -65,12 +65,30 @@ version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247" +[[package]] +name = "arrayref" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" + +[[package]] +name = "arrayvec" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" + [[package]] name = "base64" version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51" +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + [[package]] name = "bit-set" version = "0.5.3" @@ -86,12 +104,40 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" +[[package]] +name = "blake3" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30cca6d3674597c30ddf2c587bf8d9d65c9a84d2326d941cc79c9842dfe0ef52" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "bumpalo" version = "3.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa" +[[package]] +name = "cc" +version = "1.0.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" + [[package]] name = "cfg-if" version = "1.0.0" @@ -144,6 +190,37 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "constant_time_eq" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2" + +[[package]] +name = "cpufeatures" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "csv" version = "1.3.0" @@ -165,6 +242,34 @@ dependencies = [ "memchr", ] +[[package]] +name = "curve25519-dalek" +version = "4.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a677b8922c94e01bdbb12126b0bc852f00447528dee1782229af9c720c3f348" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "platforms", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.53", +] + [[package]] name = "darling" version = "0.14.4" @@ -200,6 +305,16 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "der" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c" +dependencies = [ + "const-oid", + "zeroize", +] + [[package]] name = "deranged" version = "0.3.11" @@ -240,6 +355,41 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand_core", + "serde", + "sha2", + "subtle", + "zeroize", +] + [[package]] name = "either" version = "1.10.0" @@ -262,12 +412,28 @@ dependencies = [ "regex", ] +[[package]] +name = "fiat-crypto" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c007b1ae3abe1cb6f85a16305acd418b7ca6343b953633fee2b76d8f108b830f" + [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.2.12" @@ -367,6 +533,22 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "platforms" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "626dec3cac7cc0e1577a2ec3fc496277ec2baa084bebad95bb6fdbfae235f84c" + [[package]] name = "powerfmt" version = "0.2.0" @@ -439,8 +621,10 @@ version = "0.1.0" dependencies = [ "anyhow", "base64", + "blake3", "clap", "csv", + "ed25519-dalek", "rand", "serde", "serde_json", @@ -477,12 +661,27 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + [[package]] name = "ryu" version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" +[[package]] +name = "semver" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" + [[package]] name = "serde" version = "1.0.197" @@ -527,6 +726,36 @@ dependencies = [ "unsafe-libyaml", ] +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "rand_core", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + [[package]] name = "strsim" version = "0.10.0" @@ -539,6 +768,12 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" +[[package]] +name = "subtle" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" + [[package]] name = "syn" version = "1.0.109" @@ -580,6 +815,12 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + [[package]] name = "unicode-ident" version = "1.0.12" @@ -598,6 +839,12 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -724,6 +971,12 @@ version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" +[[package]] +name = "zeroize" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" + [[package]] name = "zxcvbn" version = "2.2.2" diff --git a/Cargo.toml b/Cargo.toml index b13ae4b..b2e0270 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,8 +10,10 @@ license = "MIT" [dependencies] anyhow = "1.0.81" base64 = "0.22.0" +blake3 = "1.5.1" clap = { version = "4.5.3", features = ["derive"] } csv = "1.3.0" +ed25519-dalek = { version = "2.1.1", features = ["rand_core"] } rand = "0.8.5" serde = { version = "1.0.197", features = ["derive"] } serde_json = "1.0.114" diff --git a/deny.toml b/deny.toml index b8125bb..7752cd3 100644 --- a/deny.toml +++ b/deny.toml @@ -102,6 +102,8 @@ allow = [ "MIT", "Apache-2.0", "Unicode-DFS-2016", + "BSD-2-Clause", + "BSD-3-Clause", #"Apache-2.0 WITH LLVM-exception", ] # The confidence threshold for detecting a license from license text. diff --git a/fixtures/blake3.txt b/fixtures/blake3.txt new file mode 100644 index 0000000..7309f5d --- /dev/null +++ b/fixtures/blake3.txt @@ -0,0 +1 @@ +GJ%Ft1a#%g!!ni_tcE4@J3Ez9@5ku&b5 diff --git a/fixtures/ed25519.pk b/fixtures/ed25519.pk new file mode 100644 index 0000000000000000000000000000000000000000..f3cf40c04b47df107e2f0370dc30fc98f5f16b2b GIT binary patch literal 32 ocmbao}gX+}`3s#0R+|PL1t^4|g`^UH20pq6+$^ZZW literal 0 HcmV?d00001 diff --git a/fixtures/ed25519.sk b/fixtures/ed25519.sk new file mode 100644 index 0000000..54a35a4 --- /dev/null +++ b/fixtures/ed25519.sk @@ -0,0 +1 @@ +3k׸vr%ymx"ԁAC5G \ No newline at end of file diff --git a/src/cli/base64.rs b/src/cli/base64.rs index d5a7bd6..9ceadfa 100644 --- a/src/cli/base64.rs +++ b/src/cli/base64.rs @@ -2,7 +2,7 @@ use std::{fmt, str::FromStr}; use clap::Parser; -use super::verify_input_file; +use super::verify_file; #[derive(Debug, Parser)] pub enum Base64SubCommand { @@ -14,7 +14,7 @@ pub enum Base64SubCommand { #[derive(Debug, Parser)] pub struct Base64EncodeOpts { - #[arg(short, long, value_parser = verify_input_file, default_value = "-")] + #[arg(short, long, value_parser = verify_file, default_value = "-")] pub input: String, #[arg(long, value_parser = parse_base64_format, default_value = "standard")] pub format: Base64Format, @@ -22,7 +22,7 @@ pub struct Base64EncodeOpts { #[derive(Debug, Parser)] pub struct Base64DecodeOpts { - #[arg(short, long, value_parser = verify_input_file, default_value = "-")] + #[arg(short, long, value_parser = verify_file, default_value = "-")] pub input: String, #[arg(long, value_parser = parse_base64_format, default_value = "standard")] pub format: Base64Format, diff --git a/src/cli/csv.rs b/src/cli/csv.rs index 94e8640..15baf18 100644 --- a/src/cli/csv.rs +++ b/src/cli/csv.rs @@ -1,4 +1,4 @@ -use super::verify_input_file; +use super::verify_file; use clap::Parser; use std::{fmt, str::FromStr}; @@ -10,7 +10,7 @@ pub enum OutputFormat { #[derive(Debug, Parser)] pub struct CsvOpts { - #[arg(short, long, value_parser = verify_input_file)] + #[arg(short, long, value_parser = verify_file)] pub input: String, #[arg(short, long)] // "output.json".into() diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 41f3fb9..76b4909 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -1,8 +1,9 @@ mod base64; mod csv; mod genpass; +mod text; -use std::path::Path; +use std::path::{Path, PathBuf}; use self::{csv::CsvOpts, genpass::GenPassOpts}; use clap::Parser; @@ -10,6 +11,7 @@ use clap::Parser; pub use self::{ base64::{Base64Format, Base64SubCommand}, csv::OutputFormat, + text::{TextSignFormat, TextSubCommand}, }; #[derive(Debug, Parser)] @@ -27,9 +29,11 @@ pub enum SubCommand { GenPass(GenPassOpts), #[command(subcommand)] Base64(Base64SubCommand), + #[command(subcommand)] + Text(TextSubCommand), } -fn verify_input_file(filename: &str) -> Result { +fn verify_file(filename: &str) -> Result { // if input is "-" or file exists if filename == "-" || Path::new(filename).exists() { Ok(filename.into()) @@ -38,15 +42,25 @@ fn verify_input_file(filename: &str) -> Result { } } +fn verify_path(path: &str) -> Result { + // if input is "-" or file exists + let p = Path::new(path); + if p.exists() && p.is_dir() { + Ok(path.into()) + } else { + Err("Path does not exist or is not a directory") + } +} + #[cfg(test)] mod tests { use super::*; #[test] fn test_verify_input_file() { - assert_eq!(verify_input_file("-"), Ok("-".into())); - assert_eq!(verify_input_file("*"), Err("File does not exist")); - assert_eq!(verify_input_file("Cargo.toml"), Ok("Cargo.toml".into())); - assert_eq!(verify_input_file("not-exist"), Err("File does not exist")); + assert_eq!(verify_file("-"), Ok("-".into())); + assert_eq!(verify_file("*"), Err("File does not exist")); + assert_eq!(verify_file("Cargo.toml"), Ok("Cargo.toml".into())); + assert_eq!(verify_file("not-exist"), Err("File does not exist")); } } diff --git a/src/cli/text.rs b/src/cli/text.rs new file mode 100644 index 0000000..30b6ba3 --- /dev/null +++ b/src/cli/text.rs @@ -0,0 +1,82 @@ +use std::{fmt, path::PathBuf, str::FromStr}; + +use clap::Parser; + +use super::{verify_file, verify_path}; + +#[derive(Debug, Parser)] +pub enum TextSubCommand { + #[command(about = "Sign a text with a private/session key and return a signature")] + Sign(TextSignOpts), + #[command(about = "Verify a signature with a public/session key")] + Verify(TextVerifyOpts), + #[command(about = "Generate a random blake3 key or ed25519 key pair")] + Generate(KeyGenerateOpts), +} + +#[derive(Debug, Parser)] +pub struct TextSignOpts { + #[arg(short, long, value_parser = verify_file, default_value = "-")] + pub input: String, + #[arg(short, long, value_parser = verify_file)] + pub key: String, + #[arg(long, default_value = "blake3", value_parser = parse_text_sign_format)] + pub format: TextSignFormat, +} + +#[derive(Debug, Parser)] +pub struct TextVerifyOpts { + #[arg(short, long, value_parser = verify_file, default_value = "-")] + pub input: String, + #[arg(short, long, value_parser = verify_file)] + pub key: String, + #[arg(long)] + pub sig: String, + #[arg(long, default_value = "blake3", value_parser = parse_text_sign_format)] + pub format: TextSignFormat, +} + +#[derive(Debug, Parser)] +pub struct KeyGenerateOpts { + #[arg(long, default_value = "blake3", value_parser = parse_text_sign_format)] + pub format: TextSignFormat, + #[arg(short, long, value_parser = verify_path)] + pub output_path: PathBuf, +} + +#[derive(Debug, Clone, Copy)] +pub enum TextSignFormat { + Blake3, + Ed25519, +} + +fn parse_text_sign_format(format: &str) -> Result { + format.parse() +} + +impl FromStr for TextSignFormat { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + match s { + "blake3" => Ok(TextSignFormat::Blake3), + "ed25519" => Ok(TextSignFormat::Ed25519), + _ => Err(anyhow::anyhow!("Invalid format")), + } + } +} + +impl From for &'static str { + fn from(format: TextSignFormat) -> Self { + match format { + TextSignFormat::Blake3 => "blake3", + TextSignFormat::Ed25519 => "ed25519", + } + } +} + +impl fmt::Display for TextSignFormat { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", Into::<&str>::into(*self)) + } +} diff --git a/src/lib.rs b/src/lib.rs index 140d2e5..9f83d8c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,7 @@ mod cli; mod process; +mod utils; -pub use cli::{Base64Format, Base64SubCommand, Opts, SubCommand}; +pub use cli::{Base64Format, Base64SubCommand, Opts, SubCommand, TextSignFormat, TextSubCommand}; pub use process::*; +pub use utils::*; diff --git a/src/main.rs b/src/main.rs index 12c8442..5775441 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,15 @@ // rcli csv -i input.csv -o output.json --header -d ',' +use std::fs; + +use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; use clap::Parser; use rcli::{ - process_csv, process_decode, process_encode, process_genpass, Base64SubCommand, Opts, - SubCommand, + get_content, get_reader, process_csv, process_decode, process_encode, process_genpass, + process_text_key_generate, process_text_sign, process_text_verify, Base64SubCommand, Opts, + SubCommand, TextSubCommand, }; +use zxcvbn::zxcvbn; fn main() -> anyhow::Result<()> { let opts = Opts::parse(); @@ -18,20 +23,56 @@ fn main() -> anyhow::Result<()> { process_csv(&opts.input, output, opts.format)?; } SubCommand::GenPass(opts) => { - process_genpass( + let ret = process_genpass( opts.length, opts.uppercase, opts.lowercase, opts.number, opts.symbol, )?; + println!("{}", ret); + + // output password strength in stderr + let estimate = zxcvbn(&ret, &[])?; + eprintln!("Password strength: {}", estimate.score()); } - SubCommand::Base64(subcmd) => match subcmd { + SubCommand::Base64(cmd) => match cmd { Base64SubCommand::Encode(opts) => { - process_encode(&opts.input, opts.format)?; + let mut reader = get_reader(&opts.input)?; + let ret = process_encode(&mut reader, opts.format)?; + println!("{}", ret); } Base64SubCommand::Decode(opts) => { - process_decode(&opts.input, opts.format)?; + let mut reader = get_reader(&opts.input)?; + let ret = process_decode(&mut reader, opts.format)?; + println!("{}", ret); + } + }, + SubCommand::Text(cmd) => match cmd { + TextSubCommand::Sign(opts) => { + let mut reader = get_reader(&opts.input)?; + let key = get_content(&opts.key)?; + let sig = process_text_sign(&mut reader, &key, opts.format)?; + // base64 output + let encoded = URL_SAFE_NO_PAD.encode(sig); + println!("{}", encoded); + } + TextSubCommand::Verify(opts) => { + let mut reader = get_reader(&opts.input)?; + let key = get_content(&opts.key)?; + let decoded = URL_SAFE_NO_PAD.decode(&opts.sig)?; + let verified = process_text_verify(&mut reader, &key, &decoded, opts.format)?; + if verified { + println!("✓ Signature verified"); + } else { + println!("⚠ Signature not verified"); + } + } + TextSubCommand::Generate(opts) => { + let key = process_text_key_generate(opts.format)?; + for (k, v) in key { + fs::write(opts.output_path.join(k), v)?; + } } }, } diff --git a/src/process/b64.rs b/src/process/b64.rs index a496adb..4936ce1 100644 --- a/src/process/b64.rs +++ b/src/process/b64.rs @@ -4,22 +4,20 @@ use base64::{ engine::general_purpose::{STANDARD, URL_SAFE_NO_PAD}, Engine as _, }; -use std::{fs::File, io::Read}; +use std::io::Read; -pub fn process_encode(input: &str, format: Base64Format) -> Result<()> { - let mut reader = get_reader(input)?; +pub fn process_encode(reader: &mut dyn Read, format: Base64Format) -> Result { let mut buf = Vec::new(); reader.read_to_end(&mut buf)?; let encoded = match format { Base64Format::Standard => STANDARD.encode(&buf), Base64Format::UrlSafe => URL_SAFE_NO_PAD.encode(&buf), }; - println!("{}", encoded); - Ok(()) + + Ok(encoded) } -pub fn process_decode(input: &str, format: Base64Format) -> Result<()> { - let mut reader = get_reader(input)?; +pub fn process_decode(reader: &mut dyn Read, format: Base64Format) -> Result { let mut buf = String::new(); reader.read_to_string(&mut buf)?; // avoid accidental newlines @@ -30,35 +28,30 @@ pub fn process_decode(input: &str, format: Base64Format) -> Result<()> { Base64Format::UrlSafe => URL_SAFE_NO_PAD.decode(buf)?, }; // TODO: decoded data might not be string (but for this example, we assume it is) - let decoded = String::from_utf8(decoded)?; - println!("{}", decoded); - Ok(()) -} - -fn get_reader(input: &str) -> Result> { - let reader: Box = if input == "-" { - Box::new(std::io::stdin()) - } else { - Box::new(File::open(input)?) - }; - Ok(reader) + Ok(String::from_utf8(decoded)?) } #[cfg(test)] mod tests { use super::*; + use crate::get_reader; #[test] - fn test_process_encode() { + fn test_process_encode() -> Result<()> { let input = "Cargo.toml"; + let mut reader = get_reader(input)?; let format = Base64Format::Standard; - assert!(process_encode(input, format).is_ok()); + assert!(process_encode(&mut reader, format).is_ok()); + Ok(()) } #[test] - fn test_process_decode() { + fn test_process_decode() -> Result<()> { let input = "fixtures/b64.txt"; + let mut reader = get_reader(input)?; let format = Base64Format::UrlSafe; - process_decode(input, format).unwrap(); + process_decode(&mut reader, format)?; + + Ok(()) } } diff --git a/src/process/gen_pass.rs b/src/process/gen_pass.rs index efb96f5..3100b43 100644 --- a/src/process/gen_pass.rs +++ b/src/process/gen_pass.rs @@ -1,5 +1,4 @@ use rand::seq::SliceRandom; -use zxcvbn::zxcvbn; const UPPER: &[u8] = b"ABCDEFGHJKLMNPQRSTUVWXYZ"; const LOWER: &[u8] = b"abcdefghijkmnopqrstuvwxyz"; @@ -12,7 +11,7 @@ pub fn process_genpass( lower: bool, number: bool, symbol: bool, -) -> anyhow::Result<()> { +) -> anyhow::Result { let mut rng = rand::thread_rng(); let mut password = Vec::new(); let mut chars = Vec::new(); @@ -43,12 +42,5 @@ pub fn process_genpass( password.shuffle(&mut rng); - let password = String::from_utf8(password)?; - println!("{}", password); - - // output password strength in stderr - let estimate = zxcvbn(&password, &[])?; - eprintln!("Password strength: {}", estimate.score()); - - Ok(()) + Ok(String::from_utf8(password)?) } diff --git a/src/process/mod.rs b/src/process/mod.rs index 96e9fd7..15812a0 100644 --- a/src/process/mod.rs +++ b/src/process/mod.rs @@ -1,7 +1,9 @@ mod b64; mod csv_convert; mod gen_pass; +mod text; pub use b64::{process_decode, process_encode}; pub use csv_convert::process_csv; pub use gen_pass::process_genpass; +pub use text::{process_text_key_generate, process_text_sign, process_text_verify}; diff --git a/src/process/text.rs b/src/process/text.rs new file mode 100644 index 0000000..817169e --- /dev/null +++ b/src/process/text.rs @@ -0,0 +1,180 @@ +use crate::{process_genpass, TextSignFormat}; +use anyhow::Result; +use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey}; +use rand::rngs::OsRng; +use std::{collections::HashMap, io::Read}; + +pub trait TextSigner { + // signer could sign any input data + fn sign(&self, reader: &mut dyn Read) -> Result>; +} + +pub trait TextVerifier { + // verifier could verify any input data + fn verify(&self, reader: &mut dyn Read, sig: &[u8]) -> Result; +} + +pub struct Blake3 { + key: [u8; 32], +} + +pub struct Ed25519Signer { + key: SigningKey, +} + +pub struct Ed25519Verifier { + key: VerifyingKey, +} + +impl TextSigner for Blake3 { + fn sign(&self, reader: &mut dyn Read) -> Result> { + let mut buf = Vec::new(); + reader.read_to_end(&mut buf)?; + let ret = blake3::keyed_hash(&self.key, &buf); + Ok(ret.as_bytes().to_vec()) + } +} + +impl TextVerifier for Blake3 { + fn verify(&self, reader: &mut dyn Read, sig: &[u8]) -> Result { + let mut buf = Vec::new(); + reader.read_to_end(&mut buf)?; + let ret = blake3::keyed_hash(&self.key, &buf); + Ok(ret.as_bytes() == sig) + } +} + +impl TextSigner for Ed25519Signer { + fn sign(&self, reader: &mut dyn Read) -> Result> { + let mut buf = Vec::new(); + reader.read_to_end(&mut buf)?; + let signature = self.key.sign(&buf); + Ok(signature.to_bytes().to_vec()) + } +} + +impl TextVerifier for Ed25519Verifier { + fn verify(&self, reader: &mut dyn Read, sig: &[u8]) -> Result { + let mut buf = Vec::new(); + reader.read_to_end(&mut buf)?; + let sig = (&sig[..64]).try_into()?; + let signature = Signature::from_bytes(sig); + Ok(self.key.verify(&buf, &signature).is_ok()) + } +} + +impl Blake3 { + pub fn try_new(key: impl AsRef<[u8]>) -> Result { + let key = key.as_ref(); + // convert &[u8] to &[u8; 32] + let key = (&key[..32]).try_into()?; + Ok(Self::new(key)) + } + + pub fn new(key: [u8; 32]) -> Self { + Self { key } + } + + fn generate() -> Result>> { + let key = process_genpass(32, true, true, true, true)?; + let mut map = HashMap::new(); + map.insert("blake3.txt", key.as_bytes().to_vec()); + Ok(map) + } +} + +impl Ed25519Signer { + pub fn try_new(key: impl AsRef<[u8]>) -> Result { + let key = key.as_ref(); + let key = (&key[..32]).try_into()?; + Ok(Self::new(key)) + } + + pub fn new(key: &[u8; 32]) -> Self { + let key = SigningKey::from_bytes(key); + Self { key } + } + + fn generate() -> Result>> { + let mut csprng = OsRng; + let sk: SigningKey = SigningKey::generate(&mut csprng); + let pk: VerifyingKey = (&sk).into(); + let mut map = HashMap::new(); + map.insert("ed25519.sk", sk.to_bytes().to_vec()); + map.insert("ed25519.pk", pk.to_bytes().to_vec()); + + Ok(map) + } +} + +impl Ed25519Verifier { + pub fn try_new(key: impl AsRef<[u8]>) -> Result { + let key = key.as_ref(); + let key = (&key[..32]).try_into()?; + let key = VerifyingKey::from_bytes(key)?; + Ok(Self { key }) + } +} + +pub fn process_text_sign( + reader: &mut dyn Read, + key: &[u8], // (ptr, length) + format: TextSignFormat, +) -> Result> { + let signer: Box = match format { + TextSignFormat::Blake3 => Box::new(Blake3::try_new(key)?), + TextSignFormat::Ed25519 => Box::new(Ed25519Signer::try_new(key)?), + }; + + signer.sign(reader) +} + +pub fn process_text_verify( + reader: &mut dyn Read, + key: &[u8], + sig: &[u8], + format: TextSignFormat, +) -> Result { + let verifier: Box = match format { + TextSignFormat::Blake3 => Box::new(Blake3::try_new(key)?), + TextSignFormat::Ed25519 => Box::new(Ed25519Verifier::try_new(key)?), + }; + verifier.verify(reader, sig) +} + +pub fn process_text_key_generate(format: TextSignFormat) -> Result>> { + match format { + TextSignFormat::Blake3 => Blake3::generate(), + TextSignFormat::Ed25519 => Ed25519Signer::generate(), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; + + const KEY: &[u8] = include_bytes!("../../fixtures/blake3.txt"); + + #[test] + fn test_process_text_sign() -> Result<()> { + let mut reader = "hello".as_bytes(); + let mut reader1 = "hello".as_bytes(); + let format = TextSignFormat::Blake3; + let sig = process_text_sign(&mut reader, KEY, format)?; + let ret = process_text_verify(&mut reader1, KEY, &sig, format)?; + assert!(ret); + Ok(()) + } + + #[test] + fn test_process_text_verify() -> Result<()> { + let mut reader = "hello".as_bytes(); + let format = TextSignFormat::Blake3; + let sig = "33Ypo4rveYpWmJKAiGnnse-wHQhMVujjmcVkV4Tl43k"; + let sig = URL_SAFE_NO_PAD.decode(sig)?; + let ret = process_text_verify(&mut reader, KEY, &sig, format)?; + assert!(ret); + Ok(()) + } +} diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..9a4f7d3 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,18 @@ +use anyhow::Result; +use std::{fs::File, io::Read}; + +pub fn get_reader(input: &str) -> Result> { + let reader: Box = if input == "-" { + Box::new(std::io::stdin()) + } else { + Box::new(File::open(input)?) + }; + Ok(reader) +} + +pub fn get_content(input: &str) -> Result> { + let mut reader = get_reader(input)?; + let mut buf = Vec::new(); + reader.read_to_end(&mut buf)?; + Ok(buf) +}