Integrate process wasm/ts
This commit is contained in:
parent
fbddacdeb8
commit
c209bee651
@ -1,447 +0,0 @@
|
|||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use anyhow::{Error, Result};
|
|
||||||
use sp_backend::{
|
|
||||||
bitcoin::{
|
|
||||||
consensus::serde::hex,
|
|
||||||
hex::DisplayHex,
|
|
||||||
key::constants::SECRET_KEY_SIZE,
|
|
||||||
secp256k1::{ecdh::SharedSecret, SecretKey},
|
|
||||||
Txid,
|
|
||||||
},
|
|
||||||
silentpayments::sending::SilentPaymentAddress,
|
|
||||||
};
|
|
||||||
use wasm_bindgen::JsValue;
|
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use serde_json::{json, Value};
|
|
||||||
|
|
||||||
use aes::cipher::generic_array::GenericArray;
|
|
||||||
use aes::{
|
|
||||||
cipher::consts::{U32, U8},
|
|
||||||
Aes256,
|
|
||||||
};
|
|
||||||
use aes_gcm::{
|
|
||||||
aead::{Aead, AeadInPlace, KeyInit, Nonce},
|
|
||||||
AeadCore, Aes256Gcm, AesGcm, Key, TagSize,
|
|
||||||
};
|
|
||||||
use rand::{thread_rng, RngCore};
|
|
||||||
|
|
||||||
const HALFKEYSIZE: usize = SECRET_KEY_SIZE / 2;
|
|
||||||
|
|
||||||
const THIRTYTWO: usize = 32;
|
|
||||||
|
|
||||||
pub struct HalfKey([u8; HALFKEYSIZE]);
|
|
||||||
|
|
||||||
impl TryFrom<Vec<u8>> for HalfKey {
|
|
||||||
type Error = anyhow::Error;
|
|
||||||
fn try_from(value: Vec<u8>) -> std::prelude::v1::Result<Self, Error> {
|
|
||||||
if value.len() == HALFKEYSIZE {
|
|
||||||
let mut buf = [0u8; HALFKEYSIZE];
|
|
||||||
buf.copy_from_slice(&value);
|
|
||||||
Ok(HalfKey(buf))
|
|
||||||
} else {
|
|
||||||
Err(Error::msg("Invalid length for HalfKey"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl HalfKey {
|
|
||||||
pub fn as_slice(&self) -> &[u8] {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn to_inner(&self) -> Vec<u8> {
|
|
||||||
self.0.to_vec()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum Purpose {
|
|
||||||
Login,
|
|
||||||
ThirtyTwoBytes,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type CipherText = Vec<u8>;
|
|
||||||
|
|
||||||
pub type EncryptedKey = Vec<u8>;
|
|
||||||
|
|
||||||
pub struct Aes256Decryption {
|
|
||||||
pub purpose: Purpose,
|
|
||||||
cipher_text: CipherText,
|
|
||||||
aes_key: [u8; 32],
|
|
||||||
nonce: [u8; 12],
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Aes256Decryption {
|
|
||||||
pub fn new(
|
|
||||||
purpose: Purpose,
|
|
||||||
cipher_text: CipherText,
|
|
||||||
encrypted_aes_key: Vec<u8>, // If shared_secret is none this is actually the aes_key
|
|
||||||
shared_secret: Option<SharedSecret>, // We don't need that for certain purpose, like Login
|
|
||||||
) -> Result<Self> {
|
|
||||||
let mut aes_key = [0u8; 32];
|
|
||||||
if let Some(shared_secret) = shared_secret {
|
|
||||||
if encrypted_aes_key.len() <= 12 {
|
|
||||||
return Err(Error::msg("encrypted_aes_key is shorter than nonce length"));
|
|
||||||
} // Actually we could probably test that if the remnant is not a multiple of 32, something's wrong
|
|
||||||
// take the first 12 bytes form encrypted_aes_key as nonce
|
|
||||||
let (decrypt_key_nonce, encrypted_key) = encrypted_aes_key.split_at(12);
|
|
||||||
// decrypt key with shared_secret obtained from transaction
|
|
||||||
let decrypt_key_cipher = Aes256Gcm::new_from_slice(shared_secret.as_ref())
|
|
||||||
.map_err(|e| Error::msg(format!("{}", e)))?;
|
|
||||||
let aes_key_plain = decrypt_key_cipher
|
|
||||||
.decrypt(decrypt_key_nonce.into(), encrypted_key)
|
|
||||||
.map_err(|e| Error::msg(format!("{}", e)))?;
|
|
||||||
if aes_key_plain.len() != 32 {
|
|
||||||
return Err(Error::msg("Invalid length for decrypted key"));
|
|
||||||
}
|
|
||||||
aes_key.copy_from_slice(&aes_key_plain);
|
|
||||||
} else {
|
|
||||||
if encrypted_aes_key.len() != 32 {
|
|
||||||
return Err(Error::msg("Invalid length for decrypted key"));
|
|
||||||
}
|
|
||||||
aes_key.copy_from_slice(&encrypted_aes_key);
|
|
||||||
}
|
|
||||||
if cipher_text.len() <= 12 {
|
|
||||||
return Err(Error::msg("cipher_text is shorter than nonce length"));
|
|
||||||
}
|
|
||||||
let (message_nonce, message_cipher) = cipher_text.split_at(12);
|
|
||||||
let mut nonce = [0u8; 12];
|
|
||||||
nonce.copy_from_slice(message_nonce);
|
|
||||||
Ok(Self {
|
|
||||||
purpose,
|
|
||||||
cipher_text: message_cipher.to_vec(),
|
|
||||||
aes_key,
|
|
||||||
nonce,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn decrypt_with_key(&self) -> Result<Vec<u8>> {
|
|
||||||
match self.purpose {
|
|
||||||
Purpose::Login => {
|
|
||||||
let half_key = self.decrypt_login()?;
|
|
||||||
Ok(half_key.to_inner())
|
|
||||||
}
|
|
||||||
Purpose::ThirtyTwoBytes => {
|
|
||||||
let thirty_two_buf = self.decrypt_thirty_two()?;
|
|
||||||
Ok(thirty_two_buf.to_vec())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn decrypt_login(&self) -> Result<HalfKey> {
|
|
||||||
let cipher = Aes256Gcm::new(&self.aes_key.into());
|
|
||||||
let plain = cipher
|
|
||||||
.decrypt(&self.nonce.into(), &*self.cipher_text)
|
|
||||||
.map_err(|e| Error::msg(format!("{}", e)))?;
|
|
||||||
if plain.len() != SECRET_KEY_SIZE / 2 {
|
|
||||||
return Err(Error::msg("Plain text of invalid lenght for a login"));
|
|
||||||
}
|
|
||||||
let mut key_half = [0u8; SECRET_KEY_SIZE / 2];
|
|
||||||
key_half.copy_from_slice(&plain);
|
|
||||||
Ok(HalfKey(key_half))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn decrypt_thirty_two(&self) -> Result<[u8; THIRTYTWO]> {
|
|
||||||
let cipher = Aes256Gcm::new(&self.aes_key.into());
|
|
||||||
let plain = cipher
|
|
||||||
.decrypt(&self.nonce.into(), &*self.cipher_text)
|
|
||||||
.map_err(|e| Error::msg(format!("{}", e)))?;
|
|
||||||
if plain.len() != THIRTYTWO {
|
|
||||||
return Err(Error::msg("Plain text of invalid length, should be 32"));
|
|
||||||
}
|
|
||||||
let mut thirty_two = [0u8; THIRTYTWO];
|
|
||||||
thirty_two.copy_from_slice(&plain);
|
|
||||||
Ok(thirty_two)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Aes256Encryption {
|
|
||||||
pub purpose: Purpose,
|
|
||||||
plaintext: Vec<u8>,
|
|
||||||
aes_key: [u8; 32],
|
|
||||||
nonce: [u8; 12],
|
|
||||||
shared_secrets: HashMap<Txid, HashMap<SilentPaymentAddress, SharedSecret>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Aes256Encryption {
|
|
||||||
pub fn new(purpose: Purpose, plaintext: Vec<u8>) -> Result<Self> {
|
|
||||||
let mut rng = thread_rng();
|
|
||||||
let aes_key: [u8; 32] = Aes256Gcm::generate_key(&mut rng).into();
|
|
||||||
let nonce: [u8; 12] = Aes256Gcm::generate_nonce(&mut rng).into();
|
|
||||||
Self::import_key(purpose, plaintext, aes_key, nonce)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_shared_secret(
|
|
||||||
&mut self,
|
|
||||||
shared_secrets: HashMap<Txid, HashMap<SilentPaymentAddress, SharedSecret>>,
|
|
||||||
) {
|
|
||||||
self.shared_secrets = shared_secrets;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn encrypt_keys_with_shared_secrets(
|
|
||||||
&self,
|
|
||||||
) -> Result<HashMap<SilentPaymentAddress, EncryptedKey>> {
|
|
||||||
let mut res = HashMap::new();
|
|
||||||
let mut rng = thread_rng();
|
|
||||||
|
|
||||||
for (_, sp_address2shared_secret) in self.shared_secrets.iter() {
|
|
||||||
for (sp_address, shared_secret) in sp_address2shared_secret {
|
|
||||||
let cipher = Aes256Gcm::new_from_slice(shared_secret.as_ref())
|
|
||||||
.map_err(|e| Error::msg(format!("{}", e)))?;
|
|
||||||
let nonce = Aes256Gcm::generate_nonce(&mut rng);
|
|
||||||
let encrypted_key = cipher
|
|
||||||
.encrypt(&nonce, self.aes_key.as_slice())
|
|
||||||
.map_err(|e| Error::msg(format!("{}", e)))?;
|
|
||||||
|
|
||||||
let mut ciphertext = Vec::<u8>::with_capacity(nonce.len() + encrypted_key.len());
|
|
||||||
ciphertext.extend(nonce);
|
|
||||||
ciphertext.extend(encrypted_key);
|
|
||||||
|
|
||||||
res.insert(sp_address.to_owned(), ciphertext);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(res)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn import_key(
|
|
||||||
purpose: Purpose,
|
|
||||||
plaintext: Vec<u8>,
|
|
||||||
aes_key: [u8; 32],
|
|
||||||
nonce: [u8; 12],
|
|
||||||
) -> Result<Self> {
|
|
||||||
if plaintext.len() == 0 {
|
|
||||||
return Err(Error::msg("Can't create encryption for an empty message"));
|
|
||||||
}
|
|
||||||
Ok(Self {
|
|
||||||
purpose,
|
|
||||||
plaintext,
|
|
||||||
aes_key,
|
|
||||||
nonce,
|
|
||||||
shared_secrets: HashMap::new(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn encrypt_with_aes_key(&self) -> Result<CipherText> {
|
|
||||||
match self.purpose {
|
|
||||||
Purpose::Login => self.encrypt_login(),
|
|
||||||
Purpose::ThirtyTwoBytes => self.encrypt_thirty_two(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn encrypt_login(&self) -> Result<CipherText> {
|
|
||||||
let half_key: HalfKey = self.plaintext.clone().try_into()?;
|
|
||||||
let cipher = Aes256Gcm::new(&self.aes_key.into());
|
|
||||||
let cipher_text = cipher
|
|
||||||
.encrypt(&self.nonce.into(), half_key.as_slice())
|
|
||||||
.map_err(|e| Error::msg(format!("{}", e)))?;
|
|
||||||
let mut res = Vec::with_capacity(self.nonce.len() + cipher_text.len());
|
|
||||||
res.extend_from_slice(&self.nonce);
|
|
||||||
res.extend_from_slice(&cipher_text);
|
|
||||||
Ok(res)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn encrypt_thirty_two(&self) -> Result<CipherText> {
|
|
||||||
if self.plaintext.len() != 32 {
|
|
||||||
return Err(Error::msg("Invalid length, should be 32"));
|
|
||||||
}
|
|
||||||
let mut thirty_two = [0u8; 32];
|
|
||||||
thirty_two.copy_from_slice(&self.plaintext);
|
|
||||||
let cipher = Aes256Gcm::new(&self.aes_key.into());
|
|
||||||
let cipher_text = cipher
|
|
||||||
.encrypt(&self.nonce.into(), thirty_two.as_slice())
|
|
||||||
.map_err(|e| Error::msg(format!("{}", e)))?;
|
|
||||||
let mut res = Vec::with_capacity(self.nonce.len() + cipher_text.len());
|
|
||||||
log::info!("{}", cipher_text.len());
|
|
||||||
res.extend_from_slice(&self.nonce);
|
|
||||||
res.extend_from_slice(&cipher_text);
|
|
||||||
Ok(res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
const ALICE_SP_ADDRESS: &str = "tsp1qqw3lqr6xravz9nf8ntazgwwl0fqv47kfjdxsnxs6eutavqfwyv5q6qk97mmyf6dtkdyzqlu2zv6h9j2ggclk7vn705q5u2phglpq7yw3dg5rwpdz";
|
|
||||||
const BOB_SP_ADDRESS: &str = "tsp1qq2hlsgrj0gz8kcfkf9flqw5llz0u2vr04telqndku9mcqm6dl4fhvq60t8r78srrf56w9yr7w9e9dusc2wjqc30up6fjwnh9mw3e3veqegdmtf08";
|
|
||||||
const TRANSACTION: &str = "4e6d03dec558e1b6624f813bf2da7cd8d8fb1c2296684c08cf38724dcfd8d10b";
|
|
||||||
const ALICE_SHARED_SECRET: &str =
|
|
||||||
"ccf02d364c2641ca129a3fdf49de57b705896e233f7ba6d738991993ea7e2106";
|
|
||||||
const BOB_SHARED_SECRET: &str =
|
|
||||||
"15ef3e377fb842e81de52dbaaea8ba30aeb051a81043ee19264afd27353da521";
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn new_aes_empty_plaintext() {
|
|
||||||
let plaintext = Vec::new();
|
|
||||||
let aes_enc = Aes256Encryption::new(Purpose::Login, plaintext);
|
|
||||||
|
|
||||||
assert!(aes_enc.is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn aes_encrypt_login_invalid_length() {
|
|
||||||
let plaintext = "example";
|
|
||||||
let aes_enc_short = Aes256Encryption::new(Purpose::Login, plaintext.as_bytes().to_vec());
|
|
||||||
|
|
||||||
assert!(aes_enc_short.is_ok());
|
|
||||||
|
|
||||||
let cipher = aes_enc_short.unwrap().encrypt_with_aes_key();
|
|
||||||
|
|
||||||
assert!(cipher.is_err());
|
|
||||||
|
|
||||||
let plaintext = [1u8; 64];
|
|
||||||
let aes_enc_long = Aes256Encryption::new(Purpose::Login, plaintext.to_vec());
|
|
||||||
|
|
||||||
assert!(aes_enc_long.is_ok());
|
|
||||||
|
|
||||||
let cipher = aes_enc_long.unwrap().encrypt_with_aes_key();
|
|
||||||
|
|
||||||
assert!(cipher.is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn aes_encrypt_login() {
|
|
||||||
let plaintext = [1u8; HALFKEYSIZE];
|
|
||||||
let aes_key = Aes256Gcm::generate_key(&mut thread_rng());
|
|
||||||
let nonce = Aes256Gcm::generate_nonce(&mut thread_rng());
|
|
||||||
let aes_enc = Aes256Encryption::import_key(
|
|
||||||
Purpose::Login,
|
|
||||||
plaintext.to_vec(),
|
|
||||||
aes_key.into(),
|
|
||||||
nonce.into(),
|
|
||||||
);
|
|
||||||
|
|
||||||
assert!(aes_enc.is_ok());
|
|
||||||
|
|
||||||
let cipher = aes_enc.unwrap().encrypt_with_aes_key();
|
|
||||||
|
|
||||||
assert!(cipher.is_ok());
|
|
||||||
|
|
||||||
let mut plain_key = [0u8; 32];
|
|
||||||
plain_key.copy_from_slice(&aes_key.to_vec());
|
|
||||||
|
|
||||||
let aes_dec =
|
|
||||||
Aes256Decryption::new(Purpose::Login, cipher.unwrap(), plain_key.to_vec(), None);
|
|
||||||
|
|
||||||
assert!(aes_dec.is_ok());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn aes_encrypt_key() {
|
|
||||||
let plaintext = [1u8; HALFKEYSIZE];
|
|
||||||
let mut aes_enc = Aes256Encryption::new(Purpose::Login, plaintext.to_vec()).unwrap();
|
|
||||||
|
|
||||||
let mut shared_secrets: HashMap<Txid, _> = HashMap::new();
|
|
||||||
let mut sp_address2shared_secrets: HashMap<SilentPaymentAddress, SharedSecret> =
|
|
||||||
HashMap::new();
|
|
||||||
sp_address2shared_secrets.insert(
|
|
||||||
ALICE_SP_ADDRESS.try_into().unwrap(),
|
|
||||||
SharedSecret::from_str(ALICE_SHARED_SECRET).unwrap(),
|
|
||||||
);
|
|
||||||
shared_secrets.insert(
|
|
||||||
Txid::from_str(TRANSACTION).unwrap(),
|
|
||||||
sp_address2shared_secrets,
|
|
||||||
);
|
|
||||||
|
|
||||||
aes_enc.set_shared_secret(shared_secrets);
|
|
||||||
|
|
||||||
let sp_address2encrypted_keys = aes_enc.encrypt_keys_with_shared_secrets();
|
|
||||||
|
|
||||||
assert!(sp_address2encrypted_keys.is_ok());
|
|
||||||
|
|
||||||
let encrypted_key = sp_address2encrypted_keys
|
|
||||||
.unwrap()
|
|
||||||
.get(&ALICE_SP_ADDRESS.try_into().unwrap())
|
|
||||||
.cloned();
|
|
||||||
|
|
||||||
let ciphertext = aes_enc.encrypt_with_aes_key();
|
|
||||||
|
|
||||||
assert!(ciphertext.is_ok());
|
|
||||||
|
|
||||||
let aes_dec = Aes256Decryption::new(
|
|
||||||
Purpose::Login,
|
|
||||||
ciphertext.unwrap(),
|
|
||||||
encrypted_key.unwrap(),
|
|
||||||
Some(SharedSecret::from_str(ALICE_SHARED_SECRET).unwrap()),
|
|
||||||
);
|
|
||||||
|
|
||||||
assert!(aes_dec.is_ok());
|
|
||||||
|
|
||||||
let retrieved_plain = aes_dec.unwrap().decrypt_with_key();
|
|
||||||
|
|
||||||
assert!(retrieved_plain.is_ok());
|
|
||||||
|
|
||||||
assert!(retrieved_plain.unwrap() == plaintext);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn aes_encrypt_key_many() {
|
|
||||||
let plaintext = [1u8; THIRTYTWO];
|
|
||||||
let mut aes_enc =
|
|
||||||
Aes256Encryption::new(Purpose::ThirtyTwoBytes, plaintext.to_vec()).unwrap();
|
|
||||||
|
|
||||||
let mut shared_secrets: HashMap<Txid, _> = HashMap::new();
|
|
||||||
let mut sp_address2shared_secrets: HashMap<SilentPaymentAddress, SharedSecret> =
|
|
||||||
HashMap::new();
|
|
||||||
sp_address2shared_secrets.insert(
|
|
||||||
ALICE_SP_ADDRESS.try_into().unwrap(),
|
|
||||||
SharedSecret::from_str(ALICE_SHARED_SECRET).unwrap(),
|
|
||||||
);
|
|
||||||
sp_address2shared_secrets.insert(
|
|
||||||
BOB_SP_ADDRESS.try_into().unwrap(),
|
|
||||||
SharedSecret::from_str(BOB_SHARED_SECRET).unwrap(),
|
|
||||||
);
|
|
||||||
shared_secrets.insert(
|
|
||||||
Txid::from_str(TRANSACTION).unwrap(),
|
|
||||||
sp_address2shared_secrets,
|
|
||||||
);
|
|
||||||
|
|
||||||
aes_enc.set_shared_secret(shared_secrets);
|
|
||||||
|
|
||||||
let mut sp_address2encrypted_keys = aes_enc.encrypt_keys_with_shared_secrets();
|
|
||||||
|
|
||||||
assert!(sp_address2encrypted_keys.is_ok());
|
|
||||||
|
|
||||||
// Alice
|
|
||||||
let encrypted_key = sp_address2encrypted_keys
|
|
||||||
.as_mut()
|
|
||||||
.unwrap()
|
|
||||||
.get(&ALICE_SP_ADDRESS.try_into().unwrap())
|
|
||||||
.cloned();
|
|
||||||
|
|
||||||
let ciphertext = aes_enc.encrypt_with_aes_key();
|
|
||||||
|
|
||||||
let aes_dec = Aes256Decryption::new(
|
|
||||||
Purpose::ThirtyTwoBytes,
|
|
||||||
ciphertext.unwrap(),
|
|
||||||
encrypted_key.unwrap(),
|
|
||||||
Some(SharedSecret::from_str(ALICE_SHARED_SECRET).unwrap()),
|
|
||||||
);
|
|
||||||
|
|
||||||
let retrieved_plain = aes_dec.unwrap().decrypt_with_key();
|
|
||||||
|
|
||||||
assert!(retrieved_plain.unwrap() == plaintext);
|
|
||||||
|
|
||||||
// Bob
|
|
||||||
let encrypted_key = sp_address2encrypted_keys
|
|
||||||
.unwrap()
|
|
||||||
.get(&BOB_SP_ADDRESS.try_into().unwrap())
|
|
||||||
.cloned();
|
|
||||||
|
|
||||||
let ciphertext = aes_enc.encrypt_with_aes_key();
|
|
||||||
|
|
||||||
let aes_dec = Aes256Decryption::new(
|
|
||||||
Purpose::ThirtyTwoBytes,
|
|
||||||
ciphertext.unwrap(),
|
|
||||||
encrypted_key.unwrap(),
|
|
||||||
Some(SharedSecret::from_str(BOB_SHARED_SECRET).unwrap()),
|
|
||||||
);
|
|
||||||
|
|
||||||
let retrieved_plain = aes_dec.unwrap().decrypt_with_key();
|
|
||||||
|
|
||||||
assert!(retrieved_plain.unwrap() == plaintext);
|
|
||||||
}
|
|
||||||
}
|
|
@ -173,19 +173,49 @@ pub fn get_processes() -> ApiResult<get_process_return> {
|
|||||||
|
|
||||||
//instances of process
|
//instances of process
|
||||||
let process1 = Process {
|
let process1 = Process {
|
||||||
id: String::from("1"),
|
id: 1,
|
||||||
|
name: String::from("CREATE_ID"),
|
||||||
version: String::from("1.0"),
|
version: String::from("1.0"),
|
||||||
gestionnaires: vec![member1.clone(), member2.clone()],
|
members: vec![member1.clone(), member2.clone()],
|
||||||
|
html: crate::process::HTML_CREATE_ID.to_owned(),
|
||||||
|
style: crate::process::CSS.to_owned(),
|
||||||
|
script: "".to_owned(),
|
||||||
};
|
};
|
||||||
let process2 = Process {
|
let process2 = Process {
|
||||||
id: String::from("2"),
|
id: 2,
|
||||||
version: String::from("2.0"),
|
name: String::from("UPDATE_ID"),
|
||||||
gestionnaires: vec![member2.clone(), member3.clone()],
|
version: String::from("1.0"),
|
||||||
|
members: vec![member1.clone(), member2.clone()],
|
||||||
|
html: crate::process::HTML_UPDATE_ID.to_owned(),
|
||||||
|
style: crate::process::CSS.to_owned(),
|
||||||
|
script: "".to_owned(),
|
||||||
};
|
};
|
||||||
let process3 = Process {
|
let process3 = Process {
|
||||||
id: String::from("3"),
|
id: 3,
|
||||||
|
name: String::from("RECOVER"),
|
||||||
version: String::from("1.0"),
|
version: String::from("1.0"),
|
||||||
gestionnaires: vec![member3.clone(), member1.clone()],
|
members: vec![member1.clone(), member2.clone()],
|
||||||
|
html: crate::process::HTML_RECOVER.to_owned(),
|
||||||
|
style: crate::process::CSS.to_owned(),
|
||||||
|
script: "".to_owned(),
|
||||||
|
};
|
||||||
|
let process4 = Process {
|
||||||
|
id: 4,
|
||||||
|
name: String::from("REVOKE_IMAGE"),
|
||||||
|
version: String::from("1.0"),
|
||||||
|
members: vec![member1.clone(), member2.clone()],
|
||||||
|
html: crate::process::HTML_REVOKE_IMAGE.to_owned(),
|
||||||
|
style: crate::process::CSS.to_owned(),
|
||||||
|
script: "".to_owned(),
|
||||||
|
};
|
||||||
|
let process5 = Process {
|
||||||
|
id: 5,
|
||||||
|
name: String::from("REVOKE"),
|
||||||
|
version: String::from("1.0"),
|
||||||
|
members: vec![member1.clone(), member2.clone()],
|
||||||
|
html: crate::process::HTML_REVOKE.to_owned(),
|
||||||
|
style: crate::process::CSS.to_owned(),
|
||||||
|
script: "".to_owned(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// vec with the instances of processes
|
// vec with the instances of processes
|
||||||
@ -193,6 +223,8 @@ pub fn get_processes() -> ApiResult<get_process_return> {
|
|||||||
data_process.push(process1);
|
data_process.push(process1);
|
||||||
data_process.push(process2);
|
data_process.push(process2);
|
||||||
data_process.push(process3);
|
data_process.push(process3);
|
||||||
|
data_process.push(process4);
|
||||||
|
data_process.push(process5);
|
||||||
Ok(get_process_return(data_process))
|
Ok(get_process_return(data_process))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,9 +1,447 @@
|
|||||||
use aes_gcm;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
pub enum KeyType {
|
use anyhow::{Error, Result};
|
||||||
Aes256GcmIv96BitKey([u8;32])
|
use sp_backend::{
|
||||||
|
bitcoin::{
|
||||||
|
consensus::serde::hex,
|
||||||
|
hex::DisplayHex,
|
||||||
|
key::constants::SECRET_KEY_SIZE,
|
||||||
|
secp256k1::{ecdh::SharedSecret, SecretKey},
|
||||||
|
Txid,
|
||||||
|
},
|
||||||
|
silentpayments::sending::SilentPaymentAddress,
|
||||||
|
};
|
||||||
|
use wasm_bindgen::JsValue;
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json::{json, Value};
|
||||||
|
|
||||||
|
use aes::cipher::generic_array::GenericArray;
|
||||||
|
use aes::{
|
||||||
|
cipher::consts::{U32, U8},
|
||||||
|
Aes256,
|
||||||
|
};
|
||||||
|
use aes_gcm::{
|
||||||
|
aead::{Aead, AeadInPlace, KeyInit, Nonce},
|
||||||
|
AeadCore, Aes256Gcm, AesGcm, Key, TagSize,
|
||||||
|
};
|
||||||
|
use rand::{thread_rng, RngCore};
|
||||||
|
|
||||||
|
const HALFKEYSIZE: usize = SECRET_KEY_SIZE / 2;
|
||||||
|
|
||||||
|
const THIRTYTWO: usize = 32;
|
||||||
|
|
||||||
|
pub struct HalfKey([u8; HALFKEYSIZE]);
|
||||||
|
|
||||||
|
impl TryFrom<Vec<u8>> for HalfKey {
|
||||||
|
type Error = anyhow::Error;
|
||||||
|
fn try_from(value: Vec<u8>) -> std::prelude::v1::Result<Self, Error> {
|
||||||
|
if value.len() == HALFKEYSIZE {
|
||||||
|
let mut buf = [0u8; HALFKEYSIZE];
|
||||||
|
buf.copy_from_slice(&value);
|
||||||
|
Ok(HalfKey(buf))
|
||||||
|
} else {
|
||||||
|
Err(Error::msg("Invalid length for HalfKey"))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct EncryptionKey {
|
impl HalfKey {
|
||||||
sk: KeyType
|
pub fn as_slice(&self) -> &[u8] {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_inner(&self) -> Vec<u8> {
|
||||||
|
self.0.to_vec()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Purpose {
|
||||||
|
Login,
|
||||||
|
ThirtyTwoBytes,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type CipherText = Vec<u8>;
|
||||||
|
|
||||||
|
pub type EncryptedKey = Vec<u8>;
|
||||||
|
|
||||||
|
pub struct Aes256Decryption {
|
||||||
|
pub purpose: Purpose,
|
||||||
|
cipher_text: CipherText,
|
||||||
|
aes_key: [u8; 32],
|
||||||
|
nonce: [u8; 12],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Aes256Decryption {
|
||||||
|
pub fn new(
|
||||||
|
purpose: Purpose,
|
||||||
|
cipher_text: CipherText,
|
||||||
|
encrypted_aes_key: Vec<u8>, // If shared_secret is none this is actually the aes_key
|
||||||
|
shared_secret: Option<SharedSecret>, // We don't need that for certain purpose, like Login
|
||||||
|
) -> Result<Self> {
|
||||||
|
let mut aes_key = [0u8; 32];
|
||||||
|
if let Some(shared_secret) = shared_secret {
|
||||||
|
if encrypted_aes_key.len() <= 12 {
|
||||||
|
return Err(Error::msg("encrypted_aes_key is shorter than nonce length"));
|
||||||
|
} // Actually we could probably test that if the remnant is not a multiple of 32, something's wrong
|
||||||
|
// take the first 12 bytes form encrypted_aes_key as nonce
|
||||||
|
let (decrypt_key_nonce, encrypted_key) = encrypted_aes_key.split_at(12);
|
||||||
|
// decrypt key with shared_secret obtained from transaction
|
||||||
|
let decrypt_key_cipher = Aes256Gcm::new_from_slice(shared_secret.as_ref())
|
||||||
|
.map_err(|e| Error::msg(format!("{}", e)))?;
|
||||||
|
let aes_key_plain = decrypt_key_cipher
|
||||||
|
.decrypt(decrypt_key_nonce.into(), encrypted_key)
|
||||||
|
.map_err(|e| Error::msg(format!("{}", e)))?;
|
||||||
|
if aes_key_plain.len() != 32 {
|
||||||
|
return Err(Error::msg("Invalid length for decrypted key"));
|
||||||
|
}
|
||||||
|
aes_key.copy_from_slice(&aes_key_plain);
|
||||||
|
} else {
|
||||||
|
if encrypted_aes_key.len() != 32 {
|
||||||
|
return Err(Error::msg("Invalid length for decrypted key"));
|
||||||
|
}
|
||||||
|
aes_key.copy_from_slice(&encrypted_aes_key);
|
||||||
|
}
|
||||||
|
if cipher_text.len() <= 12 {
|
||||||
|
return Err(Error::msg("cipher_text is shorter than nonce length"));
|
||||||
|
}
|
||||||
|
let (message_nonce, message_cipher) = cipher_text.split_at(12);
|
||||||
|
let mut nonce = [0u8; 12];
|
||||||
|
nonce.copy_from_slice(message_nonce);
|
||||||
|
Ok(Self {
|
||||||
|
purpose,
|
||||||
|
cipher_text: message_cipher.to_vec(),
|
||||||
|
aes_key,
|
||||||
|
nonce,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decrypt_with_key(&self) -> Result<Vec<u8>> {
|
||||||
|
match self.purpose {
|
||||||
|
Purpose::Login => {
|
||||||
|
let half_key = self.decrypt_login()?;
|
||||||
|
Ok(half_key.to_inner())
|
||||||
|
}
|
||||||
|
Purpose::ThirtyTwoBytes => {
|
||||||
|
let thirty_two_buf = self.decrypt_thirty_two()?;
|
||||||
|
Ok(thirty_two_buf.to_vec())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decrypt_login(&self) -> Result<HalfKey> {
|
||||||
|
let cipher = Aes256Gcm::new(&self.aes_key.into());
|
||||||
|
let plain = cipher
|
||||||
|
.decrypt(&self.nonce.into(), &*self.cipher_text)
|
||||||
|
.map_err(|e| Error::msg(format!("{}", e)))?;
|
||||||
|
if plain.len() != SECRET_KEY_SIZE / 2 {
|
||||||
|
return Err(Error::msg("Plain text of invalid lenght for a login"));
|
||||||
|
}
|
||||||
|
let mut key_half = [0u8; SECRET_KEY_SIZE / 2];
|
||||||
|
key_half.copy_from_slice(&plain);
|
||||||
|
Ok(HalfKey(key_half))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decrypt_thirty_two(&self) -> Result<[u8; THIRTYTWO]> {
|
||||||
|
let cipher = Aes256Gcm::new(&self.aes_key.into());
|
||||||
|
let plain = cipher
|
||||||
|
.decrypt(&self.nonce.into(), &*self.cipher_text)
|
||||||
|
.map_err(|e| Error::msg(format!("{}", e)))?;
|
||||||
|
if plain.len() != THIRTYTWO {
|
||||||
|
return Err(Error::msg("Plain text of invalid length, should be 32"));
|
||||||
|
}
|
||||||
|
let mut thirty_two = [0u8; THIRTYTWO];
|
||||||
|
thirty_two.copy_from_slice(&plain);
|
||||||
|
Ok(thirty_two)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Aes256Encryption {
|
||||||
|
pub purpose: Purpose,
|
||||||
|
plaintext: Vec<u8>,
|
||||||
|
aes_key: [u8; 32],
|
||||||
|
nonce: [u8; 12],
|
||||||
|
shared_secrets: HashMap<Txid, HashMap<SilentPaymentAddress, SharedSecret>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Aes256Encryption {
|
||||||
|
pub fn new(purpose: Purpose, plaintext: Vec<u8>) -> Result<Self> {
|
||||||
|
let mut rng = thread_rng();
|
||||||
|
let aes_key: [u8; 32] = Aes256Gcm::generate_key(&mut rng).into();
|
||||||
|
let nonce: [u8; 12] = Aes256Gcm::generate_nonce(&mut rng).into();
|
||||||
|
Self::import_key(purpose, plaintext, aes_key, nonce)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_shared_secret(
|
||||||
|
&mut self,
|
||||||
|
shared_secrets: HashMap<Txid, HashMap<SilentPaymentAddress, SharedSecret>>,
|
||||||
|
) {
|
||||||
|
self.shared_secrets = shared_secrets;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn encrypt_keys_with_shared_secrets(
|
||||||
|
&self,
|
||||||
|
) -> Result<HashMap<SilentPaymentAddress, EncryptedKey>> {
|
||||||
|
let mut res = HashMap::new();
|
||||||
|
let mut rng = thread_rng();
|
||||||
|
|
||||||
|
for (_, sp_address2shared_secret) in self.shared_secrets.iter() {
|
||||||
|
for (sp_address, shared_secret) in sp_address2shared_secret {
|
||||||
|
let cipher = Aes256Gcm::new_from_slice(shared_secret.as_ref())
|
||||||
|
.map_err(|e| Error::msg(format!("{}", e)))?;
|
||||||
|
let nonce = Aes256Gcm::generate_nonce(&mut rng);
|
||||||
|
let encrypted_key = cipher
|
||||||
|
.encrypt(&nonce, self.aes_key.as_slice())
|
||||||
|
.map_err(|e| Error::msg(format!("{}", e)))?;
|
||||||
|
|
||||||
|
let mut ciphertext = Vec::<u8>::with_capacity(nonce.len() + encrypted_key.len());
|
||||||
|
ciphertext.extend(nonce);
|
||||||
|
ciphertext.extend(encrypted_key);
|
||||||
|
|
||||||
|
res.insert(sp_address.to_owned(), ciphertext);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn import_key(
|
||||||
|
purpose: Purpose,
|
||||||
|
plaintext: Vec<u8>,
|
||||||
|
aes_key: [u8; 32],
|
||||||
|
nonce: [u8; 12],
|
||||||
|
) -> Result<Self> {
|
||||||
|
if plaintext.len() == 0 {
|
||||||
|
return Err(Error::msg("Can't create encryption for an empty message"));
|
||||||
|
}
|
||||||
|
Ok(Self {
|
||||||
|
purpose,
|
||||||
|
plaintext,
|
||||||
|
aes_key,
|
||||||
|
nonce,
|
||||||
|
shared_secrets: HashMap::new(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn encrypt_with_aes_key(&self) -> Result<CipherText> {
|
||||||
|
match self.purpose {
|
||||||
|
Purpose::Login => self.encrypt_login(),
|
||||||
|
Purpose::ThirtyTwoBytes => self.encrypt_thirty_two(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encrypt_login(&self) -> Result<CipherText> {
|
||||||
|
let half_key: HalfKey = self.plaintext.clone().try_into()?;
|
||||||
|
let cipher = Aes256Gcm::new(&self.aes_key.into());
|
||||||
|
let cipher_text = cipher
|
||||||
|
.encrypt(&self.nonce.into(), half_key.as_slice())
|
||||||
|
.map_err(|e| Error::msg(format!("{}", e)))?;
|
||||||
|
let mut res = Vec::with_capacity(self.nonce.len() + cipher_text.len());
|
||||||
|
res.extend_from_slice(&self.nonce);
|
||||||
|
res.extend_from_slice(&cipher_text);
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encrypt_thirty_two(&self) -> Result<CipherText> {
|
||||||
|
if self.plaintext.len() != 32 {
|
||||||
|
return Err(Error::msg("Invalid length, should be 32"));
|
||||||
|
}
|
||||||
|
let mut thirty_two = [0u8; 32];
|
||||||
|
thirty_two.copy_from_slice(&self.plaintext);
|
||||||
|
let cipher = Aes256Gcm::new(&self.aes_key.into());
|
||||||
|
let cipher_text = cipher
|
||||||
|
.encrypt(&self.nonce.into(), thirty_two.as_slice())
|
||||||
|
.map_err(|e| Error::msg(format!("{}", e)))?;
|
||||||
|
let mut res = Vec::with_capacity(self.nonce.len() + cipher_text.len());
|
||||||
|
log::info!("{}", cipher_text.len());
|
||||||
|
res.extend_from_slice(&self.nonce);
|
||||||
|
res.extend_from_slice(&cipher_text);
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
const ALICE_SP_ADDRESS: &str = "tsp1qqw3lqr6xravz9nf8ntazgwwl0fqv47kfjdxsnxs6eutavqfwyv5q6qk97mmyf6dtkdyzqlu2zv6h9j2ggclk7vn705q5u2phglpq7yw3dg5rwpdz";
|
||||||
|
const BOB_SP_ADDRESS: &str = "tsp1qq2hlsgrj0gz8kcfkf9flqw5llz0u2vr04telqndku9mcqm6dl4fhvq60t8r78srrf56w9yr7w9e9dusc2wjqc30up6fjwnh9mw3e3veqegdmtf08";
|
||||||
|
const TRANSACTION: &str = "4e6d03dec558e1b6624f813bf2da7cd8d8fb1c2296684c08cf38724dcfd8d10b";
|
||||||
|
const ALICE_SHARED_SECRET: &str =
|
||||||
|
"ccf02d364c2641ca129a3fdf49de57b705896e233f7ba6d738991993ea7e2106";
|
||||||
|
const BOB_SHARED_SECRET: &str =
|
||||||
|
"15ef3e377fb842e81de52dbaaea8ba30aeb051a81043ee19264afd27353da521";
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn new_aes_empty_plaintext() {
|
||||||
|
let plaintext = Vec::new();
|
||||||
|
let aes_enc = Aes256Encryption::new(Purpose::Login, plaintext);
|
||||||
|
|
||||||
|
assert!(aes_enc.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn aes_encrypt_login_invalid_length() {
|
||||||
|
let plaintext = "example";
|
||||||
|
let aes_enc_short = Aes256Encryption::new(Purpose::Login, plaintext.as_bytes().to_vec());
|
||||||
|
|
||||||
|
assert!(aes_enc_short.is_ok());
|
||||||
|
|
||||||
|
let cipher = aes_enc_short.unwrap().encrypt_with_aes_key();
|
||||||
|
|
||||||
|
assert!(cipher.is_err());
|
||||||
|
|
||||||
|
let plaintext = [1u8; 64];
|
||||||
|
let aes_enc_long = Aes256Encryption::new(Purpose::Login, plaintext.to_vec());
|
||||||
|
|
||||||
|
assert!(aes_enc_long.is_ok());
|
||||||
|
|
||||||
|
let cipher = aes_enc_long.unwrap().encrypt_with_aes_key();
|
||||||
|
|
||||||
|
assert!(cipher.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn aes_encrypt_login() {
|
||||||
|
let plaintext = [1u8; HALFKEYSIZE];
|
||||||
|
let aes_key = Aes256Gcm::generate_key(&mut thread_rng());
|
||||||
|
let nonce = Aes256Gcm::generate_nonce(&mut thread_rng());
|
||||||
|
let aes_enc = Aes256Encryption::import_key(
|
||||||
|
Purpose::Login,
|
||||||
|
plaintext.to_vec(),
|
||||||
|
aes_key.into(),
|
||||||
|
nonce.into(),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(aes_enc.is_ok());
|
||||||
|
|
||||||
|
let cipher = aes_enc.unwrap().encrypt_with_aes_key();
|
||||||
|
|
||||||
|
assert!(cipher.is_ok());
|
||||||
|
|
||||||
|
let mut plain_key = [0u8; 32];
|
||||||
|
plain_key.copy_from_slice(&aes_key.to_vec());
|
||||||
|
|
||||||
|
let aes_dec =
|
||||||
|
Aes256Decryption::new(Purpose::Login, cipher.unwrap(), plain_key.to_vec(), None);
|
||||||
|
|
||||||
|
assert!(aes_dec.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn aes_encrypt_key() {
|
||||||
|
let plaintext = [1u8; HALFKEYSIZE];
|
||||||
|
let mut aes_enc = Aes256Encryption::new(Purpose::Login, plaintext.to_vec()).unwrap();
|
||||||
|
|
||||||
|
let mut shared_secrets: HashMap<Txid, _> = HashMap::new();
|
||||||
|
let mut sp_address2shared_secrets: HashMap<SilentPaymentAddress, SharedSecret> =
|
||||||
|
HashMap::new();
|
||||||
|
sp_address2shared_secrets.insert(
|
||||||
|
ALICE_SP_ADDRESS.try_into().unwrap(),
|
||||||
|
SharedSecret::from_str(ALICE_SHARED_SECRET).unwrap(),
|
||||||
|
);
|
||||||
|
shared_secrets.insert(
|
||||||
|
Txid::from_str(TRANSACTION).unwrap(),
|
||||||
|
sp_address2shared_secrets,
|
||||||
|
);
|
||||||
|
|
||||||
|
aes_enc.set_shared_secret(shared_secrets);
|
||||||
|
|
||||||
|
let sp_address2encrypted_keys = aes_enc.encrypt_keys_with_shared_secrets();
|
||||||
|
|
||||||
|
assert!(sp_address2encrypted_keys.is_ok());
|
||||||
|
|
||||||
|
let encrypted_key = sp_address2encrypted_keys
|
||||||
|
.unwrap()
|
||||||
|
.get(&ALICE_SP_ADDRESS.try_into().unwrap())
|
||||||
|
.cloned();
|
||||||
|
|
||||||
|
let ciphertext = aes_enc.encrypt_with_aes_key();
|
||||||
|
|
||||||
|
assert!(ciphertext.is_ok());
|
||||||
|
|
||||||
|
let aes_dec = Aes256Decryption::new(
|
||||||
|
Purpose::Login,
|
||||||
|
ciphertext.unwrap(),
|
||||||
|
encrypted_key.unwrap(),
|
||||||
|
Some(SharedSecret::from_str(ALICE_SHARED_SECRET).unwrap()),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(aes_dec.is_ok());
|
||||||
|
|
||||||
|
let retrieved_plain = aes_dec.unwrap().decrypt_with_key();
|
||||||
|
|
||||||
|
assert!(retrieved_plain.is_ok());
|
||||||
|
|
||||||
|
assert!(retrieved_plain.unwrap() == plaintext);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn aes_encrypt_key_many() {
|
||||||
|
let plaintext = [1u8; THIRTYTWO];
|
||||||
|
let mut aes_enc =
|
||||||
|
Aes256Encryption::new(Purpose::ThirtyTwoBytes, plaintext.to_vec()).unwrap();
|
||||||
|
|
||||||
|
let mut shared_secrets: HashMap<Txid, _> = HashMap::new();
|
||||||
|
let mut sp_address2shared_secrets: HashMap<SilentPaymentAddress, SharedSecret> =
|
||||||
|
HashMap::new();
|
||||||
|
sp_address2shared_secrets.insert(
|
||||||
|
ALICE_SP_ADDRESS.try_into().unwrap(),
|
||||||
|
SharedSecret::from_str(ALICE_SHARED_SECRET).unwrap(),
|
||||||
|
);
|
||||||
|
sp_address2shared_secrets.insert(
|
||||||
|
BOB_SP_ADDRESS.try_into().unwrap(),
|
||||||
|
SharedSecret::from_str(BOB_SHARED_SECRET).unwrap(),
|
||||||
|
);
|
||||||
|
shared_secrets.insert(
|
||||||
|
Txid::from_str(TRANSACTION).unwrap(),
|
||||||
|
sp_address2shared_secrets,
|
||||||
|
);
|
||||||
|
|
||||||
|
aes_enc.set_shared_secret(shared_secrets);
|
||||||
|
|
||||||
|
let mut sp_address2encrypted_keys = aes_enc.encrypt_keys_with_shared_secrets();
|
||||||
|
|
||||||
|
assert!(sp_address2encrypted_keys.is_ok());
|
||||||
|
|
||||||
|
// Alice
|
||||||
|
let encrypted_key = sp_address2encrypted_keys
|
||||||
|
.as_mut()
|
||||||
|
.unwrap()
|
||||||
|
.get(&ALICE_SP_ADDRESS.try_into().unwrap())
|
||||||
|
.cloned();
|
||||||
|
|
||||||
|
let ciphertext = aes_enc.encrypt_with_aes_key();
|
||||||
|
|
||||||
|
let aes_dec = Aes256Decryption::new(
|
||||||
|
Purpose::ThirtyTwoBytes,
|
||||||
|
ciphertext.unwrap(),
|
||||||
|
encrypted_key.unwrap(),
|
||||||
|
Some(SharedSecret::from_str(ALICE_SHARED_SECRET).unwrap()),
|
||||||
|
);
|
||||||
|
|
||||||
|
let retrieved_plain = aes_dec.unwrap().decrypt_with_key();
|
||||||
|
|
||||||
|
assert!(retrieved_plain.unwrap() == plaintext);
|
||||||
|
|
||||||
|
// Bob
|
||||||
|
let encrypted_key = sp_address2encrypted_keys
|
||||||
|
.unwrap()
|
||||||
|
.get(&BOB_SP_ADDRESS.try_into().unwrap())
|
||||||
|
.cloned();
|
||||||
|
|
||||||
|
let ciphertext = aes_enc.encrypt_with_aes_key();
|
||||||
|
|
||||||
|
let aes_dec = Aes256Decryption::new(
|
||||||
|
Purpose::ThirtyTwoBytes,
|
||||||
|
ciphertext.unwrap(),
|
||||||
|
encrypted_key.unwrap(),
|
||||||
|
Some(SharedSecret::from_str(BOB_SHARED_SECRET).unwrap()),
|
||||||
|
);
|
||||||
|
|
||||||
|
let retrieved_plain = aes_dec.unwrap().decrypt_with_key();
|
||||||
|
|
||||||
|
assert!(retrieved_plain.unwrap() == plaintext);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,105 +0,0 @@
|
|||||||
use wasm_bindgen::prelude::*;
|
|
||||||
|
|
||||||
#[wasm_bindgen]
|
|
||||||
pub fn inject_html_create_id() -> String {
|
|
||||||
String::from(
|
|
||||||
"
|
|
||||||
<div class='card'>
|
|
||||||
<div class='side-by-side'>
|
|
||||||
<h3>Create an Id</h3>
|
|
||||||
<div><a href='#'>Processes</a></div>
|
|
||||||
</div>
|
|
||||||
<form id='form4nk' action='#'>
|
|
||||||
<label for='password'>Password :</label>
|
|
||||||
<input type='password' id='password' /><hr/>
|
|
||||||
<input type='hidden' id='currentpage' value='creatid' />
|
|
||||||
<select id='selectProcess' class='custom-select'></select><hr/>
|
|
||||||
<div class='side-by-side'>
|
|
||||||
<button type='submit' id='submitButton' class='bg-primary'>Create</button>
|
|
||||||
<div>
|
|
||||||
<a href='#' id='displayrecover'>Recover</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form><br/>
|
|
||||||
<div id='passwordalert' class='passwordalert'></div>
|
|
||||||
</div>
|
|
||||||
",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[wasm_bindgen]
|
|
||||||
pub fn inject_html_recover() -> String {
|
|
||||||
String::from("
|
|
||||||
<div class='card'>
|
|
||||||
<div class='side-by-side'>
|
|
||||||
<h3>Recover my Id</h3>
|
|
||||||
<div><a href='#'>Processes</a></div>
|
|
||||||
</div>
|
|
||||||
<form id='form4nk' action='#'>
|
|
||||||
<label for='password'>Password :</label>
|
|
||||||
<input type='password' id='password' />
|
|
||||||
<input type='hidden' id='currentpage' value='recover' />
|
|
||||||
<select id='selectProcess' class='custom-select'></select><hr/>
|
|
||||||
<div class='side-by-side'>
|
|
||||||
<button type='submit' id='submitButton' class='recover bg-primary'>Recover</button>
|
|
||||||
<div>
|
|
||||||
<a href='#' id='displaycreateid'>Create an Id</a>
|
|
||||||
</div>
|
|
||||||
</div><hr/>
|
|
||||||
<a href='#' id='displayrevoke' class='btn'>Revoke</a>
|
|
||||||
</form><br/>
|
|
||||||
<div id='passwordalert' class='passwordalert'></div>
|
|
||||||
</div>
|
|
||||||
")
|
|
||||||
}
|
|
||||||
|
|
||||||
#[wasm_bindgen]
|
|
||||||
pub fn inject_html_revokeimage() -> String {
|
|
||||||
String::from("
|
|
||||||
<div class='card'>
|
|
||||||
<div class='side-by-side'>
|
|
||||||
<h3>Revoke image</h3>
|
|
||||||
<div><a href='#' id='displayupdateanid'>Update an Id</a></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class='card-revoke'>
|
|
||||||
<a href='#' download='revoke_4NK.jpg' id='revoke'>
|
|
||||||
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 448 512'>
|
|
||||||
<path
|
|
||||||
d='M246.6 9.4c-12.5-12.5-32.8-12.5-45.3 0l-128 128c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L192 109.3V320c0 17.7 14.3 32 32 32s32-14.3 32-32V109.3l73.4 73.4c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3l-128-128zM64 352c0-17.7-14.3-32-32-32s-32 14.3-32 32v64c0 53 43 96 96 96H352c53 0 96-43 96-96V352c0-17.7-14.3-32-32-32s-32 14.3-32 32v64c0 17.7-14.3 32-32 32H96c-17.7 0-32-14.3-32-32V352z'
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
<div class='image-container'>
|
|
||||||
<img src='assets/4nk_revoke.jpg' alt='' />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
")
|
|
||||||
}
|
|
||||||
|
|
||||||
#[wasm_bindgen]
|
|
||||||
pub fn inject_html_revoke() -> String {
|
|
||||||
String::from(
|
|
||||||
"
|
|
||||||
<div class='card'>
|
|
||||||
<div class='side-by-side'>
|
|
||||||
<h3>Revoke an Id</h3>
|
|
||||||
<div>
|
|
||||||
<a href='#' id='displayrecover'>Recover</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<form id='form4nk' action='#'>
|
|
||||||
<label for='password'>Password :</label>
|
|
||||||
<input type='password' id='password' />
|
|
||||||
<hr/>
|
|
||||||
<div class='image-container'>
|
|
||||||
<label class='image-label'>Revoke image</label>
|
|
||||||
<img src='assets/revoke.jpeg' alt='' />
|
|
||||||
</div>
|
|
||||||
<hr/>
|
|
||||||
<button type='submit' id='submitButton' class='recover bg-primary'>Revoke</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
",
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,8 +1,7 @@
|
|||||||
#![allow(warnings)]
|
#![allow(warnings)]
|
||||||
mod Prd_list;
|
mod Prd_list;
|
||||||
mod aesgcm;
|
|
||||||
pub mod api;
|
pub mod api;
|
||||||
mod injecteurhtml;
|
mod crypto;
|
||||||
mod peers;
|
mod peers;
|
||||||
mod process;
|
mod process;
|
||||||
mod user;
|
mod user;
|
||||||
|
@ -3,6 +3,321 @@ use serde_json::{json, Value};
|
|||||||
use tsify::Tsify;
|
use tsify::Tsify;
|
||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
|
pub const HTML_CREATE_ID: &str = "
|
||||||
|
<div class='card'>
|
||||||
|
<div class='side-by-side'>
|
||||||
|
<h3>Create an Id</h3>
|
||||||
|
<div><a href='#'>Processes</a></div>
|
||||||
|
</div>
|
||||||
|
<form id='form4nk' action='#'>
|
||||||
|
<label for='password'>Password :</label>
|
||||||
|
<input type='password' id='password' /><hr/>
|
||||||
|
<input type='hidden' id='currentpage' value='creatid' />
|
||||||
|
<select id='selectProcess' class='custom-select'></select><hr/>
|
||||||
|
<div class='side-by-side'>
|
||||||
|
<button type='submit' id='submitButton' class='bg-primary'>Create</button>
|
||||||
|
<div>
|
||||||
|
<a href='#' id='displayrecover'>Recover</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form><br/>
|
||||||
|
<div id='passwordalert' class='passwordalert'></div>
|
||||||
|
</div>
|
||||||
|
";
|
||||||
|
|
||||||
|
pub const HTML_UPDATE_ID: &str = "
|
||||||
|
<body>
|
||||||
|
<div class='container'>
|
||||||
|
<div>
|
||||||
|
<h3>Update an Id</h3>
|
||||||
|
</div>
|
||||||
|
<hr />
|
||||||
|
<form id='form4nk' action='#'>
|
||||||
|
<label for='firstName'>First Name:</label>
|
||||||
|
<input type='text' id='firstName' name='firstName' required />
|
||||||
|
|
||||||
|
<label for='lastName'>Last Name:</label>
|
||||||
|
<input type='text' id='lastName' name='lastName' required />
|
||||||
|
|
||||||
|
<label for='Birthday'>Birthday:</label>
|
||||||
|
<input type='date' id='Birthday' name='birthday' />
|
||||||
|
|
||||||
|
<label for='file'>File:</label>
|
||||||
|
<input type='file' id='fileInput' name='file' />
|
||||||
|
|
||||||
|
<label>Third parties:</label>
|
||||||
|
<div id='sp-address-block'>
|
||||||
|
<div class='side-by-side'>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
name='sp-address'
|
||||||
|
id='sp-address'
|
||||||
|
placeholder='sp address'
|
||||||
|
form='no-form'
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
class='circle-btn bg-secondary'
|
||||||
|
id='add-sp-address-btn'
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class='div-text-area'>
|
||||||
|
<textarea
|
||||||
|
name='bio'
|
||||||
|
id=''
|
||||||
|
cols='30'
|
||||||
|
rows='10'
|
||||||
|
placeholder='Bio'
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
<button type='submit' class='bg-primary'>Update</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
";
|
||||||
|
|
||||||
|
pub const HTML_RECOVER: &str = "
|
||||||
|
<div class='card'>
|
||||||
|
<div class='side-by-side'>
|
||||||
|
<h3>Recover my Id</h3>
|
||||||
|
<div><a href='#'>Processes</a></div>
|
||||||
|
</div>
|
||||||
|
<form id='form4nk' action='#'>
|
||||||
|
<label for='password'>Password :</label>
|
||||||
|
<input type='password' id='password' />
|
||||||
|
<input type='hidden' id='currentpage' value='recover' />
|
||||||
|
<select id='selectProcess' class='custom-select'></select><hr/>
|
||||||
|
<div class='side-by-side'>
|
||||||
|
<button type='submit' id='submitButton' class='recover bg-primary'>Recover</button>
|
||||||
|
<div>
|
||||||
|
<a href='#' id='displaycreateid'>Create an Id</a>
|
||||||
|
</div>
|
||||||
|
</div><hr/>
|
||||||
|
<a href='#' id='displayrevoke' class='btn'>Revoke</a>
|
||||||
|
</form><br/>
|
||||||
|
<div id='passwordalert' class='passwordalert'></div>
|
||||||
|
</div>
|
||||||
|
";
|
||||||
|
|
||||||
|
pub const HTML_REVOKE_IMAGE: &str = "
|
||||||
|
<div class='card'>
|
||||||
|
<div class='side-by-side'>
|
||||||
|
<h3>Revoke image</h3>
|
||||||
|
<div><a href='#' id='displayupdateanid'>Update an Id</a></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class='card-revoke'>
|
||||||
|
<a href='#' download='revoke_4NK.jpg' id='revoke'>
|
||||||
|
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 448 512'>
|
||||||
|
<path
|
||||||
|
d='M246.6 9.4c-12.5-12.5-32.8-12.5-45.3 0l-128 128c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L192 109.3V320c0 17.7 14.3 32 32 32s32-14.3 32-32V109.3l73.4 73.4c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3l-128-128zM64 352c0-17.7-14.3-32-32-32s-32 14.3-32 32v64c0 53 43 96 96 96H352c53 0 96-43 96-96V352c0-17.7-14.3-32-32-32s-32 14.3-32 32v64c0 17.7-14.3 32-32 32H96c-17.7 0-32-14.3-32-32V352z'
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
<div class='image-container'>
|
||||||
|
<img src='assets/4nk_revoke.jpg' alt='' />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
";
|
||||||
|
|
||||||
|
pub const HTML_REVOKE: &str = "
|
||||||
|
<div class='card'>
|
||||||
|
<div class='side-by-side'>
|
||||||
|
<h3>Revoke an Id</h3>
|
||||||
|
<div>
|
||||||
|
<a href='#' id='displayrecover'>Recover</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<form id='form4nk' action='#'>
|
||||||
|
<label for='password'>Password :</label>
|
||||||
|
<input type='password' id='password' />
|
||||||
|
<hr/>
|
||||||
|
<div class='image-container'>
|
||||||
|
<label class='image-label'>Revoke image</label>
|
||||||
|
<img src='assets/revoke.jpeg' alt='' />
|
||||||
|
</div>
|
||||||
|
<hr/>
|
||||||
|
<button type='submit' id='submitButton' class='recover bg-primary'>Revoke</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
";
|
||||||
|
|
||||||
|
pub const CSS: &str = "
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: #f4f4f4;
|
||||||
|
font-family: 'Arial', sans-serif;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
max-width: 400px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 8px;
|
||||||
|
text-align: left;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
/* flex-wrap: wrap; */
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
hr {
|
||||||
|
border: 0;
|
||||||
|
height: 1px;
|
||||||
|
background-color: #ddd;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
input, select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
margin: 8px 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
padding: 10px;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
display: inline-block;
|
||||||
|
background-color: #4caf50;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
padding: 12px 17px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
button:hover {
|
||||||
|
background-color: #45a049;
|
||||||
|
}
|
||||||
|
.side-by-side {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.side-by-side>* {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
button.recover {
|
||||||
|
display: inline-block;
|
||||||
|
text-align: center;
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-block;
|
||||||
|
background-color: #4caf50;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
padding: 12px 17px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
button.recover:hover {
|
||||||
|
background-color: #45a049;
|
||||||
|
}
|
||||||
|
a.btn {
|
||||||
|
display: inline-block;
|
||||||
|
text-align: center;
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-block;
|
||||||
|
background-color: #4caf50;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
padding: 12px 17px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.btn:hover {
|
||||||
|
background-color: #45a049;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: #78a6de;
|
||||||
|
}
|
||||||
|
.bg-secondary {
|
||||||
|
background-color: #2b81ed;
|
||||||
|
}
|
||||||
|
.bg-primary {
|
||||||
|
background-color: #1A61ED;
|
||||||
|
}
|
||||||
|
.bg-primary:hover {
|
||||||
|
background-color: #457be8;
|
||||||
|
}
|
||||||
|
.card-revoke {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
max-width: 400px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 8px;
|
||||||
|
text-align: center;
|
||||||
|
align-items: center;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.card-revoke a {
|
||||||
|
max-width: 50px;
|
||||||
|
width: 100%;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.card-revoke button {
|
||||||
|
max-width: 200px;
|
||||||
|
width: 100%;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #78a6de;
|
||||||
|
}
|
||||||
|
.card-revoke svg {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
fill: #333;
|
||||||
|
}
|
||||||
|
.image-label {
|
||||||
|
display: block;
|
||||||
|
color: #fff;
|
||||||
|
padding: 5px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.image-container {
|
||||||
|
width: 400px;
|
||||||
|
height: 300px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.image-container img {
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
object-position: center center;
|
||||||
|
}
|
||||||
|
.passwordalert {
|
||||||
|
color: #FF0000;
|
||||||
|
}
|
||||||
|
";
|
||||||
|
|
||||||
// process member (gestionnaire for now)
|
// process member (gestionnaire for now)
|
||||||
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
|
||||||
pub enum Role {
|
pub enum Role {
|
||||||
@ -10,6 +325,7 @@ pub enum Role {
|
|||||||
#[default]
|
#[default]
|
||||||
User,
|
User,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Default, Tsify, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Default, Tsify, Clone)]
|
||||||
#[tsify(into_wasm_abi)]
|
#[tsify(into_wasm_abi)]
|
||||||
pub struct ItemMember {
|
pub struct ItemMember {
|
||||||
@ -31,8 +347,12 @@ impl ItemMember {
|
|||||||
#[derive(Debug, Serialize, Deserialize, Default, Tsify)]
|
#[derive(Debug, Serialize, Deserialize, Default, Tsify)]
|
||||||
#[tsify(into_wasm_abi, from_wasm_abi)]
|
#[tsify(into_wasm_abi, from_wasm_abi)]
|
||||||
pub struct Process {
|
pub struct Process {
|
||||||
pub id: String,
|
pub id: u32,
|
||||||
|
pub name: String,
|
||||||
pub version: String,
|
pub version: String,
|
||||||
pub gestionnaires: Vec<ItemMember>,
|
pub members: Vec<ItemMember>,
|
||||||
|
pub html: String,
|
||||||
|
pub style: String,
|
||||||
|
pub script: String,
|
||||||
//item_name : String,
|
//item_name : String,
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,7 @@ use wasm_bindgen::prelude::*;
|
|||||||
use shamir::SecretData;
|
use shamir::SecretData;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::{Read, Write, Cursor};
|
use std::io::{Cursor, Read, Write};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::sync::{Mutex, OnceLock};
|
use std::sync::{Mutex, OnceLock};
|
||||||
|
|
||||||
@ -28,9 +28,7 @@ use sp_backend::silentpayments::sending::SilentPaymentAddress;
|
|||||||
use sp_backend::spclient::SpendKey;
|
use sp_backend::spclient::SpendKey;
|
||||||
use sp_backend::spclient::{OutputList, SpClient};
|
use sp_backend::spclient::{OutputList, SpClient};
|
||||||
|
|
||||||
use crate::aesgcm::Aes256Decryption;
|
use crate::crypto::{Aes256Decryption, Aes256Encryption, HalfKey, Purpose};
|
||||||
use crate::aesgcm::HalfKey;
|
|
||||||
use crate::aesgcm::{Aes256Encryption, Purpose};
|
|
||||||
use crate::peers::Peer;
|
use crate::peers::Peer;
|
||||||
use crate::user;
|
use crate::user;
|
||||||
|
|
||||||
@ -200,7 +198,7 @@ impl User {
|
|||||||
let mut entropy1 = [0u8; 32];
|
let mut entropy1 = [0u8; 32];
|
||||||
let mut entropy2 = [0u8; 32];
|
let mut entropy2 = [0u8; 32];
|
||||||
let mut entropy3 = [0u8; 32];
|
let mut entropy3 = [0u8; 32];
|
||||||
let mut cipher_scan_key = [0u8; 60];
|
let mut cipher_scan_key = [0u8; 60]; // cipher length == plain.len() + 16 + nonce.len()
|
||||||
let mut part1_ciphertext = [0u8; 44];
|
let mut part1_ciphertext = [0u8; 44];
|
||||||
|
|
||||||
let mut reader = Cursor::new(recover_data);
|
let mut reader = Cursor::new(recover_data);
|
||||||
|
@ -38,8 +38,14 @@ class Database {
|
|||||||
},
|
},
|
||||||
AnkProcess: {
|
AnkProcess: {
|
||||||
name: "process",
|
name: "process",
|
||||||
options: {},
|
options: {'keyPath': 'id'},
|
||||||
indices: []
|
indices: [{
|
||||||
|
name: 'by_name',
|
||||||
|
keyPath: 'name',
|
||||||
|
options: {
|
||||||
|
'unique': true
|
||||||
|
}
|
||||||
|
}]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -101,7 +107,7 @@ class Database {
|
|||||||
return objectList;
|
return objectList;
|
||||||
}
|
}
|
||||||
|
|
||||||
public writeObject(db: IDBDatabase, storeName: string, obj: any, key: string | null): Promise<IDBRequest> {
|
public writeObject(db: IDBDatabase, storeName: string, obj: any, key: IDBValidKey | null): Promise<IDBRequest> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const transaction = db.transaction(storeName, 'readwrite');
|
const transaction = db.transaction(storeName, 'readwrite');
|
||||||
const store = transaction.objectStore(storeName);
|
const store = transaction.objectStore(storeName);
|
||||||
@ -117,7 +123,7 @@ class Database {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public getObject<T>(db: IDBDatabase, storeName: string, key: string | number): Promise<T> {
|
public getObject<T>(db: IDBDatabase, storeName: string, key: IDBValidKey): Promise<T> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const transaction = db.transaction(storeName, 'readonly');
|
const transaction = db.transaction(storeName, 'readonly');
|
||||||
const store = transaction.objectStore(storeName);
|
const store = transaction.objectStore(storeName);
|
||||||
@ -128,6 +134,25 @@ class Database {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getFirstMatchWithIndex<T>(db: IDBDatabase, storeName: string, indexName: string, lookup: string): Promise<T | null> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const transaction = db.transaction(storeName, 'readonly');
|
||||||
|
const store = transaction.objectStore(storeName);
|
||||||
|
const index = store.index(indexName);
|
||||||
|
const request = index.openCursor(IDBKeyRange.only(lookup));
|
||||||
|
|
||||||
|
request.onerror = () => reject(request.error);
|
||||||
|
request.onsuccess = () => {
|
||||||
|
const cursor = request.result;
|
||||||
|
if (cursor) {
|
||||||
|
resolve(cursor.value);
|
||||||
|
} else {
|
||||||
|
resolve(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public setObject(db: IDBDatabase, storeName: string, obj: any, key: string | null): Promise<IDBRequest> {
|
public setObject(db: IDBDatabase, storeName: string, obj: any, key: string | null): Promise<IDBRequest> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const transaction = db.transaction(storeName, 'readwrite');
|
const transaction = db.transaction(storeName, 'readwrite');
|
||||||
|
@ -4,6 +4,7 @@ import IndexedDB from './database'
|
|||||||
document.addEventListener('DOMContentLoaded', async () => {
|
document.addEventListener('DOMContentLoaded', async () => {
|
||||||
try {
|
try {
|
||||||
const services = await Services.getInstance();
|
const services = await Services.getInstance();
|
||||||
|
|
||||||
if ((await services.isNewUser())) {
|
if ((await services.isNewUser())) {
|
||||||
await services.displayCreateId();
|
await services.displayCreateId();
|
||||||
}
|
}
|
||||||
|
176
src/services.ts
176
src/services.ts
@ -1,6 +1,5 @@
|
|||||||
import { createUserReturn, User } from '../dist/pkg/sdk_client';
|
import { createUserReturn, User, Process } from '../dist/pkg/sdk_client';
|
||||||
import IndexedDB from './database'
|
import IndexedDB from './database'
|
||||||
import Processstore from './store/processstore';
|
|
||||||
|
|
||||||
class Services {
|
class Services {
|
||||||
private static instance: Services;
|
private static instance: Services;
|
||||||
@ -60,14 +59,11 @@ class Services {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async displayCreateId(): Promise<void> {
|
public async displayCreateId(): Promise<void> {
|
||||||
Services.instance.injectHtml(Services.instance.get_html_create_id());
|
const services = await Services.getInstance();
|
||||||
Services.instance.attachSubmitListener("form4nk", (event) => Services.instance.createId(event));
|
await services.injectHtml('CREATE_ID');
|
||||||
Services.instance.attachClickListener("displayrecover", Services.instance.displayRecover);
|
services.attachSubmitListener("form4nk", (event) => services.createId(event));
|
||||||
Services.instance.displayProcess(await Services.instance.getAllProcess());
|
services.attachClickListener("displayrecover", services.displayRecover);
|
||||||
}
|
services.displayProcess();
|
||||||
|
|
||||||
public get_html_create_id(): string {
|
|
||||||
return this.sdkClient.inject_html_create_id();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async createId(event: Event): Promise<void> {
|
public async createId(event: Event): Promise<void> {
|
||||||
@ -88,15 +84,14 @@ class Services {
|
|||||||
// if (!Services.instance.isPasswordValid(password)) return;
|
// if (!Services.instance.isPasswordValid(password)) return;
|
||||||
|
|
||||||
let label = null;
|
let label = null;
|
||||||
let birthday = 50000;
|
let birthday_signet = 50000;
|
||||||
const user: createUserReturn = this.sdkClient.create_user(password, label, birthday, this.current_process);
|
let birthday_main = 500000;
|
||||||
|
const user: createUserReturn = this.sdkClient.create_user(password, label, birthday_main, birthday_signet, this.current_process);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const indexedDb = await IndexedDB.getInstance();
|
const indexedDb = await IndexedDB.getInstance();
|
||||||
const db = indexedDb.getDb();
|
const db = indexedDb.getDb();
|
||||||
await indexedDb.writeObject(db, indexedDb.getStoreList().AnkUser, user.user, null);
|
await indexedDb.writeObject(db, indexedDb.getStoreList().AnkUser, user.user, null);
|
||||||
// console.log("JS User added");
|
|
||||||
|
|
||||||
await indexedDb.writeObject(db, indexedDb.getStoreList().SpOutputs, user.output_list_vec, null);
|
await indexedDb.writeObject(db, indexedDb.getStoreList().SpOutputs, user.output_list_vec, null);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to write user object :", error);
|
console.error("Failed to write user object :", error);
|
||||||
@ -106,16 +101,13 @@ class Services {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async displayRecover(): Promise<void> {
|
public async displayRecover(): Promise<void> {
|
||||||
Services.instance.injectHtml(Services.instance.get_html_recover());
|
const services = await Services.getInstance();
|
||||||
Services.instance.attachSubmitListener("form4nk", Services.instance.recover);
|
services.injectHtml('RECOVER');
|
||||||
Services.instance.attachClickListener("displaycreateid", Services.instance.displayCreateId);
|
services.attachSubmitListener("form4nk", services.recover);
|
||||||
Services.instance.attachClickListener("displayrevoke", Services.instance.displayRevoke);
|
services.attachClickListener("displaycreateid", services.displayCreateId);
|
||||||
Services.instance.attachClickListener("submitButtonRevoke", Services.instance.revoke);
|
services.attachClickListener("displayrevoke", services.displayRevoke);
|
||||||
Services.instance.displayProcess(await Services.instance.getAllUserProcess());
|
services.attachClickListener("submitButtonRevoke", services.revoke);
|
||||||
}
|
services.displayProcess();
|
||||||
|
|
||||||
public get_html_recover(): string {
|
|
||||||
return this.sdkClient.inject_html_recover();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async recover(event: Event) {
|
public async recover(event: Event) {
|
||||||
@ -141,8 +133,7 @@ class Services {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async displayRevokeImage(): Promise<void> {
|
public async displayRevokeImage(): Promise<void> {
|
||||||
const html = Services.instance.get_html_revokeimage();
|
Services.instance.injectHtml('REVOKE_IMAGE');
|
||||||
Services.instance.injectHtml(html);
|
|
||||||
Services.instance.attachClickListener("displayupdateanid", Services.instance.displayUpdateAnId);
|
Services.instance.attachClickListener("displayupdateanid", Services.instance.displayUpdateAnId);
|
||||||
|
|
||||||
let imageBytes = await Services.instance.getRecoverImage('assets/4nk_revoke.jpg');
|
let imageBytes = await Services.instance.getRecoverImage('assets/4nk_revoke.jpg');
|
||||||
@ -155,10 +146,6 @@ class Services {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public get_html_revokeimage(): string {
|
|
||||||
return this.sdkClient.inject_html_revokeimage();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async getRecoverImage(imageUrl:string): Promise<Uint8Array|null> {
|
private async getRecoverImage(imageUrl:string): Promise<Uint8Array|null> {
|
||||||
let imageBytes = null;
|
let imageBytes = null;
|
||||||
try {
|
try {
|
||||||
@ -175,16 +162,11 @@ class Services {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async displayRevoke(): Promise<void> {
|
public async displayRevoke(): Promise<void> {
|
||||||
const html = Services.instance.get_html_revoke();
|
Services.instance.injectHtml('REVOKE');
|
||||||
Services.instance.injectHtml(html);
|
|
||||||
Services.instance.attachClickListener("displayrecover", Services.instance.displayRecover);
|
Services.instance.attachClickListener("displayrecover", Services.instance.displayRecover);
|
||||||
Services.instance.attachSubmitListener("form4nk", Services.instance.revoke);
|
Services.instance.attachSubmitListener("form4nk", Services.instance.revoke);
|
||||||
}
|
}
|
||||||
|
|
||||||
public get_html_revoke(): string {
|
|
||||||
return this.sdkClient.inject_html_revoke();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async revoke(event: Event): Promise<void> {
|
public async revoke(event: Event): Promise<void> {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
console.log("JS revoke click ");
|
console.log("JS revoke click ");
|
||||||
@ -198,18 +180,14 @@ class Services {
|
|||||||
let style = "";
|
let style = "";
|
||||||
let script = "";
|
let script = "";
|
||||||
try {
|
try {
|
||||||
const indexedDB = await IndexedDB.getInstance();
|
const processObject = await this.getProcessByName(Services.instance.current_process!);
|
||||||
const db = indexedDB.getDb();
|
if (processObject) {
|
||||||
try {
|
|
||||||
let processObject = await indexedDB.getObject<Processstore>(db, indexedDB.getStoreList().AnkProcess, Services.instance.current_process!);
|
|
||||||
body = processObject.html;
|
body = processObject.html;
|
||||||
style = processObject.style;
|
style = processObject.style;
|
||||||
script = processObject.script;
|
script = processObject.script;
|
||||||
} catch (error) {
|
|
||||||
console.log("JS Processstore not exist ");
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to retrieve user object :", error);
|
console.error("Failed to retrieve process with Error:", error);
|
||||||
}
|
}
|
||||||
|
|
||||||
Services.instance.injectUpdateAnIdHtml(body, style, script);
|
Services.instance.injectUpdateAnIdHtml(body, style, script);
|
||||||
@ -253,76 +231,82 @@ class Services {
|
|||||||
// TODO Mock add user member to process
|
// TODO Mock add user member to process
|
||||||
}
|
}
|
||||||
|
|
||||||
public displayProcess(processList: string[]): void {
|
public async displayProcess(): Promise<void> {
|
||||||
console.log("JS processList : "+processList);
|
const services = await Services.getInstance();
|
||||||
|
const processList = await services.getAllProcess();
|
||||||
const selectProcess = document.getElementById("selectProcess");
|
const selectProcess = document.getElementById("selectProcess");
|
||||||
if (selectProcess) {
|
if (selectProcess) {
|
||||||
processList.forEach((process) => {
|
processList.forEach((process) => {
|
||||||
let child = new Option(process, process);
|
let child = new Option(process.name, process.name);
|
||||||
if (!selectProcess.contains(child)) {
|
if (!selectProcess.contains(child)) {
|
||||||
selectProcess.appendChild(child);
|
selectProcess.appendChild(child);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getAllUserProcess(): Promise<string[]> {
|
public async getAllProcess(): Promise<Process[]> {
|
||||||
let userProcessList: string[] = [];
|
|
||||||
try {
|
try {
|
||||||
const indexedDB = await IndexedDB.getInstance();
|
const indexedDB = await IndexedDB.getInstance();
|
||||||
const db = indexedDB.getDb();
|
const db = indexedDB.getDb();
|
||||||
let processListObject = await indexedDB.getAll<Processstore>(db, indexedDB.getStoreList().AnkProcess);
|
let processListObject = await indexedDB.getAll<Process>(db, indexedDB.getStoreList().AnkProcess);
|
||||||
|
return processListObject;
|
||||||
|
} catch (error) {
|
||||||
|
console.log('getAllProcess failed: ',error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getAllProcessForUser(pre_id: string): Promise<Process[]> {
|
||||||
|
const services = await Services.getInstance();
|
||||||
|
let user: User;
|
||||||
|
let userProcessList: Process[] = [];
|
||||||
|
try {
|
||||||
|
const indexedDB = await IndexedDB.getInstance();
|
||||||
|
const db = indexedDB.getDb();
|
||||||
|
user = await indexedDB.getObject<User>(db, indexedDB.getStoreList().AnkUser, pre_id);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('getAllUserProcess failed: ',error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const processListObject = await services.getAllProcess();
|
||||||
processListObject.forEach(async (processObject) => {
|
processListObject.forEach(async (processObject) => {
|
||||||
const listMember = processObject.listMember;
|
if (processObject.members.includes(user.pre_id)) {
|
||||||
const processName = processObject.process;
|
userProcessList.push(processObject);
|
||||||
listMember.forEach(async (member) => {
|
}
|
||||||
if (member == "user1") {
|
|
||||||
userProcessList.push(processName);
|
|
||||||
console.log("JS UserProcess found");
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("JS Processstore not found");
|
console.error('getAllUserProcess failed: ',error);
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
return userProcessList;
|
return userProcessList;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getAllProcess(): Promise<string[]> {
|
public async getProcessByName(name: string): Promise<Process | null> {
|
||||||
// if indexedDB is empty, get list from wasm
|
const indexedDB = await IndexedDB.getInstance();
|
||||||
let processList: string[] = [];
|
const db = indexedDB.getDb();
|
||||||
try {
|
const process = await indexedDB.getFirstMatchWithIndex<Process>(db, indexedDB.getStoreList().AnkProcess, 'by_name', name);
|
||||||
const indexedDB = await IndexedDB.getInstance();
|
|
||||||
const db = indexedDB.getDb();
|
return process;
|
||||||
let processListObject = await indexedDB.getAll<Processstore>(db, indexedDB.getStoreList().AnkProcess);
|
|
||||||
processListObject.forEach(async (processObject) => {
|
|
||||||
const processName = processObject.process;
|
|
||||||
processList.push(processName);
|
|
||||||
console.log("JS Processstore found");
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.log("JS Processstore not found");
|
|
||||||
}
|
|
||||||
if (processList.length == 0) {
|
|
||||||
processList = await this.addProcessStore();
|
|
||||||
}
|
|
||||||
return processList;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async addProcessStore(): Promise<string[]> {
|
public async loadProcesses(): Promise<void> {
|
||||||
const processList = this.sdkClient.get_process()
|
const services = await Services.getInstance();
|
||||||
processList.forEach(async (process: string) => {
|
const processList: Process[] = services.sdkClient.get_processes();
|
||||||
// TODO process mock
|
processList.forEach(async (process: Process) => {
|
||||||
let processstore = new Processstore;
|
|
||||||
processstore.process = process;
|
|
||||||
const indexedDB = await IndexedDB.getInstance();
|
const indexedDB = await IndexedDB.getInstance();
|
||||||
const db = indexedDB.getDb();
|
const db = indexedDB.getDb();
|
||||||
await indexedDB.writeObject(db, indexedDB.getStoreList().AnkProcess, processstore, process);
|
try {
|
||||||
console.log("JS Processstore mock added");
|
if (await indexedDB.getObject<Process>(db, indexedDB.getStoreList().AnkProcess, process.id) === null) {
|
||||||
|
await indexedDB.writeObject(db, indexedDB.getStoreList().AnkProcess, process, null);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Error while writing process', process.name, 'to indexedDB:', error);
|
||||||
|
}
|
||||||
})
|
})
|
||||||
return processList;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public attachClickListener(elementId: string, callback: (event: Event) => void): void {
|
public attachClickListener(elementId: string, callback: (event: Event) => void): void {
|
||||||
const element = document.getElementById(elementId);
|
const element = document.getElementById(elementId);
|
||||||
@ -336,15 +320,25 @@ class Services {
|
|||||||
element?.addEventListener("submit", callback);
|
element?.addEventListener("submit", callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
public injectHtml(html: string) {
|
public async injectHtml(processName: string) {
|
||||||
console.log("JS html : "+html);
|
// console.log("JS html : "+html);
|
||||||
const container = document.getElementById('containerId');
|
const container = document.getElementById('containerId');
|
||||||
|
|
||||||
if (!container) {
|
if (!container) {
|
||||||
console.error("No html container");
|
console.error("No html container");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
container.innerHTML = html;
|
|
||||||
|
const services = await Services.getInstance();
|
||||||
|
|
||||||
|
await services.loadProcesses();
|
||||||
|
|
||||||
|
const process = await services.getProcessByName(processName);
|
||||||
|
if (process) {
|
||||||
|
container.innerHTML = process.html;
|
||||||
|
} else {
|
||||||
|
console.error("No process ", processName);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// public async getCurrentProcess(): Promise<string> {
|
// public async getCurrentProcess(): Promise<string> {
|
||||||
|
@ -1,237 +0,0 @@
|
|||||||
class Processstore {
|
|
||||||
process: string;
|
|
||||||
html: string;
|
|
||||||
style: string;
|
|
||||||
script: string;
|
|
||||||
listMember: string[];
|
|
||||||
createDate: Date;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.process = "";
|
|
||||||
this.html = getMockHtml();
|
|
||||||
this.style = getMockStyle();
|
|
||||||
this.script = getMockScript();
|
|
||||||
this.script = getMockScript();
|
|
||||||
this.listMember = getMockListMember();
|
|
||||||
this.createDate = new Date;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Processstore;
|
|
||||||
|
|
||||||
function getMockHtml(): string {
|
|
||||||
let html: string = `
|
|
||||||
<body>
|
|
||||||
<div class='container'>
|
|
||||||
<div>
|
|
||||||
<h3>Update an Id</h3>
|
|
||||||
</div>
|
|
||||||
<hr />
|
|
||||||
<form id='form4nk' action='#'>
|
|
||||||
<label for='firstName'>First Name:</label>
|
|
||||||
<input type='text' id='firstName' name='firstName' required />
|
|
||||||
|
|
||||||
<label for='lastName'>Last Name:</label>
|
|
||||||
<input type='text' id='lastName' name='lastName' required />
|
|
||||||
|
|
||||||
<label for='Birthday'>Birthday:</label>
|
|
||||||
<input type='date' id='Birthday' name='birthday' />
|
|
||||||
|
|
||||||
<label for='file'>File:</label>
|
|
||||||
<input type='file' id='fileInput' name='file' />
|
|
||||||
|
|
||||||
<label>Third parties:</label>
|
|
||||||
<div id='sp-address-block'>
|
|
||||||
<div class='side-by-side'>
|
|
||||||
<input
|
|
||||||
type='text'
|
|
||||||
name='sp-address'
|
|
||||||
id='sp-address'
|
|
||||||
placeholder='sp address'
|
|
||||||
form='no-form'
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type='button'
|
|
||||||
class='circle-btn bg-secondary'
|
|
||||||
id='add-sp-address-btn'
|
|
||||||
>
|
|
||||||
+
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class='div-text-area'>
|
|
||||||
<textarea
|
|
||||||
name='bio'
|
|
||||||
id=''
|
|
||||||
cols='30'
|
|
||||||
rows='10'
|
|
||||||
placeholder='Bio'
|
|
||||||
></textarea>
|
|
||||||
</div>
|
|
||||||
<button type='submit' class='bg-primary'>Update</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
`;
|
|
||||||
return html;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getMockStyle(): string {
|
|
||||||
let style: string = `
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
min-height: 100vh;
|
|
||||||
background-color: #f4f4f4;
|
|
||||||
font-family: 'Arial', sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
max-width: 400px;
|
|
||||||
width: 100%;
|
|
||||||
padding: 20px;
|
|
||||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
|
||||||
background-color: #ffffff;
|
|
||||||
border-radius: 8px;
|
|
||||||
text-align: left;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
form {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(1fr, 2fr);
|
|
||||||
gap: 10px;
|
|
||||||
max-width: 400px;
|
|
||||||
margin: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bg-primary {
|
|
||||||
background-color: #1a61ed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bg-primary:hover {
|
|
||||||
background-color: #457be8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bg-secondary {
|
|
||||||
background-color: #2b81ed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bg-secondary:hover {
|
|
||||||
background-color: #5f9bff;
|
|
||||||
}
|
|
||||||
|
|
||||||
label {
|
|
||||||
text-align: left;
|
|
||||||
padding-right: 10px;
|
|
||||||
line-height: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
input {
|
|
||||||
width: 100%;
|
|
||||||
padding: 8px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
grid-column: span 2;
|
|
||||||
display: inline-block;
|
|
||||||
color: #fff;
|
|
||||||
border: none;
|
|
||||||
padding: 12px 17px;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.div-text-area {
|
|
||||||
grid-column: span 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
textarea {
|
|
||||||
width: 100%;
|
|
||||||
padding: 8px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.side-by-side {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 10px;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
.circle-btn {
|
|
||||||
width: 25px;
|
|
||||||
height: 25px;
|
|
||||||
border-radius: 50%;
|
|
||||||
border: none;
|
|
||||||
color: white;
|
|
||||||
padding: 0px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
#fileInput {
|
|
||||||
width: 100%;
|
|
||||||
padding: 8px;
|
|
||||||
padding-left: 0px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
</style>`;
|
|
||||||
return style;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getMockScript(): string {
|
|
||||||
let script: string = `
|
|
||||||
var addSpAddressBtn = document.getElementById('add-sp-address-btn');
|
|
||||||
var removeSpAddressBtn = document.querySelectorAll('.minus-sp-address-btn');
|
|
||||||
|
|
||||||
addSpAddressBtn.addEventListener('click', function (event) {
|
|
||||||
addDynamicField(this);
|
|
||||||
});
|
|
||||||
|
|
||||||
function addDynamicField(element) {
|
|
||||||
var addSpAddressBlock = document.getElementById('sp-address-block');
|
|
||||||
var spAddress = addSpAddressBlock.querySelector('#sp-address').value;
|
|
||||||
addSpAddressBlock.querySelector('#sp-address').value = '';
|
|
||||||
spAddress = spAddress.trim();
|
|
||||||
if (spAddress != '') {
|
|
||||||
var sideBySideDiv = document.createElement('div');
|
|
||||||
sideBySideDiv.className = 'side-by-side';
|
|
||||||
|
|
||||||
var inputElement = document.createElement('input');
|
|
||||||
inputElement.type = 'text';
|
|
||||||
inputElement.name = 'spAddresses[]';
|
|
||||||
inputElement.setAttribute('form', 'no-form');
|
|
||||||
inputElement.value = spAddress;
|
|
||||||
inputElement.disabled = true;
|
|
||||||
|
|
||||||
var buttonElement = document.createElement('button');
|
|
||||||
buttonElement.type = 'button';
|
|
||||||
buttonElement.className =
|
|
||||||
'circle-btn bg-secondary minus-sp-address-btn';
|
|
||||||
buttonElement.innerHTML = '-';
|
|
||||||
|
|
||||||
buttonElement.addEventListener('click', function (event) {
|
|
||||||
removeDynamicField(this.parentElement);
|
|
||||||
});
|
|
||||||
|
|
||||||
sideBySideDiv.appendChild(inputElement);
|
|
||||||
sideBySideDiv.appendChild(buttonElement);
|
|
||||||
|
|
||||||
addSpAddressBlock.appendChild(sideBySideDiv);
|
|
||||||
}
|
|
||||||
function removeDynamicField(element) {
|
|
||||||
element.remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
return script;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getMockListMember(): string[] {
|
|
||||||
return ["user1","user2","user3"];
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user