// Copyright (c) 2022-2023 Niko Bonnieure, Par le Peuple, NextGraph.org developers // All rights reserved. // Licensed under the Apache License, Version 2.0 // // or the MIT license , // at your option. All files in the project carrying such // notice may not be copied, modified, or distributed except // according to those terms. // #[macro_use] // extern crate slice_as_array; #[macro_use] extern crate p2p_net; pub mod types; pub mod bip39; use std::io::Cursor; use crate::bip39::bip39_wordlist; use crate::types::*; use aes_gcm_siv::{ aead::{heapless::Vec as HeaplessVec, AeadInPlace, KeyInit}, Aes256GcmSiv, Nonce, }; use argon2::{Algorithm, Argon2, AssociatedData, ParamsBuilder, Version}; use chacha20poly1305::XChaCha20Poly1305; use image::{imageops::FilterType, io::Reader as ImageReader, ImageOutputFormat}; use safe_transmute::transmute_to_bytes; use p2p_repo::types::{PubKey, Site, SiteType, Timestamp}; use p2p_repo::utils::{generate_keypair, now_timestamp, sign, verify}; use rand::{thread_rng, Rng}; use serde_bare::{from_slice, to_vec}; pub fn enc_master_key( master_key: [u8; 32], key: [u8; 32], nonce: u8, wallet_id: WalletId, ) -> Result<[u8; 48], NgWalletError> { let cipher = Aes256GcmSiv::new(&key.into()); let mut nonce_buffer = [0u8; 12]; nonce_buffer[0] = nonce; let nonce = Nonce::from_slice(&nonce_buffer); let mut buffer: HeaplessVec = HeaplessVec::new(); // Note: buffer needs 16-bytes overhead for auth tag buffer.extend_from_slice(&master_key); // Encrypt `buffer` in-place, replacing the plaintext contents with ciphertext cipher .encrypt_in_place(nonce, &to_vec(&wallet_id).unwrap(), &mut buffer) .map_err(|e| NgWalletError::EncryptionError)?; // `buffer` now contains the encrypted master key // println!("cipher {:?}", buffer); Ok(buffer.into_array::<48>().unwrap()) } pub fn dec_master_key( ciphertext: [u8; 48], key: [u8; 32], nonce: u8, wallet_id: WalletId, ) -> Result<[u8; 32], NgWalletError> { let cipher = Aes256GcmSiv::new(&key.into()); let mut nonce_buffer = [0u8; 12]; nonce_buffer[0] = nonce; let nonce = Nonce::from_slice(&nonce_buffer); let mut buffer: HeaplessVec = HeaplessVec::from_slice(&ciphertext).unwrap(); // Note: buffer needs 16-bytes overhead for auth tag // Decrypt `buffer` in-place, replacing its ciphertext context with the original plaintext cipher .decrypt_in_place(nonce, &to_vec(&wallet_id).unwrap(), &mut buffer) .map_err(|e| NgWalletError::DecryptionError)?; Ok(buffer.into_array::<32>().unwrap()) } fn gen_nonce(peer_id: PubKey, nonce: u64) -> [u8; 24] { let mut buffer = Vec::with_capacity(24); buffer.extend_from_slice(&peer_id.slice()[0..16]); buffer.extend_from_slice(&nonce.to_be_bytes()); buffer.try_into().unwrap() } fn gen_associated_data(timestamp: Timestamp, wallet_id: WalletId) -> Vec { let ser_wallet = to_vec(&wallet_id).unwrap(); [ser_wallet, timestamp.to_be_bytes().to_vec()].concat() } pub fn enc_encrypted_block( block: &EncryptedWalletV0, master_key: [u8; 32], peer_id: PubKey, nonce: u64, timestamp: Timestamp, wallet_id: WalletId, ) -> Result, NgWalletError> { let ser_encrypted_block = to_vec(block).map_err(|e| NgWalletError::InternalError)?; let nonce_buffer: [u8; 24] = gen_nonce(peer_id, nonce); let cipher = XChaCha20Poly1305::new(&master_key.into()); let mut buffer: Vec = Vec::with_capacity(ser_encrypted_block.len() + 16); // Note: buffer needs 16-bytes overhead for auth tag buffer.extend_from_slice(&ser_encrypted_block); // Encrypt `buffer` in-place, replacing the plaintext contents with ciphertext cipher .encrypt_in_place( &nonce_buffer.into(), &gen_associated_data(timestamp, wallet_id), &mut buffer, ) .map_err(|e| NgWalletError::EncryptionError)?; // `buffer` now contains the message ciphertext // println!("encrypted_block ciphertext {:?}", buffer); Ok(buffer) } pub fn dec_encrypted_block( mut ciphertext: Vec, master_key: [u8; 32], peer_id: PubKey, nonce: u64, timestamp: Timestamp, wallet_id: WalletId, ) -> Result { let nonce_buffer: [u8; 24] = gen_nonce(peer_id, nonce); let cipher = XChaCha20Poly1305::new(&master_key.into()); // Decrypt `ciphertext` in-place, replacing its ciphertext context with the original plaintext cipher .decrypt_in_place( &nonce_buffer.into(), &gen_associated_data(timestamp, wallet_id), &mut ciphertext, ) .map_err(|e| NgWalletError::DecryptionError)?; // `ciphertext` now contains the decrypted block //println!("decrypted_block {:?}", ciphertext); let decrypted_block = from_slice::(&ciphertext).map_err(|e| NgWalletError::DecryptionError)?; Ok(decrypted_block) } pub fn derive_key_from_pass(pass: Vec, salt: [u8; 16], wallet_id: WalletId) -> [u8; 32] { let params = ParamsBuilder::new() .m_cost(50 * 1024) .t_cost(2) .p_cost(1) .data(AssociatedData::new(wallet_id.slice()).unwrap()) .output_len(32) .build() .unwrap(); let argon = Argon2::new(Algorithm::Argon2id, Version::V0x13, params); let mut out = [0u8; 32]; argon.hash_password_into(&pass, &salt, &mut out).unwrap(); out } pub fn open_wallet_with_pazzle( wallet: Wallet, pazzle: Vec, pin: [u8; 4], ) -> Result { verify(&wallet.content_as_bytes(), wallet.sig(), wallet.id()) .map_err(|e| NgWalletError::InvalidSignature)?; match wallet { Wallet::V0(v0) => { let pazzle_key = derive_key_from_pass( [pazzle, pin.to_vec()].concat(), v0.content.salt_pazzle, v0.id, ); let master_key = dec_master_key( v0.content.enc_master_key_pazzle, pazzle_key, v0.content.master_nonce, v0.id, )?; Ok(EncryptedWallet::V0(dec_encrypted_block( v0.content.encrypted, master_key, v0.content.peer_id, v0.content.nonce, v0.content.timestamp, v0.id, )?)) } } } pub fn open_wallet_with_mnemonic( wallet: Wallet, mnemonic: [u16; 12], pin: [u8; 4], ) -> Result { verify(&wallet.content_as_bytes(), wallet.sig(), wallet.id()) .map_err(|e| NgWalletError::InvalidSignature)?; match wallet { Wallet::V0(v0) => { let mnemonic_key = derive_key_from_pass( [transmute_to_bytes(&mnemonic), &pin].concat(), v0.content.salt_mnemonic, v0.id, ); let master_key = dec_master_key( v0.content.enc_master_key_mnemonic, mnemonic_key, v0.content.master_nonce, v0.id, )?; Ok(EncryptedWallet::V0(dec_encrypted_block( v0.content.encrypted, master_key, v0.content.peer_id, v0.content.nonce, v0.content.timestamp, v0.id, )?)) } } } pub fn display_mnemonic(mnemonic: &[u16; 12]) -> Vec { let res: Vec = mnemonic .into_iter() .map(|i| String::from(bip39_wordlist[*i as usize])) .collect(); res } /// creates a Wallet from a pin, a security text and image (with option to send the bootstrap and wallet to nextgraph.one) /// and returns the Wallet, the pazzle and the mnemonic pub fn create_wallet_v0( security_img: Vec, security_txt: String, pin: [u8; 4], pazzle_length: u8, send_bootstrap: Option<&Bootstrap>, send_wallet: bool, peer_id: PubKey, nonce: u64, ) -> Result<(Wallet, Vec, [u16; 12]), NgWalletError> { // TODO : use some automatically zeroed variable for the 2 first arguments, and for the returned values // pazzle_length can only be 9, 12, or 15 if (pazzle_length != 9 && pazzle_length != 12 && pazzle_length != 15) { return Err(NgWalletError::InvalidPazzleLength); } // cannot submit wallet if we don't submit also the bootstrap if send_bootstrap.is_none() && send_wallet { return Err(NgWalletError::SubmissionError); } // check validity of PIN // shouldn't start with 0 if pin[0] == 0 { return Err(NgWalletError::InvalidPin); } // each digit shouldnt be greater than 9 if pin[0] > 9 || pin[1] > 9 || pin[2] > 9 || pin[3] > 9 { return Err(NgWalletError::InvalidPin); } // check for uniqueness of each digit if pin[1] == pin[0] || pin[1] == pin[2] || pin[1] == pin[3] || pin[2] == pin[0] || pin[2] == pin[3] || pin[3] == pin[0] { return Err(NgWalletError::InvalidPin); } // check for ascending series if pin[1] == pin[0] + 1 && pin[2] == pin[1] + 1 && pin[3] == pin[2] + 1 { return Err(NgWalletError::InvalidPin); } // check for descending series if pin[3] >= 3 && pin[2] == pin[3] - 1 && pin[1] == pin[2] - 1 && pin[0] == pin[1] - 1 { return Err(NgWalletError::InvalidPin); } // check validity of security text let words: Vec<_> = security_txt.split_whitespace().collect(); let new_string = words.join(" "); let count = new_string.chars().count(); if count < 10 || count > 100 { return Err(NgWalletError::InvalidSecurityText); } // check validity of image let decoded_img = ImageReader::new(Cursor::new(security_img)) .with_guessed_format() .map_err(|e| NgWalletError::InvalidSecurityImage)? .decode() .map_err(|e| NgWalletError::InvalidSecurityImage)?; if decoded_img.height() < 150 || decoded_img.width() < 150 { return Err(NgWalletError::InvalidSecurityImage); } let resized_img = decoded_img.resize_to_fill(400, 400, FilterType::Triangle); let buffer: Vec = Vec::with_capacity(100000); let mut cursor = Cursor::new(buffer); resized_img .write_to(&mut cursor, ImageOutputFormat::Jpeg(72)) .map_err(|e| NgWalletError::InvalidSecurityImage)?; // creating the wallet keys let (wallet_key, wallet_id) = generate_keypair(); let site = Site::create(SiteType::Individual).map_err(|e| NgWalletError::InternalError)?; // let mut pazzle_random = vec![0u8; pazzle_length.into()]; // getrandom::getrandom(&mut pazzle_random).map_err(|e| NgWalletError::InternalError)?; let mut pazzle = vec![0u8; pazzle_length.into()]; let mut ran = thread_rng(); for i in &mut pazzle { *i = ran.gen_range(0, 16); } //println!("pazzle {:?}", pazzle); let mut mnemonic = [0u16; 12]; for i in &mut mnemonic { *i = ran.gen_range(0, 2048); } //println!("mnemonic {:?}", display_mnemonic(&mnemonic)); //slice_as_array!(&mnemonic, [String; 12]) //.ok_or(NgWalletError::InternalError)? //.clone(), let encrypted_block = EncryptedWalletV0 { pazzle: pazzle.clone(), mnemonic, pin, sites: vec![site], }; let mut salt_pazzle = [0u8; 16]; getrandom::getrandom(&mut salt_pazzle).map_err(|e| NgWalletError::InternalError)?; let mut salt_mnemonic = [0u8; 16]; getrandom::getrandom(&mut salt_mnemonic).map_err(|e| NgWalletError::InternalError)?; //println!("salt_pazzle {:?}", salt_pazzle); //println!("salt_mnemonic {:?}", salt_mnemonic); let pazzle_key = derive_key_from_pass( [pazzle.clone(), pin.to_vec()].concat(), salt_pazzle, wallet_id, ); let mnemonic_key = derive_key_from_pass( [transmute_to_bytes(&mnemonic), &pin].concat(), salt_mnemonic, wallet_id, ); let mut master_key = [0u8; 32]; getrandom::getrandom(&mut master_key).map_err(|e| NgWalletError::InternalError)?; let enc_master_key_pazzle = enc_master_key(master_key, pazzle_key, 0, wallet_id)?; let enc_master_key_mnemonic = enc_master_key(master_key, mnemonic_key, 0, wallet_id)?; let timestamp = now_timestamp(); let encrypted = enc_encrypted_block( &encrypted_block, master_key, peer_id, nonce, timestamp, wallet_id, )?; let wallet_content = WalletContentV0 { security_img: cursor.into_inner(), security_txt: new_string, salt_pazzle, salt_mnemonic, enc_master_key_pazzle, enc_master_key_mnemonic, master_nonce: 0, timestamp, peer_id, nonce, encrypted, }; let ser_wallet = serde_bare::to_vec(&wallet_content).unwrap(); let sig = sign(wallet_key, wallet_id, &ser_wallet).unwrap(); let wallet_v0 = WalletV0 { /// ID id: wallet_id, /// Content content: wallet_content, /// Signature over content by wallet's private key sig, }; // let content = BootstrapContentV0 { servers: vec![] }; // let ser = serde_bare::to_vec(&content).unwrap(); // let sig = sign(wallet_key, wallet_id, &ser).unwrap(); // let bootstrap = Bootstrap::V0(BootstrapV0 { // id: wallet_id, // content, // sig, // }); // TODO send bootstrap (if) // TODO send wallet (if) Ok((Wallet::V0(wallet_v0), pazzle, mnemonic)) } #[cfg(test)] mod tests { use super::*; use p2p_repo::utils::generate_keypair; use std::fs::File; use std::io::BufReader; use std::io::Read; use std::io::Write; use std::time::Instant; #[test] fn create_wallet() { // loading an image file from disk let f = File::open("tests/valid_security_image.jpg") .expect("open of tests/valid_security_image.jpg"); let mut reader = BufReader::new(f); let mut img_buffer = Vec::new(); // Read file into vector. reader .read_to_end(&mut img_buffer) .expect("read of valid_security_image.jpg"); let pin = [5, 2, 9, 1]; let creation = Instant::now(); let res = create_wallet_v0( img_buffer, " know yourself ".to_string(), pin, 9, None, false, PubKey::Ed25519PubKey([ 119, 251, 253, 29, 135, 199, 254, 50, 134, 67, 1, 208, 117, 196, 167, 107, 2, 113, 98, 243, 49, 90, 7, 0, 157, 58, 14, 187, 14, 3, 116, 86, ]), 0, ) .expect("create_wallet_v0"); log!( "creation of wallet took: {} ms", creation.elapsed().as_millis() ); log!("-----------------------------"); let (wallet, pazzle, mnemonic) = res; let mut file = File::create("tests/wallet.ngw").expect("open wallet write file"); let ser_wallet = to_vec(&NgFile::V0(NgFileV0::Wallet(wallet.clone()))).unwrap(); file.write_all(&ser_wallet); log!("wallet id: {:?}", base64_url::encode(&wallet.id().slice())); log!("pazzle {:?}", pazzle); log!("mnemonic {:?}", display_mnemonic(&mnemonic)); log!("pin {:?}", pin); if let Wallet::V0(v0) = wallet { log!("security text: {:?}", v0.content.security_txt); let mut file = File::create("tests/generated_security_image.jpg").expect("open write file"); file.write_all(&v0.content.security_img); let f = File::open("tests/generated_security_image.jpg.compare") .expect("open of generated_security_image.jpg.compare"); let mut reader = BufReader::new(f); let mut generated_security_image_compare = Vec::new(); // Read file into vector. reader .read_to_end(&mut generated_security_image_compare) .expect("read of generated_security_image.jpg.compare"); assert_eq!(v0.content.security_img, generated_security_image_compare); let opening_mnemonic = Instant::now(); let w = open_wallet_with_mnemonic(Wallet::V0(v0.clone()), mnemonic, pin) .expect("open with mnemonic"); //println!("encrypted part {:?}", w); log!( "opening of wallet with mnemonic took: {} ms", opening_mnemonic.elapsed().as_millis() ); let opening_pazzle = Instant::now(); let w = open_wallet_with_pazzle(Wallet::V0(v0.clone()), pazzle, pin) .expect("open with pazzle"); //println!("encrypted part {:?}", w); log!( "opening of wallet with pazzle took: {} ms", opening_pazzle.elapsed().as_millis() ); } } }