diff --git a/Cargo.lock b/Cargo.lock index 420b4841ec2a2751b8df910f8465843d464ec740..3ad7041f45acf746279c31f58bd6a20da90c49f5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,220 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "base64" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" + +[[package]] +name = "base64ct" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a32fd6af2b5827bce66c29053ba0e7c42b9dcab01835835058558c10851a46b" + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bs58" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "771fe0050b883fcc3ea2359b1a96bcfbc090b7116eae7c3c512c7a083fdf23d3" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cipher" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" +dependencies = [ + "generic-array", +] + +[[package]] +name = "cpufeatures" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66c99696f6c9dd7f35d486b9d04d7e6e202aa3e8c40d553f2fdf5e7e0c6a71ef" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-mac" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714" +dependencies = [ + "generic-array", + "subtle", +] + +[[package]] +name = "cryptoxide" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8c4fdc86023bc33b265f256ce8205329125b86c38a8a96e243a6a705b7230ec" + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + [[package]] name = "duniter-mini-client" version = "0.1.0" +dependencies = [ + "base64", + "bs58", + "cryptoxide", + "scrypt", +] + +[[package]] +name = "generic-array" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "hmac" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b" +dependencies = [ + "crypto-mac", + "digest", +] + +[[package]] +name = "libc" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1fa8cddc8fbbee11227ef194b5317ed014b8acbf15139bd716a18ad3fe99ec5" + +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + +[[package]] +name = "password-hash" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77e0b28ace46c5a396546bcf443bf422b57049617433d8854227352a4a9b24e7" +dependencies = [ + "base64ct", + "rand_core", + "subtle", +] + +[[package]] +name = "pbkdf2" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d95f5254224e617595d2cc3cc73ff0a5eaf2637519e25f03388154e9378b6ffa" +dependencies = [ + "crypto-mac", +] + +[[package]] +name = "rand_core" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +dependencies = [ + "getrandom", +] + +[[package]] +name = "salsa20" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecbd2eb639fd7cab5804a0837fe373cc2172d15437e804c054a9fb885cb923b0" +dependencies = [ + "cipher", +] + +[[package]] +name = "scrypt" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879588d8f90906e73302547e20fffefdd240eb3e0e744e142321f5d49dea0518" +dependencies = [ + "base64ct", + "hmac", + "password-hash", + "pbkdf2", + "salsa20", + "sha2", +] + +[[package]] +name = "sha2" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b362ae5752fd2137731f9fa25fd4d9058af34666ca1966fb969119cc35719f12" +dependencies = [ + "block-buffer", + "cfg-if", + "cpufeatures", + "digest", + "opaque-debug", +] + +[[package]] +name = "subtle" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" + +[[package]] +name = "typenum" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06" + +[[package]] +name = "version_check" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" + +[[package]] +name = "wasi" +version = "0.10.2+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" diff --git a/Cargo.toml b/Cargo.toml index 582e632761bedbe6a1ad365b479f075ec6e6639d..23fe1ea1b4453339116503fc4a79e1ac30b49082 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,3 +6,7 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +scrypt = "0.7.0" +base64 = "0.13.0" +bs58 = "0.4.0" +cryptoxide = "0.3.3" \ No newline at end of file diff --git a/src/cli/mod.rs b/src/cli/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..e2b2546be4c6c5a70cde25bf1ecc55bc55350722 --- /dev/null +++ b/src/cli/mod.rs @@ -0,0 +1,42 @@ +use std::env::Args; + +use crate::cli::Command::*; + +pub enum Command { + /// Compute the public key from salt/passwd and displays it + PUB(String, String), + /// Compute the secret key from salt/passwd and displays it + SEC(String, String), + /// Compute the keyring from salt/passwd and displays it + KEYRING(String, String), + /// Unknown command + UNKNOWN(String) +} + +impl Command { + + pub fn from(mut args: Args) -> Command { + args.next(); + let command = args.next().expect("Command must be provided"); + + let command = command.as_str(); + match command { + "pub" => { + let salt = args.next().expect("Salt must be provided"); + let passwd = args.next().expect("Password must be provided"); + PUB(salt, passwd) + }, + "sec" => { + let salt = args.next().expect("Salt must be provided"); + let passwd = args.next().expect("Password must be provided"); + SEC(salt, passwd) + }, + "keyring" => { + let salt = args.next().expect("Salt must be provided"); + let passwd = args.next().expect("Password must be provided"); + KEYRING(salt, passwd) + }, + _ => UNKNOWN(String::from(command)), + } + } +} \ No newline at end of file diff --git a/src/crypto/duniter_key.rs b/src/crypto/duniter_key.rs new file mode 100644 index 0000000000000000000000000000000000000000..4b3aba2e98fb0e90d8494f5037b09e22a5c96972 --- /dev/null +++ b/src/crypto/duniter_key.rs @@ -0,0 +1,65 @@ +use std::error::Error; +use std::fmt::Formatter; + +use bs58; +use cryptoxide::ed25519; +use scrypt::{Params, Scrypt}; +use scrypt::password_hash::{Output, PasswordHasher, SaltString}; + +use crate::crypto::duniter_signature::DuniterSignature; + +/// The main functional class for handle Duniter crypto. +pub struct DuniterKey { + pub public: [u8; 32], + pub secret: [u8; 64], +} + +/// How to display a duniter keyring: "pub_base58:sec_base58" format. +impl std::fmt::Display for DuniterKey { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}:{}", bs58::encode(&self.public).into_string(), bs58::encode(&self.secret).into_string()) + } +} + +impl DuniterKey { + + pub fn from_scrypt(salt: &str, passwd: &str) -> Result<DuniterKey, Box<dyn Error>> { + let seed = derive_seed(salt, passwd)?; + let (secret, public) = ed25519::keypair(seed.as_bytes()); + Ok(DuniterKey { public, secret }) + } + + pub fn from_base58(public: &str, secret: &str) -> Result<DuniterKey, Box<dyn Error>> { + let public_vec = bs58::decode(public).into_vec().unwrap(); + let secret_vec = bs58::decode(secret).into_vec().unwrap(); + let mut public = [0u8; 32]; + for (i, v) in public_vec.iter().enumerate() { + if i < 32 { + public[i] = *v; + } + } + let mut secret = [0u8; 64]; + for (i, v) in secret_vec.iter().enumerate() { + if i < 64 { + secret[i] = *v; + } + } + Ok(DuniterKey { public, secret }) + } + + pub fn sign(&self, message: &str) -> DuniterSignature { + DuniterSignature::new(ed25519::signature(message.as_bytes(), &self.secret[..])) + } + + pub fn verify(&self, message: &str, signature: &DuniterSignature) -> bool { + ed25519::verify(message.as_bytes(), &self.public, &signature.as_bytes()) + } +} + +fn derive_seed(salt: &str, passwd: &str) -> Result<Output, Box<dyn Error>> { + let saltb64 = base64::encode(salt); // convert utf8 to base64 + let salt = SaltString::new(saltb64.as_str())?; // expectes base64-encoded str + let params = Params::new(12, 16, 1)?; + let password_hash = Scrypt.hash_password(passwd.as_bytes(), None, params, &salt)?; + Ok(password_hash.hash.expect("Seed should have been generated")) +} \ No newline at end of file diff --git a/src/crypto/duniter_signature.rs b/src/crypto/duniter_signature.rs new file mode 100644 index 0000000000000000000000000000000000000000..770fecb26a3e3c47e3b1656ca54ba5977486c691 --- /dev/null +++ b/src/crypto/duniter_signature.rs @@ -0,0 +1,21 @@ +use std::fmt::Formatter; + +const SIGNATURE_LENGTH: usize = 64; + +pub struct DuniterSignature([u8; SIGNATURE_LENGTH]); + +/// A DuniterSignature wraps an actuel signature (64 bytes data) +impl DuniterSignature { + pub fn new(signature: [u8; SIGNATURE_LENGTH]) -> DuniterSignature { + DuniterSignature(signature) + } + pub fn as_bytes(&self) -> [u8; SIGNATURE_LENGTH] { + self.0 + } +} + +impl std::fmt::Display for DuniterSignature { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", base64::encode(self.0)) + } +} \ No newline at end of file diff --git a/src/crypto/mod.rs b/src/crypto/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..e838147252916b52ea0d1d65b9b31b871cd853a1 --- /dev/null +++ b/src/crypto/mod.rs @@ -0,0 +1,4 @@ +mod duniter_signature; + +/// The API of crypto module. +pub mod duniter_key; diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000000000000000000000000000000000000..85f9fdbf919000a5cd94224ba4d1df7ed82976c2 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,64 @@ +mod crypto; + +use crate::crypto::duniter_key::DuniterKey; + +pub fn compute_pub(salt: &str, passwd: &str) -> String { + let key = DuniterKey::from_scrypt(salt, passwd).expect("Error generating key from scrypt"); + String::from(format!("{}", key).split(":").next().unwrap()) +} + +pub fn compute_sec(salt: &str, passwd: &str) -> String { + let key = DuniterKey::from_scrypt(salt, passwd).expect("Error generating key from scrypt"); + let key_str = key.to_string(); + let mut split = key_str.split(":"); + split.next(); + String::from(split.next().unwrap()) +} + +pub fn compute_key(salt: &str, passwd: &str) -> String { + let key = DuniterKey::from_scrypt(salt, passwd).expect("Error generating key from scrypt"); + format!("{}", key) +} + +#[cfg(test)] +mod tests { + + use crate::*; + + const SALT: &str = "test_salt"; + const PASSWD: &str = "test_passwd"; + const PUBKEY: &str = "953SE7QJoDC2vFFwkSDojGKjUFU6BCu1kH6s4MnoyQCw"; + const SECKEY: &str = "s3G7v2FPuTSWh8f9BSrU8EurwcM214x4fMLWFDLYmH7doqfyCqM96Ai5q4xB8y4GBKTCUvHoR3Et8AJKf7XTTZR"; + + #[test] + fn should_compute_pub() { + assert_eq!(compute_pub(SALT, PASSWD), PUBKEY); + } + + #[test] + fn should_compute_sec() { + assert_eq!(compute_sec(SALT, PASSWD), SECKEY); + } + + #[test] + fn should_compute_keyring() { + assert_eq!(compute_key(SALT, PASSWD), format!("{}:{}", PUBKEY, SECKEY)); + } + + #[test] + fn base58_or_scrypt_is_the_same() { + let scrypt_key = DuniterKey::from_scrypt(SALT, PASSWD).expect("Error generating key from scrypt"); + let bs58_key = DuniterKey::from_base58(PUBKEY, SECKEY).expect("Wrong public/secret key"); + assert_eq!(bs58_key.public, scrypt_key.public); + assert_eq!(bs58_key.secret, scrypt_key.secret); + } + + #[test] + fn should_verify_or_reject_sig() { + let bs58_key = DuniterKey::from_base58(PUBKEY, SECKEY).expect("Wrong public/secret key"); + let key = bs58_key; + let signature = key.sign("GOOD MESSAGE"); + assert_eq!(true, key.verify("GOOD MESSAGE", &signature)); + assert_eq!(false, key.verify("WRONG MESSAGE", &signature)); + } +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index e7a11a969c037e00a796aafeff6258501ec15e9a..368af760ffd9e434d676e3c1d5e581c40ee011ba 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,20 @@ +use std::env; + +use duniter_mini_client::{compute_pub, compute_sec, compute_key}; + +use crate::cli::Command; +pub use crate::cli::Command::{PUB, UNKNOWN}; +use crate::cli::Command::{SEC, KEYRING}; + +mod cli; + fn main() { - println!("Hello, world!"); -} + let command = Command::from(env::args()); + + match command { + PUB(salt, passwd) => println!("{}", compute_pub(salt.as_str(), passwd.as_str())), + SEC(salt, passwd) => println!("{}", compute_sec(salt.as_str(), passwd.as_str())), + KEYRING(salt, passwd) => println!("{}", compute_key(salt.as_str(), passwd.as_str())), + UNKNOWN(cmd) => eprintln!("Unknown command {}", cmd), + }; +} \ No newline at end of file