Skip to content

Commit

Permalink
feat: begin porting crypto
Browse files Browse the repository at this point in the history
  • Loading branch information
jurevans committed Dec 18, 2024
1 parent 11539e9 commit e2191d5
Show file tree
Hide file tree
Showing 12 changed files with 1,196 additions and 27 deletions.
197 changes: 171 additions & 26 deletions packages/sdk/lib/Cargo.lock

Large diffs are not rendered by default.

9 changes: 8 additions & 1 deletion packages/sdk/lib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ namada_tx = { git = "https://github.com/anoma/namada", rev = "49a4a5d3260423df19

[dependencies]
async-trait = {version = "0.1.51"}
tiny-bip39 = "0.8.2"
# tiny-bip39 = "0.8.2"
tiny-bip39 = { git = "https://github.com/anoma/tiny-bip39", rev = "743d537349c8deab14409ce726b868dcde90fd8e" }
chrono = "0.4.22"
getrandom = { version = "0.2.7", features = ["js"] }
gloo-utils = { version = "0.1.5", features = ["serde"] }
Expand All @@ -44,6 +45,12 @@ zeroize = "1.6.0"
hex = "0.4.3"
reqwest = "0.11.25"
subtle-encoding = "0.5.1"
aes-gcm = "0.10.1"
argon2 = "0.4.1"
slip10_ed25519 = "0.1.3"
password-hash = "0.3.2"
masp_primitives = { git = "https://github.com/anoma/masp", tag = "v1.1.0" }
borsh-ext = { git = "https://github.com/heliaxdev/borsh-ext", tag = "v1.2.0" }

[dependencies.web-sys]
version = "0.3.4"
Expand Down
99 changes: 99 additions & 0 deletions packages/sdk/lib/src/crypto/aes.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
use crate::crypto::pointer_types::VecU8Pointer;
use aes_gcm::{
aead::{generic_array::GenericArray, Aead, KeyInit},
Aes256Gcm, Nonce,
};
use thiserror::Error;
use wasm_bindgen::prelude::*;
use zeroize::Zeroize;

#[derive(Debug, Error)]
pub enum AESError {
#[error("Invalid key size! Minimum key size is 32.")]
KeyLengthError,
#[error("Invalid IV! Expected 96 bits (12 bytes)")]
IVSizeError,
}

#[wasm_bindgen]
pub struct AES {
cipher: Aes256Gcm,
iv: [u8; 12],
}

#[wasm_bindgen]
impl AES {
#[wasm_bindgen(constructor)]
pub fn new(key: VecU8Pointer, iv: Vec<u8>) -> Result<AES, String> {
if key.length < 32 {
return Err(format!(
"{} Received {}",
AESError::KeyLengthError,
key.length
));
}
let mut key = GenericArray::from_iter(key.vec.clone().into_iter());
let iv: [u8; 12] = match iv.try_into() {
Ok(iv) => iv,
Err(_) => {
key.zeroize();
return Err(AESError::IVSizeError.to_string());
}
};

let aes = AES {
cipher: Aes256Gcm::new(&key),
iv,
};
key.zeroize();
Ok(aes)
}

pub fn encrypt(&self, mut text: String) -> Result<Vec<u8>, String> {
let nonce = Nonce::from_slice(&self.iv);
let result = self
.cipher
.encrypt(nonce, text.as_ref())
.map_err(|err| err.to_string());
text.zeroize();
result
}

pub fn decrypt(&self, ciphertext: Vec<u8>) -> Result<VecU8Pointer, String> {
let nonce = Nonce::from_slice(&self.iv);
let plaintext = self
.cipher
.decrypt(nonce, ciphertext.as_ref())
.map_err(|err| err.to_string())?;

Ok(VecU8Pointer::new(plaintext))
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::crypto::rng::{ByteSize, Rng};
use wasm_bindgen_test::*;

#[wasm_bindgen_test]
fn can_encrypt_and_decrypt() {
let key = Rng::generate_bytes(Some(ByteSize::N32))
.expect("Generating random bytes should not fail");
let iv = Rng::generate_bytes(Some(ByteSize::N12))
.expect("Generating random bytes should not fail");
let aes = AES::new(VecU8Pointer::new(key), iv).unwrap();
let plaintext = "my secret message";
let encrypted = aes
.encrypt(String::from(plaintext))
.expect("AES should not fail encrypting plaintext");

let decrypted: &[u8] = &aes
.decrypt(encrypted)
.expect("AES should not fail decrypting ciphertext")
.vec;
let decrypted = std::str::from_utf8(decrypted).expect("Should parse as string");

assert_eq!(decrypted, plaintext);
}
}
214 changes: 214 additions & 0 deletions packages/sdk/lib/src/crypto/argon2.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
use crate::crypto::pointer_types::VecU8Pointer;
use password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString};
use wasm_bindgen::prelude::*;
use zeroize::{Zeroize, ZeroizeOnDrop};

#[wasm_bindgen]
pub struct Argon2Params {
m_cost: u32,
t_cost: u32,
p_cost: u32,
}

#[wasm_bindgen]
impl Argon2Params {
#[wasm_bindgen(constructor)]
pub fn new(m_cost: u32, t_cost: u32, p_cost: u32) -> Self {
Self {
m_cost,
t_cost,
p_cost,
}
}

#[wasm_bindgen(getter)]
pub fn m_cost(&self) -> u32 {
self.m_cost
}

#[wasm_bindgen(getter)]
pub fn t_cost(&self) -> u32 {
self.t_cost
}

#[wasm_bindgen(getter)]
pub fn p_cost(&self) -> u32 {
self.p_cost
}
}

#[wasm_bindgen]
#[derive(ZeroizeOnDrop)]
pub struct Argon2 {
#[zeroize(skip)]
salt: SaltString,
password: Vec<u8>,
#[zeroize(skip)]
params: argon2::Params,
}

/// Argon2 password hashing
#[wasm_bindgen]
impl Argon2 {
#[wasm_bindgen(constructor)]
pub fn new(
password: String,
salt: Option<String>,
params: Option<Argon2Params>,
) -> Result<Argon2, String> {
let password = Vec::from(password.as_bytes());
let default_params = argon2::Params::default();

let salt = match salt {
Some(salt) => SaltString::new(&salt).map_err(|err| err.to_string())?,
None => SaltString::generate(&mut OsRng),
};

let params = match params {
Some(params) => argon2::Params::new(params.m_cost, params.t_cost, params.p_cost, None)
.map_err(|err| err.to_string())?,
None => default_params,
};

Ok(Argon2 {
salt,
password,
params,
})
}

pub fn to_hash(&self) -> Result<String, String> {
let argon2 = argon2::Argon2::default();
let bytes: &[u8] = &self.password;
let params = &self.params;

// Hash password to PHC string ($argon2id$v=19$...)
let password_hash = argon2
.hash_password_customized(
bytes,
None, // Default alg_id = Argon2id
None, // Default ver = v19
params.to_owned(),
&self.salt,
)
.map_err(|err| err.to_string())?
.to_string();

Ok(password_hash)
}

pub fn verify(&self, hash: String) -> Result<(), String> {
let argon2 = argon2::Argon2::default();
let bytes: &[u8] = &self.password;
let parsed_hash = PasswordHash::new(&hash).map_err(|err| err.to_string())?;

match argon2.verify_password(bytes, &parsed_hash) {
Ok(_) => Ok(()),
Err(err) => Err(err.to_string()),
}
}

pub fn params(&self) -> Argon2Params {
Argon2Params::new(
self.params.m_cost(),
self.params.t_cost(),
self.params.p_cost(),
)
}

/// Convert PHC string to serialized key
pub fn key(&self) -> Result<VecU8Pointer, String> {
let mut hash = self.to_hash()?;
let split = hash.split('$');
let items: Vec<&str> = split.collect();

let key = items[items.len() - 1];
let vec = Vec::from(key.as_bytes());
hash.zeroize();

Ok(VecU8Pointer::new(vec))
}
}

#[cfg(test)]
mod tests {
use super::*;
use wasm_bindgen_test::*;

#[wasm_bindgen_test]
fn can_hash_password() {
let password = "unhackable";
let argon2 = Argon2::new(password.into(), None, None)
.expect("Creating instance with default params should not fail");
let hash = argon2
.to_hash()
.expect("Hashing password with Argon2 should not fail!");

assert!(argon2.verify(hash).is_ok());
}

#[wasm_bindgen_test]
fn can_hash_password_with_custom_params() {
// Memory cost
let m_cost = 2048;
// Iterations/Time cost:
let t_cost = 2;
// Degree of parallelism:
let p_cost = 2;
let params = Argon2Params::new(m_cost, t_cost, p_cost);
let password = "unhackable";
let argon2 = Argon2::new(password.into(), None, Some(params))
.expect("Creating instance with custom params should not fail");

let hash = argon2
.to_hash()
.expect("Hashing password with Argon2 should not fail!");
assert!(argon2.verify(hash).is_ok());
}

#[wasm_bindgen_test]
fn can_verify_stored_hash() {
let password = "unhackable";
let argon2 = Argon2::new(password.into(), None, None)
.expect("Creating instance with default params should not fail");
let stored_hash = "$argon2id$v=19$m=4096,t=3,p=1$0UUjc4ZBOJJLTPrS1mQr1w$orbgGGRzWC0GvplgJuteaDORldnQiJfVumhXSuwO3UE";

// With randomly generated salt, this should not create
// an equivalent hash:
assert_ne!(argon2.to_hash().unwrap(), stored_hash);
assert!(argon2.verify(stored_hash.to_string()).is_ok());
}

#[wasm_bindgen_test]
fn can_verify_stored_hash_with_custom_salt() {
let password = "unhackable";
let salt = String::from("41oVKhMIBZ+oF4efwq7e0A");
let argon2 = Argon2::new(password.into(), Some(salt), None)
.expect("Creating instance with default params should not fail");
let stored_hash = "$argon2id$v=19$m=4096,t=3,p=1$41oVKhMIBZ+oF4efwq7e0A$ec9kY153e/S6z9awayWdUTLdaQowoAxrdo7ZkTjhBl4";

// Providing salt, this should create an equivalent hash:
assert_eq!(argon2.to_hash().unwrap(), stored_hash);
assert!(argon2.verify(stored_hash.to_string()).is_ok());
}

#[wasm_bindgen_test]
fn can_get_key_and_params() {
let password = "unhackable";
let argon2 = Argon2::new(password.into(), None, None)
.expect("Creating instance with default params should not fail");
let hash = argon2
.to_hash()
.expect("Hashing password with Argon2 should not fail!");

assert!(argon2.verify(hash).is_ok());

let params = argon2.params();
let key = argon2.key().expect("Creating key should not fail");

assert_eq!(params.m_cost(), 4096);
assert_eq!(params.t_cost(), 3);
assert_eq!(params.p_cost(), 1);
assert_eq!(key.vec.len(), 43);
}
}
Loading

0 comments on commit e2191d5

Please sign in to comment.