diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0ec60d6..e0905eb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,4 +1,4 @@ -name: Shuttle Deploy +name: Deploy on: push: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..a84877a --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,19 @@ +name: Test + +on: + push: + branches: [ '*' ] + +env: + CARGO_TERM_COLOR: always + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Build + run: cargo build --verbose + - name: Run tests + run: cargo test --verbose \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 18fcf08..1cd3768 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -306,6 +306,16 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.4" @@ -421,7 +431,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" dependencies = [ "cfg-if", - "hashbrown 0.14.0", + "hashbrown 0.14.1", "lock_api", "once_cell", "parking_lot_core", @@ -508,9 +518,9 @@ dependencies = [ [[package]] name = "errno" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "136526188508e25c6fef639d7927dfb3e0e3084488bf202267829cf7fc23dbdd" +checksum = "add4f07d43996f76ef320709726a556a9d4f965d9410d8d0271132d2f8293480" dependencies = [ "errno-dragonfly", "libc", @@ -716,9 +726,9 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" -version = "0.14.0" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" +checksum = "7dfda62a12f55daeae5015f81b0baea145391cb4520f86c248fc615d72640d12" [[package]] name = "headers" @@ -1017,9 +1027,9 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-raw-sys" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a9bad9f94746442c783ca431b22403b519cd7fbeed0533fdd6328b2f2212128" +checksum = "3852614a3bd9ca9804678ba6be5e3b8ce76dfc902cae004e3e0c44051b6e88db" [[package]] name = "lock_api" @@ -1054,9 +1064,9 @@ checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" [[package]] name = "memchr" -version = "2.6.3" +version = "2.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" +checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" [[package]] name = "merlin" @@ -1527,13 +1537,13 @@ dependencies = [ [[package]] name = "regex" -version = "1.9.5" +version = "1.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "697061221ea1b4a94a624f67d0ae2bfe4e22b8a17b6a192afb11046542cc8c47" +checksum = "ebee201405406dbf528b8b672104ae6d6d63e6d118cb10e4d51abbc7b58044ff" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.3.8", + "regex-automata 0.3.9", "regex-syntax 0.7.5", ] @@ -1548,9 +1558,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2f401f4955220693b56f8ec66ee9c78abffd8d1c4f23dc41a23839eb88f0795" +checksum = "59b23e92ee4318893fa3fe3e6fb365258efbfe6ac6ab30f090cdcbb7aa37efa9" dependencies = [ "aho-corasick", "memchr", @@ -1571,9 +1581,9 @@ checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" [[package]] name = "reqwest" -version = "0.11.20" +version = "0.11.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e9ad3fe7488d7e34558a2033d45a0c90b72d97b4f80705666fea71472e2e6a1" +checksum = "046cd98826c46c2ac8ddecae268eb5c2e58628688a5fc7a2643704a73faba95b" dependencies = [ "base64 0.21.4", "bytes", @@ -1597,6 +1607,7 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", + "system-configuration", "tokio", "tokio-rustls", "tower-service", @@ -1659,9 +1670,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustix" -version = "0.38.14" +version = "0.38.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "747c788e9ce8e92b12cd485c49ddf90723550b654b32508f979b71a7b1ecda4f" +checksum = "f25469e9ae0f3d0047ca8b93fc56843f38e6774f0914a107ff8b41be8be8e0b7" dependencies = [ "bitflags 2.4.0", "errno", @@ -2161,6 +2172,27 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "thiserror" version = "1.0.49" diff --git a/src/hasher.rs b/src/hasher.rs new file mode 100644 index 0000000..6b32520 --- /dev/null +++ b/src/hasher.rs @@ -0,0 +1,41 @@ +pub mod base64 { + use anyhow::Error; + use base64::{engine::general_purpose, Engine}; + + pub fn encode(input: &[u8]) -> String { + general_purpose::STANDARD_NO_PAD.encode(input) + } + + pub fn decode(input: &str) -> Result, Error> { + general_purpose::STANDARD_NO_PAD.decode(input) + .map_err(|_| Error::msg("Error on decoding")) + } +} + +mod hasher_tess { + use crate::hasher; + + #[test] + fn test_base64_encode() { + // Given + let input = "Hello World!"; + + // When + let result = hasher::base64::encode(input.as_bytes()); + + // Then + assert_eq!(result, "SGVsbG8gV29ybGQh"); + } + + #[test] + fn test_base64_decode() { + // Given + let input = "SGVsbG8gV29ybGQh"; + + // When + let result = hasher::base64::decode(input).unwrap(); + + // Then + assert_eq!(result, "Hello World!".as_bytes()); + } +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 6090d4e..323f7c9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,18 +1,17 @@ -use std::fs::read_to_string; +mod vault; +mod hasher; +use std::fs::read_to_string; use anyhow::Error; use axum::{routing::get, routing::post, Router, response::IntoResponse, http::{HeaderMap, header, StatusCode}, Json}; -use bip39::{Mnemonic, MnemonicType, Language}; use json::JsonValue; -use schnorrkel::{keys, Keypair, Signature, PublicKey}; -use substrate_bip39::mini_secret_from_entropy; +use schnorrkel::Signature; use serde::Deserialize; -use base64::{Engine, engine::general_purpose}; +use vault::keystore::PublicKey; #[macro_use] extern crate json; -const LANGUAGE: Language = Language::Italian; #[shuttle_runtime::main] async fn axum() -> shuttle_axum::ShuttleAxum { @@ -36,11 +35,8 @@ async fn get_doc_file() -> impl IntoResponse { * Generate a new random menmonic */ async fn generate_seed() -> impl IntoResponse { - let mnemonic: Mnemonic = Mnemonic::new(MnemonicType::Words12, LANGUAGE); - let words: Vec<&str> = mnemonic.phrase().split(" ").collect(); - let response = object! { - "mnemonic": words + "mnemonic": vault::mnemonic::generate() }; create_json_response(response).into_response() @@ -52,21 +48,17 @@ async fn generate_seed() -> impl IntoResponse { * @param payload Body */ async fn sign_message(Json(payload): Json) -> impl IntoResponse { - let key_pair_result: Result = Mnemonic::from_phrase(&payload.mnemonic.join(" "), LANGUAGE) - .map_err(|_| Error::msg("Invalid Mnemonic")) - .and_then(|m| mini_secret_from_entropy(m.entropy(), "").map_err(|_|Error::msg("Invalid Secret"))) - .and_then(|s|Ok(s.expand_to_keypair(keys::ExpansionMode::Uniform))); - - match key_pair_result { - Ok(key_pair) => { - let signature = key_pair.sign_simple(&[], &payload.message.as_bytes()).to_bytes(); - let hashed_signature = general_purpose::STANDARD_NO_PAD.encode(signature); - let public_key = general_purpose::STANDARD_NO_PAD.encode(key_pair.public.to_bytes()); + let private_key = vault::keystore::private_key_from_phrase(&payload.mnemonic) + .map_err(|_| Error::msg("Invalid Mnemonic")); + + match private_key { + Ok(keystore) => { + let signature = keystore.sign(&payload.message).to_bytes(); let response: JsonValue = object! { "message": payload.message, - "signature": hashed_signature, - "public_key": public_key + "signature": hasher::base64::encode(&signature), + "public_key": hasher::base64::encode(&keystore.public_key().to_bytes()) }; create_json_response(response).into_response() @@ -81,31 +73,23 @@ async fn sign_message(Json(payload): Json) -> impl IntoRespo * @param payload */ async fn verify_message(Json(payload): Json) -> impl IntoResponse { - let public_key_result: Result = decode_hash(&payload.public_key) + let public_key_result: Result = hasher::base64::decode(&payload.public_key) .and_then(|v: Vec| -> Result<[u8; 32], _> { - v.as_slice() - .try_into() - .map_err(|_| Error::msg("Invalid public key")) + v.as_slice().try_into().map_err(|_| Error::msg("Invalid signature key")) }) - .and_then(|h: [u8; 32]| -> Result { - PublicKey::from_bytes(&h).map_err(|_| Error::msg("Invalid public key")) - }); + .and_then(|h|PublicKey::new(h).map_err(|_| Error::msg("Invalid public key"))); - let signature_result: Result = decode_hash(&payload.signature) + let signature_result: Result = hasher::base64::decode(&payload.signature) .and_then(|v: Vec| -> Result<[u8; 64], _> { - v.as_slice() - .try_into() - .map_err(|_| Error::msg("Invalid signature key")) + v.as_slice().try_into().map_err(|_| Error::msg("Invalid signature key")) }) - .and_then(|h: [u8; 64]| -> Result { - Signature::from_bytes(&h).map_err(|_| Error::msg("Invalid signature key")) - }); + .and_then(|h| Signature::from_bytes(&h).map_err(|_| Error::msg("Invalid signature key"))); match public_key_result { Ok(public_key) => { match signature_result { Ok(signature) => { - let matched_signature = public_key.verify_simple(&[], &payload.message.as_bytes(), &signature).is_ok(); + let matched_signature = public_key.verify(&payload.message, &signature).is_ok(); let response = object! { "message": payload.message, "match": matched_signature @@ -130,12 +114,6 @@ fn bad_request() -> impl IntoResponse { (StatusCode::BAD_REQUEST, "".to_string()) } -fn decode_hash(str: &String) -> Result, Error> { - general_purpose::STANDARD_NO_PAD - .decode(str) - .map_err(|_| Error::msg("Error on decoding")) -} - #[derive(Deserialize)] struct SignMessagePayload { message: String, @@ -150,7 +128,7 @@ struct VerifyMessagePayload { } #[cfg(test)] -mod tests { +mod api_tests { use tokio::runtime::Runtime; use super::*; diff --git a/src/vault.rs b/src/vault.rs new file mode 100644 index 0000000..3928f68 --- /dev/null +++ b/src/vault.rs @@ -0,0 +1,128 @@ +use bip39::Language; + +const LANGUAGE: Language = Language::Italian; + +pub mod mnemonic { + use bip39::{Mnemonic, MnemonicType}; + + /** + * Generate a new random menmonic + */ + pub fn generate() -> Vec { + let mnemonic: Mnemonic = Mnemonic::new(MnemonicType::Words12, super::LANGUAGE); + mnemonic.phrase().split(" ").map(|w| w.to_string()).collect() + } + +} + +pub mod keystore { + use bip39::Mnemonic; + use substrate_bip39::mini_secret_from_entropy; + use schnorrkel::{Keypair, keys, Signature}; + use anyhow::Error; + + /** + * Get a keystore from a mnemonic phrase + */ + pub fn private_key_from_phrase(words: &Vec) -> Result { + Mnemonic::from_phrase(words.join(" ").as_str(), super::LANGUAGE) + .and_then(|m| PrivateKey::new(m)) + .map_err(|_| Error::msg("Invalid mnemonic")) + } + + pub struct PrivateKey { + key_pair: Keypair + } + + impl PrivateKey { + + /** + * Create a new keystore from a mnemonic + */ + pub fn new(mnemonic: Mnemonic) -> Result { + mini_secret_from_entropy(mnemonic.entropy(), "") + .map(|secret_key| secret_key.expand_to_keypair(keys::ExpansionMode::Uniform)) + .map(|key_pair| PrivateKey { key_pair }) + .map_err(|_| Error::msg("Invalid entrophy or password")) + } + + /** + * Sign a message and return the signature + */ + pub fn sign(&self, message: &String) -> Signature { + self.key_pair.sign_simple(&[], &message.as_bytes()) + } + + /** + * Get the public key + */ + pub fn public_key(&self) -> PublicKey { + PublicKey::new(self.key_pair.public.to_bytes()).unwrap() + } + + } + + pub struct PublicKey { + key: schnorrkel::PublicKey + } + + impl PublicKey { + pub fn new(bytes: [u8; 32]) -> Result { + schnorrkel::PublicKey::from_bytes(&bytes) + .map(|key| PublicKey { key }) + .map_err(|_| Error::msg("Invalid public key")) + } + + pub fn verify(&self, message: &String, signature: &Signature) -> Result<(), Error> { + self.key.verify_simple(&[], &message.as_bytes(), &signature) + .map_err(|_| Error::msg("Invalid signature")) + } + + #[inline] + pub fn to_bytes(&self) -> [u8; 32] { + self.key.to_bytes() + } + } + +} + +#[cfg(test)] +mod vault_tests { + use crate::vault; + + #[test] + fn test_keystore_new() { + // Given + let words: Vec = "scrutinio casaccio cedibile oste tumulto irrorare notturno uffa doganale classico esercito vibrante".split(" ").map(|s| s.to_string()).collect(); + + // When + let result = vault::keystore::private_key_from_phrase(&words); + + // Then + assert!(result.is_ok()); + } + + #[test] + fn test_generate() { + // When + let result = vault::mnemonic::generate(); + + // Then + assert_eq!(result.len(), 12); + } + + #[test] + fn test_sign_message() { + // Given + let words: Vec = "scrutinio casaccio cedibile oste tumulto irrorare notturno uffa doganale classico esercito vibrante".split(" ").map(|s| s.to_string()).collect(); + let private_key = vault::keystore::private_key_from_phrase(&words).unwrap(); + let message = "Hello, world!".to_string(); + + // When + let signature = private_key.sign(&message); + + // Then + assert!(private_key.public_key().verify(&message, &signature).is_ok()); + } + +} \ No newline at end of file