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
|
||||
let process1 = Process {
|
||||
id: String::from("1"),
|
||||
id: 1,
|
||||
name: String::from("CREATE_ID"),
|
||||
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 {
|
||||
id: String::from("2"),
|
||||
version: String::from("2.0"),
|
||||
gestionnaires: vec![member2.clone(), member3.clone()],
|
||||
id: 2,
|
||||
name: String::from("UPDATE_ID"),
|
||||
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 {
|
||||
id: String::from("3"),
|
||||
id: 3,
|
||||
name: String::from("RECOVER"),
|
||||
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
|
||||
@ -193,6 +223,8 @@ pub fn get_processes() -> ApiResult<get_process_return> {
|
||||
data_process.push(process1);
|
||||
data_process.push(process2);
|
||||
data_process.push(process3);
|
||||
data_process.push(process4);
|
||||
data_process.push(process5);
|
||||
Ok(get_process_return(data_process))
|
||||
}
|
||||
|
||||
|
@ -1,9 +1,447 @@
|
||||
use aes_gcm;
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub enum KeyType {
|
||||
Aes256GcmIv96BitKey([u8;32])
|
||||
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"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EncryptionKey {
|
||||
sk: KeyType
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -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)]
|
||||
mod Prd_list;
|
||||
mod aesgcm;
|
||||
pub mod api;
|
||||
mod injecteurhtml;
|
||||
mod crypto;
|
||||
mod peers;
|
||||
mod process;
|
||||
mod user;
|
||||
|
@ -3,6 +3,321 @@ use serde_json::{json, Value};
|
||||
use tsify::Tsify;
|
||||
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)
|
||||
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
|
||||
pub enum Role {
|
||||
@ -10,6 +325,7 @@ pub enum Role {
|
||||
#[default]
|
||||
User,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Default, Tsify, Clone)]
|
||||
#[tsify(into_wasm_abi)]
|
||||
pub struct ItemMember {
|
||||
@ -31,8 +347,12 @@ impl ItemMember {
|
||||
#[derive(Debug, Serialize, Deserialize, Default, Tsify)]
|
||||
#[tsify(into_wasm_abi, from_wasm_abi)]
|
||||
pub struct Process {
|
||||
pub id: String,
|
||||
pub id: u32,
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
pub gestionnaires: Vec<ItemMember>,
|
||||
pub members: Vec<ItemMember>,
|
||||
pub html: String,
|
||||
pub style: String,
|
||||
pub script: String,
|
||||
//item_name : String,
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ use wasm_bindgen::prelude::*;
|
||||
use shamir::SecretData;
|
||||
use std::collections::HashMap;
|
||||
use std::fs::File;
|
||||
use std::io::{Read, Write, Cursor};
|
||||
use std::io::{Cursor, Read, Write};
|
||||
use std::str::FromStr;
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
|
||||
@ -28,9 +28,7 @@ use sp_backend::silentpayments::sending::SilentPaymentAddress;
|
||||
use sp_backend::spclient::SpendKey;
|
||||
use sp_backend::spclient::{OutputList, SpClient};
|
||||
|
||||
use crate::aesgcm::Aes256Decryption;
|
||||
use crate::aesgcm::HalfKey;
|
||||
use crate::aesgcm::{Aes256Encryption, Purpose};
|
||||
use crate::crypto::{Aes256Decryption, Aes256Encryption, HalfKey, Purpose};
|
||||
use crate::peers::Peer;
|
||||
use crate::user;
|
||||
|
||||
@ -200,7 +198,7 @@ impl User {
|
||||
let mut entropy1 = [0u8; 32];
|
||||
let mut entropy2 = [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 reader = Cursor::new(recover_data);
|
||||
|
@ -38,8 +38,14 @@ class Database {
|
||||
},
|
||||
AnkProcess: {
|
||||
name: "process",
|
||||
options: {},
|
||||
indices: []
|
||||
options: {'keyPath': 'id'},
|
||||
indices: [{
|
||||
name: 'by_name',
|
||||
keyPath: 'name',
|
||||
options: {
|
||||
'unique': true
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
@ -101,7 +107,7 @@ class Database {
|
||||
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) => {
|
||||
const transaction = db.transaction(storeName, 'readwrite');
|
||||
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) => {
|
||||
const transaction = db.transaction(storeName, 'readonly');
|
||||
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> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction(storeName, 'readwrite');
|
||||
|
@ -4,6 +4,7 @@ import IndexedDB from './database'
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
try {
|
||||
const services = await Services.getInstance();
|
||||
|
||||
if ((await services.isNewUser())) {
|
||||
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 Processstore from './store/processstore';
|
||||
|
||||
class Services {
|
||||
private static instance: Services;
|
||||
@ -60,14 +59,11 @@ class Services {
|
||||
}
|
||||
|
||||
public async displayCreateId(): Promise<void> {
|
||||
Services.instance.injectHtml(Services.instance.get_html_create_id());
|
||||
Services.instance.attachSubmitListener("form4nk", (event) => Services.instance.createId(event));
|
||||
Services.instance.attachClickListener("displayrecover", Services.instance.displayRecover);
|
||||
Services.instance.displayProcess(await Services.instance.getAllProcess());
|
||||
}
|
||||
|
||||
public get_html_create_id(): string {
|
||||
return this.sdkClient.inject_html_create_id();
|
||||
const services = await Services.getInstance();
|
||||
await services.injectHtml('CREATE_ID');
|
||||
services.attachSubmitListener("form4nk", (event) => services.createId(event));
|
||||
services.attachClickListener("displayrecover", services.displayRecover);
|
||||
services.displayProcess();
|
||||
}
|
||||
|
||||
public async createId(event: Event): Promise<void> {
|
||||
@ -88,15 +84,14 @@ class Services {
|
||||
// if (!Services.instance.isPasswordValid(password)) return;
|
||||
|
||||
let label = null;
|
||||
let birthday = 50000;
|
||||
const user: createUserReturn = this.sdkClient.create_user(password, label, birthday, this.current_process);
|
||||
let birthday_signet = 50000;
|
||||
let birthday_main = 500000;
|
||||
const user: createUserReturn = this.sdkClient.create_user(password, label, birthday_main, birthday_signet, this.current_process);
|
||||
|
||||
try {
|
||||
const indexedDb = await IndexedDB.getInstance();
|
||||
const db = indexedDb.getDb();
|
||||
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);
|
||||
} catch (error) {
|
||||
console.error("Failed to write user object :", error);
|
||||
@ -106,16 +101,13 @@ class Services {
|
||||
}
|
||||
|
||||
public async displayRecover(): Promise<void> {
|
||||
Services.instance.injectHtml(Services.instance.get_html_recover());
|
||||
Services.instance.attachSubmitListener("form4nk", Services.instance.recover);
|
||||
Services.instance.attachClickListener("displaycreateid", Services.instance.displayCreateId);
|
||||
Services.instance.attachClickListener("displayrevoke", Services.instance.displayRevoke);
|
||||
Services.instance.attachClickListener("submitButtonRevoke", Services.instance.revoke);
|
||||
Services.instance.displayProcess(await Services.instance.getAllUserProcess());
|
||||
}
|
||||
|
||||
public get_html_recover(): string {
|
||||
return this.sdkClient.inject_html_recover();
|
||||
const services = await Services.getInstance();
|
||||
services.injectHtml('RECOVER');
|
||||
services.attachSubmitListener("form4nk", services.recover);
|
||||
services.attachClickListener("displaycreateid", services.displayCreateId);
|
||||
services.attachClickListener("displayrevoke", services.displayRevoke);
|
||||
services.attachClickListener("submitButtonRevoke", services.revoke);
|
||||
services.displayProcess();
|
||||
}
|
||||
|
||||
public async recover(event: Event) {
|
||||
@ -141,8 +133,7 @@ class Services {
|
||||
}
|
||||
|
||||
public async displayRevokeImage(): Promise<void> {
|
||||
const html = Services.instance.get_html_revokeimage();
|
||||
Services.instance.injectHtml(html);
|
||||
Services.instance.injectHtml('REVOKE_IMAGE');
|
||||
Services.instance.attachClickListener("displayupdateanid", Services.instance.displayUpdateAnId);
|
||||
|
||||
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> {
|
||||
let imageBytes = null;
|
||||
try {
|
||||
@ -175,16 +162,11 @@ class Services {
|
||||
}
|
||||
|
||||
public async displayRevoke(): Promise<void> {
|
||||
const html = Services.instance.get_html_revoke();
|
||||
Services.instance.injectHtml(html);
|
||||
Services.instance.injectHtml('REVOKE');
|
||||
Services.instance.attachClickListener("displayrecover", Services.instance.displayRecover);
|
||||
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> {
|
||||
event.preventDefault();
|
||||
console.log("JS revoke click ");
|
||||
@ -198,18 +180,14 @@ class Services {
|
||||
let style = "";
|
||||
let script = "";
|
||||
try {
|
||||
const indexedDB = await IndexedDB.getInstance();
|
||||
const db = indexedDB.getDb();
|
||||
try {
|
||||
let processObject = await indexedDB.getObject<Processstore>(db, indexedDB.getStoreList().AnkProcess, Services.instance.current_process!);
|
||||
const processObject = await this.getProcessByName(Services.instance.current_process!);
|
||||
if (processObject) {
|
||||
body = processObject.html;
|
||||
style = processObject.style;
|
||||
script = processObject.script;
|
||||
} catch (error) {
|
||||
console.log("JS Processstore not exist ");
|
||||
}
|
||||
} 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);
|
||||
@ -253,76 +231,82 @@ class Services {
|
||||
// TODO Mock add user member to process
|
||||
}
|
||||
|
||||
public displayProcess(processList: string[]): void {
|
||||
console.log("JS processList : "+processList);
|
||||
public async displayProcess(): Promise<void> {
|
||||
const services = await Services.getInstance();
|
||||
const processList = await services.getAllProcess();
|
||||
const selectProcess = document.getElementById("selectProcess");
|
||||
if (selectProcess) {
|
||||
processList.forEach((process) => {
|
||||
let child = new Option(process, process);
|
||||
let child = new Option(process.name, process.name);
|
||||
if (!selectProcess.contains(child)) {
|
||||
selectProcess.appendChild(child);
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
public async getAllUserProcess(): Promise<string[]> {
|
||||
let userProcessList: string[] = [];
|
||||
|
||||
public async getAllProcess(): Promise<Process[]> {
|
||||
try {
|
||||
const indexedDB = await IndexedDB.getInstance();
|
||||
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) => {
|
||||
const listMember = processObject.listMember;
|
||||
const processName = processObject.process;
|
||||
listMember.forEach(async (member) => {
|
||||
if (member == "user1") {
|
||||
userProcessList.push(processName);
|
||||
console.log("JS UserProcess found");
|
||||
}
|
||||
})
|
||||
if (processObject.members.includes(user.pre_id)) {
|
||||
userProcessList.push(processObject);
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.log("JS Processstore not found");
|
||||
console.error('getAllUserProcess failed: ',error);
|
||||
return [];
|
||||
}
|
||||
return userProcessList;
|
||||
}
|
||||
|
||||
public async getAllProcess(): Promise<string[]> {
|
||||
// if indexedDB is empty, get list from wasm
|
||||
let processList: string[] = [];
|
||||
try {
|
||||
const indexedDB = await IndexedDB.getInstance();
|
||||
const db = indexedDB.getDb();
|
||||
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 getProcessByName(name: string): Promise<Process | null> {
|
||||
const indexedDB = await IndexedDB.getInstance();
|
||||
const db = indexedDB.getDb();
|
||||
const process = await indexedDB.getFirstMatchWithIndex<Process>(db, indexedDB.getStoreList().AnkProcess, 'by_name', name);
|
||||
|
||||
return process;
|
||||
}
|
||||
|
||||
public async addProcessStore(): Promise<string[]> {
|
||||
const processList = this.sdkClient.get_process()
|
||||
processList.forEach(async (process: string) => {
|
||||
// TODO process mock
|
||||
let processstore = new Processstore;
|
||||
processstore.process = process;
|
||||
public async loadProcesses(): Promise<void> {
|
||||
const services = await Services.getInstance();
|
||||
const processList: Process[] = services.sdkClient.get_processes();
|
||||
processList.forEach(async (process: Process) => {
|
||||
const indexedDB = await IndexedDB.getInstance();
|
||||
const db = indexedDB.getDb();
|
||||
await indexedDB.writeObject(db, indexedDB.getStoreList().AnkProcess, processstore, process);
|
||||
console.log("JS Processstore mock added");
|
||||
try {
|
||||
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 {
|
||||
const element = document.getElementById(elementId);
|
||||
@ -336,15 +320,25 @@ class Services {
|
||||
element?.addEventListener("submit", callback);
|
||||
}
|
||||
|
||||
public injectHtml(html: string) {
|
||||
console.log("JS html : "+html);
|
||||
public async injectHtml(processName: string) {
|
||||
// console.log("JS html : "+html);
|
||||
const container = document.getElementById('containerId');
|
||||
|
||||
if (!container) {
|
||||
console.error("No html container");
|
||||
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> {
|
||||
|
@ -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