first commit
This commit is contained in:
45
components/Cargo.toml
Normal file
45
components/Cargo.toml
Normal file
@@ -0,0 +1,45 @@
|
||||
[package]
|
||||
name = "self-components"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
yew = { workspace = true }
|
||||
wasm-bindgen = { workspace = true }
|
||||
wasm-bindgen-futures = { workspace = true }
|
||||
web-sys = { workspace = true, features = [
|
||||
"console",
|
||||
"HtmlInputElement",
|
||||
"HtmlTextAreaElement",
|
||||
"Event",
|
||||
"EventTarget",
|
||||
"InputEvent",
|
||||
"MouseEvent",
|
||||
"Window",
|
||||
"Document",
|
||||
"Element",
|
||||
"EventSource",
|
||||
"MessageEvent",
|
||||
"Clipboard",
|
||||
"Navigator",
|
||||
"Crypto",
|
||||
"CryptoKey",
|
||||
"SubtleCrypto",
|
||||
"AesKeyGenParams",
|
||||
"CryptoKeyPair",
|
||||
] }
|
||||
js-sys = { workspace = true }
|
||||
gloo = { workspace = true }
|
||||
gloo-timers = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
getrandom = { workspace = true }
|
||||
sha2 = { workspace = true }
|
||||
aes-gcm = { workspace = true }
|
||||
base64 = { workspace = true }
|
||||
hex = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
k256 = { version = "0.13", features = ["ecdsa", "sha256"] }
|
158
components/src/crypto.rs
Normal file
158
components/src/crypto.rs
Normal file
@@ -0,0 +1,158 @@
|
||||
use wasm_bindgen::prelude::*;
|
||||
use web_sys::console;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Sha256, Digest};
|
||||
use aes_gcm::{Aes256Gcm, Key, Nonce, aead::{Aead, KeyInit}};
|
||||
use rand::RngCore;
|
||||
use base64::{Engine as _, engine::general_purpose};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct KeyPair {
|
||||
pub private_key: String,
|
||||
pub public_key: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct EncryptedPrivateKey {
|
||||
pub encrypted_data: String,
|
||||
pub nonce: String,
|
||||
pub salt: String,
|
||||
}
|
||||
|
||||
/// Generate a new secp256k1 key pair
|
||||
pub fn generate_keypair() -> Result<KeyPair, String> {
|
||||
// Generate 32 random bytes for private key
|
||||
let mut private_key_bytes = [0u8; 32];
|
||||
getrandom::getrandom(&mut private_key_bytes)
|
||||
.map_err(|e| format!("Failed to generate random bytes: {:?}", e))?;
|
||||
|
||||
// Ensure private key is valid (not zero, not greater than curve order)
|
||||
// For simplicity, we'll just ensure it's not all zeros
|
||||
if private_key_bytes.iter().all(|&b| b == 0) {
|
||||
return Err("Generated invalid private key".to_string());
|
||||
}
|
||||
|
||||
let private_key = hex::encode(private_key_bytes);
|
||||
|
||||
// Generate public key from private key (simplified secp256k1 derivation)
|
||||
let public_key = derive_public_key(&private_key)?;
|
||||
|
||||
Ok(KeyPair {
|
||||
private_key,
|
||||
public_key,
|
||||
})
|
||||
}
|
||||
|
||||
/// Derive public key from private key (simplified implementation)
|
||||
fn derive_public_key(private_key: &str) -> Result<String, String> {
|
||||
let private_bytes = hex::decode(private_key)
|
||||
.map_err(|e| format!("Invalid private key hex: {:?}", e))?;
|
||||
|
||||
// Simple hash-based public key derivation (not actual secp256k1)
|
||||
// In production, use proper secp256k1 point multiplication
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(&private_bytes);
|
||||
hasher.update(b"secp256k1_public_key");
|
||||
let hash = hasher.finalize();
|
||||
|
||||
// Add uncompressed public key prefix
|
||||
let mut public_key = vec![0x04];
|
||||
public_key.extend_from_slice(&hash);
|
||||
// Add another hash to make it 65 bytes total (uncompressed public key size)
|
||||
let mut hasher2 = Sha256::new();
|
||||
hasher2.update(&hash);
|
||||
let hash2 = hasher2.finalize();
|
||||
public_key.extend_from_slice(&hash2);
|
||||
|
||||
Ok(hex::encode(public_key))
|
||||
}
|
||||
|
||||
/// Encrypt private key with password using AES-256-GCM
|
||||
pub fn encrypt_private_key(private_key: &str, password: &str) -> Result<EncryptedPrivateKey, String> {
|
||||
// Generate random salt
|
||||
let mut salt = [0u8; 32];
|
||||
getrandom::getrandom(&mut salt)
|
||||
.map_err(|e| format!("Failed to generate salt: {:?}", e))?;
|
||||
|
||||
// Derive key from password using PBKDF2-like approach (simplified)
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(password.as_bytes());
|
||||
hasher.update(&salt);
|
||||
// Multiple rounds for key stretching
|
||||
let mut key_material = hasher.finalize().to_vec();
|
||||
for _ in 0..10000 {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(&key_material);
|
||||
hasher.update(&salt);
|
||||
key_material = hasher.finalize().to_vec();
|
||||
}
|
||||
|
||||
let key = Key::<Aes256Gcm>::from_slice(&key_material);
|
||||
let cipher = Aes256Gcm::new(key);
|
||||
|
||||
// Generate random nonce
|
||||
let mut nonce_bytes = [0u8; 12];
|
||||
getrandom::getrandom(&mut nonce_bytes)
|
||||
.map_err(|e| format!("Failed to generate nonce: {:?}", e))?;
|
||||
let nonce = Nonce::from_slice(&nonce_bytes);
|
||||
|
||||
// Encrypt private key
|
||||
let ciphertext = cipher.encrypt(nonce, private_key.as_bytes())
|
||||
.map_err(|e| format!("Encryption failed: {:?}", e))?;
|
||||
|
||||
Ok(EncryptedPrivateKey {
|
||||
encrypted_data: general_purpose::STANDARD.encode(&ciphertext),
|
||||
nonce: general_purpose::STANDARD.encode(&nonce_bytes),
|
||||
salt: general_purpose::STANDARD.encode(&salt),
|
||||
})
|
||||
}
|
||||
|
||||
/// Decrypt private key with password
|
||||
pub fn decrypt_private_key(encrypted: &EncryptedPrivateKey, password: &str) -> Result<String, String> {
|
||||
let salt = general_purpose::STANDARD.decode(&encrypted.salt)
|
||||
.map_err(|e| format!("Invalid salt base64: {:?}", e))?;
|
||||
let nonce_bytes = general_purpose::STANDARD.decode(&encrypted.nonce)
|
||||
.map_err(|e| format!("Invalid nonce base64: {:?}", e))?;
|
||||
let ciphertext = general_purpose::STANDARD.decode(&encrypted.encrypted_data)
|
||||
.map_err(|e| format!("Invalid encrypted data base64: {:?}", e))?;
|
||||
|
||||
// Derive key from password (same as encryption)
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(password.as_bytes());
|
||||
hasher.update(&salt);
|
||||
let mut key_material = hasher.finalize().to_vec();
|
||||
for _ in 0..10000 {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(&key_material);
|
||||
hasher.update(&salt);
|
||||
key_material = hasher.finalize().to_vec();
|
||||
}
|
||||
|
||||
let key = Key::<Aes256Gcm>::from_slice(&key_material);
|
||||
let cipher = Aes256Gcm::new(key);
|
||||
let nonce = Nonce::from_slice(&nonce_bytes);
|
||||
|
||||
// Decrypt
|
||||
let plaintext = cipher.decrypt(nonce, ciphertext.as_ref())
|
||||
.map_err(|e| format!("Decryption failed: {:?}", e))?;
|
||||
|
||||
String::from_utf8(plaintext)
|
||||
.map_err(|e| format!("Invalid UTF-8 in decrypted data: {:?}", e))
|
||||
}
|
||||
|
||||
/// Copy text to clipboard
|
||||
pub async fn copy_to_clipboard(text: &str) -> Result<(), String> {
|
||||
let window = web_sys::window().ok_or("No window object")?;
|
||||
let navigator = window.navigator();
|
||||
|
||||
let clipboard = navigator.clipboard();
|
||||
let promise = clipboard.write_text(text);
|
||||
wasm_bindgen_futures::JsFuture::from(promise)
|
||||
.await
|
||||
.map_err(|_| {
|
||||
// Fallback: show alert with text to copy manually
|
||||
let _ = window.alert_with_message(&format!("Copy this text: {}", text));
|
||||
"Failed to copy to clipboard".to_string()
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
5
components/src/lib.rs
Normal file
5
components/src/lib.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
pub mod registration;
|
||||
pub mod crypto;
|
||||
|
||||
pub use registration::{Registration, RegistrationConfig};
|
||||
pub use crypto::*;
|
780
components/src/registration.rs
Normal file
780
components/src/registration.rs
Normal file
@@ -0,0 +1,780 @@
|
||||
use yew::prelude::*;
|
||||
use web_sys::{HtmlInputElement, EventSource, MessageEvent};
|
||||
use wasm_bindgen::prelude::*;
|
||||
use wasm_bindgen::JsCast;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use gloo_timers::callback::Timeout;
|
||||
use crate::crypto::{generate_keypair, encrypt_private_key, copy_to_clipboard, KeyPair, EncryptedPrivateKey};
|
||||
use k256::{SecretKey, PublicKey};
|
||||
use k256::elliptic_curve::sec1::ToEncodedPoint;
|
||||
use rand::rngs::OsRng;
|
||||
use hex;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct RegistrationConfig {
|
||||
pub server_url: String,
|
||||
pub app_name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum EmailVerificationStatus {
|
||||
NotStarted,
|
||||
Pending,
|
||||
Verified,
|
||||
Failed,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum RegistrationStep {
|
||||
Identity,
|
||||
EmailVerification,
|
||||
KeyGeneration,
|
||||
KeyConfirmation,
|
||||
Complete,
|
||||
}
|
||||
|
||||
#[derive(Properties, PartialEq)]
|
||||
pub struct RegistrationProps {
|
||||
pub config: RegistrationConfig,
|
||||
pub on_complete: Callback<(String, String)>, // (email, public_key)
|
||||
}
|
||||
|
||||
pub struct Registration {
|
||||
// Form data
|
||||
name: String,
|
||||
email: String,
|
||||
|
||||
// Key management
|
||||
keypair: Option<KeyPair>,
|
||||
secret_phrase: String,
|
||||
generated_private_key: Option<String>,
|
||||
generated_public_key: Option<String>,
|
||||
private_key_input: String,
|
||||
key_copied: bool,
|
||||
key_confirmation: String,
|
||||
|
||||
// Email verification
|
||||
email_status: EmailVerificationStatus,
|
||||
event_source: Option<EventSource>,
|
||||
|
||||
// UI state
|
||||
current_step: RegistrationStep,
|
||||
show_private_key: bool,
|
||||
errors: Vec<String>,
|
||||
processing: bool,
|
||||
}
|
||||
|
||||
pub enum RegistrationMsg {
|
||||
UpdateName(String),
|
||||
UpdateEmail(String),
|
||||
UpdateSecretPhrase(String),
|
||||
UpdatePrivateKeyInput(String),
|
||||
|
||||
SendEmailVerification,
|
||||
EmailVerified,
|
||||
EmailVerificationFailed,
|
||||
|
||||
GenerateKeys,
|
||||
UpdateKeyConfirmation(String),
|
||||
TogglePrivateKeyVisibility,
|
||||
CopyPrivateKey,
|
||||
|
||||
NextStep,
|
||||
SubmitRegistration,
|
||||
RegistrationComplete,
|
||||
RegistrationFailed(String),
|
||||
|
||||
ClearErrors,
|
||||
}
|
||||
|
||||
impl Component for Registration {
|
||||
type Message = RegistrationMsg;
|
||||
type Properties = RegistrationProps;
|
||||
|
||||
fn create(_ctx: &Context<Self>) -> Self {
|
||||
Self {
|
||||
name: String::new(),
|
||||
email: String::new(),
|
||||
keypair: None,
|
||||
secret_phrase: String::new(),
|
||||
generated_private_key: None,
|
||||
generated_public_key: None,
|
||||
private_key_input: String::new(),
|
||||
key_copied: false,
|
||||
key_confirmation: String::new(),
|
||||
email_status: EmailVerificationStatus::NotStarted,
|
||||
event_source: None,
|
||||
current_step: RegistrationStep::Identity,
|
||||
show_private_key: false,
|
||||
errors: Vec::new(),
|
||||
processing: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
|
||||
match msg {
|
||||
RegistrationMsg::UpdateName(name) => {
|
||||
self.name = name;
|
||||
true
|
||||
}
|
||||
RegistrationMsg::UpdateEmail(email) => {
|
||||
self.email = email;
|
||||
if self.email_status == EmailVerificationStatus::Verified {
|
||||
self.email_status = EmailVerificationStatus::NotStarted;
|
||||
}
|
||||
true
|
||||
}
|
||||
RegistrationMsg::UpdateSecretPhrase(secret) => {
|
||||
self.secret_phrase = secret;
|
||||
true
|
||||
}
|
||||
RegistrationMsg::UpdatePrivateKeyInput(value) => {
|
||||
self.private_key_input = value;
|
||||
true
|
||||
}
|
||||
RegistrationMsg::UpdateKeyConfirmation(value) => {
|
||||
self.key_confirmation = value;
|
||||
true
|
||||
}
|
||||
RegistrationMsg::SendEmailVerification => {
|
||||
self.send_email_verification(ctx);
|
||||
true
|
||||
}
|
||||
RegistrationMsg::EmailVerified => {
|
||||
self.email_status = EmailVerificationStatus::Verified;
|
||||
if let Some(event_source) = &self.event_source {
|
||||
event_source.close();
|
||||
self.event_source = None;
|
||||
}
|
||||
true
|
||||
}
|
||||
RegistrationMsg::EmailVerificationFailed => {
|
||||
self.email_status = EmailVerificationStatus::Failed;
|
||||
if let Some(event_source) = &self.event_source {
|
||||
event_source.close();
|
||||
self.event_source = None;
|
||||
}
|
||||
true
|
||||
}
|
||||
RegistrationMsg::GenerateKeys => {
|
||||
self.generate_secp256k1_keys();
|
||||
true
|
||||
}
|
||||
RegistrationMsg::CopyPrivateKey => {
|
||||
self.copy_private_key();
|
||||
false
|
||||
}
|
||||
RegistrationMsg::TogglePrivateKeyVisibility => {
|
||||
self.show_private_key = !self.show_private_key;
|
||||
true
|
||||
}
|
||||
RegistrationMsg::NextStep => {
|
||||
// No longer needed - single step form
|
||||
true
|
||||
}
|
||||
RegistrationMsg::SubmitRegistration => {
|
||||
if self.validate_complete_form() {
|
||||
self.submit_registration(ctx);
|
||||
}
|
||||
true
|
||||
}
|
||||
RegistrationMsg::RegistrationComplete => {
|
||||
self.current_step = RegistrationStep::Complete;
|
||||
if let Some(keypair) = &self.keypair {
|
||||
ctx.props().on_complete.emit((self.email.clone(), keypair.public_key.clone()));
|
||||
}
|
||||
true
|
||||
}
|
||||
RegistrationMsg::RegistrationFailed(error) => {
|
||||
self.processing = false;
|
||||
self.errors.push(error);
|
||||
true
|
||||
}
|
||||
RegistrationMsg::ClearErrors => {
|
||||
self.errors.clear();
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
if self.current_step == RegistrationStep::Complete {
|
||||
return self.render_complete_step();
|
||||
}
|
||||
|
||||
html! {
|
||||
<div class="registration-container" style="max-width: 600px; margin: 0 auto; padding: 2rem;">
|
||||
<div class="card shadow-lg border-0" style="border-radius: 16px;">
|
||||
<div class="card-header bg-primary text-white text-center" style="border-radius: 16px 16px 0 0; padding: 2rem;">
|
||||
<h2 class="mb-0">{"Self-Sovereign Identity"}</h2>
|
||||
<p class="mb-0 mt-2 opacity-75">{"Create your decentralized identity"}</p>
|
||||
</div>
|
||||
|
||||
<div class="card-body" style="padding: 2rem;">
|
||||
{self.render_errors(ctx)}
|
||||
{self.render_identity_step(ctx)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Registration {
|
||||
fn render_errors(&self, ctx: &Context<Self>) -> Html {
|
||||
if self.errors.is_empty() {
|
||||
return html! {};
|
||||
}
|
||||
|
||||
let link = ctx.link();
|
||||
html! {
|
||||
<div class="alert alert-danger alert-dismissible fade show mb-4">
|
||||
<ul class="mb-0">
|
||||
{for self.errors.iter().map(|error| html! {
|
||||
<li>{error}</li>
|
||||
})}
|
||||
</ul>
|
||||
<button type="button" class="btn-close"
|
||||
onclick={link.callback(|_| RegistrationMsg::ClearErrors)}></button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
fn render_status_notification(&self) -> Html {
|
||||
let (alert_class, icon, message) = if self.processing {
|
||||
("alert-info", "bi-hourglass-split", "Processing registration...")
|
||||
} else if self.name.trim().is_empty() {
|
||||
("alert-secondary", "bi-person", "Please enter your full name to get started")
|
||||
} else if self.email.trim().is_empty() {
|
||||
("alert-secondary", "bi-envelope", "Please enter your email address")
|
||||
} else if self.email_status == EmailVerificationStatus::NotStarted {
|
||||
("alert-warning", "bi-envelope-exclamation", "Please verify your email address")
|
||||
} else if self.email_status == EmailVerificationStatus::Pending {
|
||||
("alert-info", "bi-envelope-check", "Check your email and click the verification link")
|
||||
} else if self.email_status == EmailVerificationStatus::Failed {
|
||||
("alert-danger", "bi-envelope-x", "Email verification failed. Please try again")
|
||||
} else if self.secret_phrase.trim().is_empty() {
|
||||
("alert-warning", "bi-key", "Please enter a secret phrase to generate your keys")
|
||||
} else if self.generated_private_key.is_none() {
|
||||
("alert-warning", "bi-key-fill", "Please generate your cryptographic keys")
|
||||
} else if self.key_confirmation.is_empty() {
|
||||
("alert-warning", "bi-shield-exclamation", "Please confirm your private key to complete registration. Save it securely - it cannot be recovered if lost!")
|
||||
} else if self.generated_private_key.as_ref() != Some(&self.key_confirmation) {
|
||||
("alert-danger", "bi-shield-x", "Private key confirmation does not match. Please try again")
|
||||
} else {
|
||||
("alert-success", "bi-shield-check", "All requirements completed! Ready to register")
|
||||
};
|
||||
|
||||
html! {
|
||||
<div class={format!("alert {} mb-3", alert_class)}>
|
||||
<i class={format!("bi {} me-2", icon)}></i>
|
||||
{message}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
fn render_identity_step(&self, ctx: &Context<Self>) -> Html {
|
||||
let link = ctx.link();
|
||||
|
||||
html! {
|
||||
<div class="step-content">
|
||||
<h4 class="mb-4">{"Personal Information"}</h4>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">{"Full Name"}</label>
|
||||
<input type="text" class="form-control"
|
||||
value={self.name.clone()}
|
||||
oninput={link.callback(|e: InputEvent| {
|
||||
let input: HtmlInputElement = e.target_unchecked_into();
|
||||
RegistrationMsg::UpdateName(input.value())
|
||||
})}
|
||||
placeholder="Enter your full name" />
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="form-label">{"Email Address"}</label>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<input type="email" class="form-control"
|
||||
value={self.email.clone()}
|
||||
oninput={link.callback(|e: InputEvent| {
|
||||
let input: HtmlInputElement = e.target_unchecked_into();
|
||||
RegistrationMsg::UpdateEmail(input.value())
|
||||
})}
|
||||
placeholder="your.email@example.com" />
|
||||
|
||||
{match self.email_status {
|
||||
EmailVerificationStatus::NotStarted => html! {
|
||||
<button type="button" class="btn btn-outline-primary"
|
||||
onclick={link.callback(|_| RegistrationMsg::SendEmailVerification)}
|
||||
disabled={self.email.trim().is_empty()}>
|
||||
{"Verify"}
|
||||
</button>
|
||||
},
|
||||
EmailVerificationStatus::Pending => html! {
|
||||
<div class="text-warning d-flex align-items-center">
|
||||
<i class="bi bi-clock-history me-1"></i>
|
||||
{"Verifying..."}
|
||||
</div>
|
||||
},
|
||||
EmailVerificationStatus::Verified => html! {
|
||||
<div class="text-success d-flex align-items-center">
|
||||
<i class="bi bi-check-circle-fill me-1"></i>
|
||||
{"Verified"}
|
||||
</div>
|
||||
},
|
||||
EmailVerificationStatus::Failed => html! {
|
||||
<button type="button" class="btn btn-outline-danger"
|
||||
onclick={link.callback(|_| RegistrationMsg::SendEmailVerification)}>
|
||||
{"Retry"}
|
||||
</button>
|
||||
},
|
||||
}}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="form-label">{"Secret Phrase for Key Generation"}</label>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<input type="password" class="form-control"
|
||||
value={self.secret_phrase.clone()}
|
||||
oninput={link.callback(|e: InputEvent| {
|
||||
let input: HtmlInputElement = e.target_unchecked_into();
|
||||
RegistrationMsg::UpdateSecretPhrase(input.value())
|
||||
})}
|
||||
placeholder="Enter a secret phrase to generate your keys" />
|
||||
|
||||
<button type="button" class="btn btn-outline-primary"
|
||||
onclick={link.callback(|_| RegistrationMsg::GenerateKeys)}
|
||||
disabled={self.secret_phrase.trim().is_empty()}>
|
||||
<i class="bi bi-key me-1"></i>
|
||||
{"Generate"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<label class="form-label mb-0">{"Generated Private Key"}</label>
|
||||
<div>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary me-2"
|
||||
onclick={link.callback(|_| RegistrationMsg::TogglePrivateKeyVisibility)}
|
||||
disabled={self.generated_private_key.is_none()}>
|
||||
{if self.show_private_key { "Hide" } else { "Show" }}
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-primary"
|
||||
onclick={link.callback(|_| RegistrationMsg::CopyPrivateKey)}
|
||||
disabled={self.generated_private_key.is_none()}>
|
||||
<i class="bi bi-copy me-1"></i>
|
||||
{"Copy"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-dark text-light p-3 rounded font-monospace small"
|
||||
style="word-break: break-all;">
|
||||
{if let Some(private_key) = &self.generated_private_key {
|
||||
if self.show_private_key {
|
||||
private_key.clone()
|
||||
} else {
|
||||
"••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••".to_string()
|
||||
}
|
||||
} else {
|
||||
"Private key will appear here after generation".to_string()
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{if let Some(_) = &self.generated_private_key {
|
||||
html! {
|
||||
<div class="mb-4">
|
||||
<label class="form-label">{"Confirm Private Key"}</label>
|
||||
<input type="password" class="form-control"
|
||||
value={self.key_confirmation.clone()}
|
||||
oninput={link.callback(|e: InputEvent| {
|
||||
let input: HtmlInputElement = e.target_unchecked_into();
|
||||
RegistrationMsg::UpdateKeyConfirmation(input.value())
|
||||
})}
|
||||
placeholder="Enter your private key to confirm" />
|
||||
|
||||
</div>
|
||||
}
|
||||
} else {
|
||||
html! {}
|
||||
}}
|
||||
|
||||
{self.render_status_notification()}
|
||||
|
||||
<div class="d-grid">
|
||||
<button type="button" class="btn btn-primary btn-lg"
|
||||
onclick={link.callback(|_| RegistrationMsg::SubmitRegistration)}
|
||||
disabled={!self.validate_complete_form()}>
|
||||
{if self.processing {
|
||||
html! {
|
||||
<>
|
||||
<span class="spinner-border spinner-border-sm me-2"></span>
|
||||
{"Registering..."}
|
||||
</>
|
||||
}
|
||||
} else {
|
||||
html! { "Complete Registration" }
|
||||
}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
fn render_email_verification_step(&self, ctx: &Context<Self>) -> Html {
|
||||
let link = ctx.link();
|
||||
|
||||
html! {
|
||||
<div class="step-content">
|
||||
<h4 class="mb-4">{"Email Verification"}</h4>
|
||||
|
||||
<div class="mb-4">
|
||||
<div class="d-flex align-items-center">
|
||||
<input type="email" class="form-control me-3"
|
||||
value={self.email.clone()} readonly=true />
|
||||
{match self.email_status {
|
||||
EmailVerificationStatus::NotStarted => html! {
|
||||
<button type="button" class="btn btn-outline-primary"
|
||||
onclick={link.callback(|_| RegistrationMsg::SendEmailVerification)}>
|
||||
{"Send Verification"}
|
||||
</button>
|
||||
},
|
||||
EmailVerificationStatus::Pending => html! {
|
||||
<div class="text-warning">
|
||||
<i class="bi bi-clock-history me-2"></i>
|
||||
{"Waiting..."}
|
||||
</div>
|
||||
},
|
||||
EmailVerificationStatus::Verified => html! {
|
||||
<div class="text-success">
|
||||
<i class="bi bi-check-circle-fill me-2"></i>
|
||||
{"Verified"}
|
||||
</div>
|
||||
},
|
||||
EmailVerificationStatus::Failed => html! {
|
||||
<div class="text-danger">
|
||||
<i class="bi bi-x-circle-fill me-2"></i>
|
||||
{"Failed"}
|
||||
</div>
|
||||
},
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="d-grid">
|
||||
<button type="button" class="btn btn-primary"
|
||||
onclick={link.callback(|_| RegistrationMsg::NextStep)}
|
||||
disabled={self.email_status != EmailVerificationStatus::Verified}>
|
||||
{"Continue"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
fn render_key_generation_step(&self, ctx: &Context<Self>) -> Html {
|
||||
let link = ctx.link();
|
||||
|
||||
html! {
|
||||
<div class="step-content">
|
||||
<h4 class="mb-4">{"Generate Secp256k1 Keys"}</h4>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">{"Secret Phrase"}</label>
|
||||
<input type="password" class="form-control"
|
||||
value={self.secret_phrase.clone()}
|
||||
oninput={link.callback(|e: InputEvent| {
|
||||
let input: HtmlInputElement = e.target_unchecked_into();
|
||||
RegistrationMsg::UpdateSecretPhrase(input.value())
|
||||
})}
|
||||
placeholder="Enter a secret phrase to generate your keys" />
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<button type="button" class="btn btn-primary"
|
||||
onclick={link.callback(|_| RegistrationMsg::GenerateKeys)}
|
||||
disabled={self.secret_phrase.trim().is_empty()}>
|
||||
<i class="bi bi-key me-2"></i>
|
||||
{"Generate Keys"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{if let Some(private_key) = &self.generated_private_key {
|
||||
html! {
|
||||
<div class="mb-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<label class="form-label mb-0">{"Generated Private Key"}</label>
|
||||
<div>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary me-2"
|
||||
onclick={link.callback(|_| RegistrationMsg::TogglePrivateKeyVisibility)}>
|
||||
{if self.show_private_key { "Hide" } else { "Show" }}
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-primary"
|
||||
onclick={link.callback(|_| RegistrationMsg::CopyPrivateKey)}>
|
||||
<i class="bi bi-copy me-1"></i>
|
||||
{"Copy"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-dark text-light p-3 rounded font-monospace small"
|
||||
style="word-break: break-all;">
|
||||
{if self.show_private_key {
|
||||
private_key.clone()
|
||||
} else {
|
||||
"••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••".to_string()
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
} else {
|
||||
html! {}
|
||||
}}
|
||||
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
fn render_key_confirmation_step(&self, ctx: &Context<Self>) -> Html {
|
||||
let link = ctx.link();
|
||||
|
||||
html! {
|
||||
<div class="step-content">
|
||||
<h4 class="mb-4">{"Confirm Private Key"}</h4>
|
||||
|
||||
<p class="mb-4">{"Please enter your private key to confirm you have saved it securely:"}</p>
|
||||
|
||||
<div class="mb-4">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<input type="password" class="form-control font-monospace"
|
||||
value={self.private_key_input.clone()}
|
||||
oninput={link.callback(|e: InputEvent| {
|
||||
let input: HtmlInputElement = e.target_unchecked_into();
|
||||
RegistrationMsg::UpdatePrivateKeyInput(input.value())
|
||||
})}
|
||||
placeholder="Enter your private key..." />
|
||||
|
||||
{if !self.private_key_input.is_empty() {
|
||||
let is_correct = self.generated_private_key.as_ref()
|
||||
.map(|pk| pk == &self.private_key_input.trim())
|
||||
.unwrap_or(false);
|
||||
|
||||
if is_correct {
|
||||
html! {
|
||||
<button type="button" class="btn btn-success"
|
||||
onclick={link.callback(|_| RegistrationMsg::SubmitRegistration)}
|
||||
disabled={self.processing}>
|
||||
{if self.processing {
|
||||
html! {
|
||||
<>
|
||||
<span class="spinner-border spinner-border-sm me-2"></span>
|
||||
{"Registering..."}
|
||||
</>
|
||||
}
|
||||
} else {
|
||||
html! { "Register" }
|
||||
}}
|
||||
</button>
|
||||
}
|
||||
} else {
|
||||
html! {
|
||||
<button type="button" class="btn btn-outline-danger" disabled=true>
|
||||
{"Invalid Key"}
|
||||
</button>
|
||||
}
|
||||
}
|
||||
} else {
|
||||
html! {
|
||||
<button type="button" class="btn btn-outline-secondary" disabled=true>
|
||||
{"Register"}
|
||||
</button>
|
||||
}
|
||||
}}
|
||||
</div>
|
||||
|
||||
{if !self.private_key_input.is_empty() {
|
||||
let is_correct = self.generated_private_key.as_ref()
|
||||
.map(|pk| pk == &self.private_key_input.trim())
|
||||
.unwrap_or(false);
|
||||
|
||||
if is_correct {
|
||||
html! {
|
||||
<div class="alert alert-success mt-2">
|
||||
<i class="bi bi-check-circle me-2"></i>
|
||||
{"Private key confirmed successfully!"}
|
||||
</div>
|
||||
}
|
||||
} else {
|
||||
html! {
|
||||
<div class="alert alert-danger mt-2">
|
||||
<i class="bi bi-x-circle me-2"></i>
|
||||
{"Private key does not match. Please try again."}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
} else {
|
||||
html! {}
|
||||
}}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
fn render_complete_step(&self) -> Html {
|
||||
html! {
|
||||
<div class="step-content text-center">
|
||||
<div class="mb-4">
|
||||
<i class="bi bi-check-circle-fill text-success" style="font-size: 4rem;"></i>
|
||||
</div>
|
||||
<h4 class="mb-3">{"Registration Complete!"}</h4>
|
||||
<p class="text-muted">{"Your self-sovereign identity has been created successfully."}</p>
|
||||
<div class="alert alert-info">
|
||||
<strong>{"Email:"}</strong> {&self.email}<br/>
|
||||
<strong>{"Public Key:"}</strong>
|
||||
<code class="d-block mt-2 small text-break">
|
||||
{self.generated_public_key.as_ref().unwrap_or(&String::new())}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_complete_form(&self) -> bool {
|
||||
!self.name.trim().is_empty() &&
|
||||
!self.email.trim().is_empty() &&
|
||||
self.email_status == EmailVerificationStatus::Verified &&
|
||||
self.generated_private_key.is_some() &&
|
||||
if let Some(private_key) = &self.generated_private_key {
|
||||
self.key_confirmation == *private_key
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn send_email_verification(&mut self, ctx: &Context<Self>) {
|
||||
self.email_status = EmailVerificationStatus::Pending;
|
||||
|
||||
let server_url = ctx.props().config.server_url.clone();
|
||||
let email = self.email.clone();
|
||||
let link = ctx.link().clone();
|
||||
// Send verification request
|
||||
wasm_bindgen_futures::spawn_local(async move {
|
||||
let url = format!("{}/api/send-verification", server_url);
|
||||
let body = serde_json::json!({
|
||||
"email": email
|
||||
});
|
||||
|
||||
// Make HTTP request to server
|
||||
let mut opts = web_sys::RequestInit::new();
|
||||
opts.method("POST");
|
||||
opts.mode(web_sys::RequestMode::Cors);
|
||||
|
||||
let headers = web_sys::Headers::new().unwrap();
|
||||
headers.set("Content-Type", "application/json").unwrap();
|
||||
opts.headers(&headers);
|
||||
|
||||
opts.body(Some(&wasm_bindgen::JsValue::from_str(&body.to_string())));
|
||||
|
||||
let request = web_sys::Request::new_with_str_and_init(&url, &opts).unwrap();
|
||||
|
||||
let window = web_sys::window().unwrap();
|
||||
match wasm_bindgen_futures::JsFuture::from(window.fetch_with_request(&request)).await {
|
||||
Ok(_response) => {
|
||||
web_sys::console::log_1(&format!("Email verification sent to: {}", email).into());
|
||||
}
|
||||
Err(e) => {
|
||||
web_sys::console::error_1(&format!("Failed to send verification: {:?}", e).into());
|
||||
}
|
||||
}
|
||||
|
||||
// Set up SSE connection for verification status
|
||||
let sse_url = format!("{}/api/verification-status/{}", server_url, email);
|
||||
|
||||
match EventSource::new(&sse_url) {
|
||||
Ok(event_source) => {
|
||||
let link_clone = link.clone();
|
||||
let onmessage_callback = Closure::wrap(Box::new(move |e: MessageEvent| {
|
||||
if let Some(data) = e.data().as_string() {
|
||||
if data == "verified" {
|
||||
link_clone.send_message(RegistrationMsg::EmailVerified);
|
||||
} else if data == "failed" {
|
||||
link_clone.send_message(RegistrationMsg::EmailVerificationFailed);
|
||||
}
|
||||
}
|
||||
}) as Box<dyn FnMut(_)>);
|
||||
|
||||
event_source.set_onmessage(Some(onmessage_callback.as_ref().unchecked_ref()));
|
||||
onmessage_callback.forget();
|
||||
}
|
||||
Err(_) => {
|
||||
link.send_message(RegistrationMsg::EmailVerificationFailed);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn generate_secp256k1_keys(&mut self) {
|
||||
use sha2::{Sha256, Digest};
|
||||
|
||||
// Use secret phrase to derive private key deterministically
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(self.secret_phrase.as_bytes());
|
||||
let hash = hasher.finalize();
|
||||
|
||||
// Generate secp256k1 keypair from hash
|
||||
match SecretKey::from_slice(&hash) {
|
||||
Ok(secret_key) => {
|
||||
let public_key = secret_key.public_key();
|
||||
|
||||
// Store keys as hex strings
|
||||
self.generated_private_key = Some(hex::encode(secret_key.to_bytes()));
|
||||
self.generated_public_key = Some(hex::encode(public_key.to_encoded_point(false).as_bytes()));
|
||||
}
|
||||
Err(_) => {
|
||||
self.errors.push("Failed to generate valid private key from secret phrase".to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn copy_private_key(&mut self) {
|
||||
if let Some(private_key) = &self.generated_private_key {
|
||||
// Use web API to copy to clipboard
|
||||
if let Some(window) = web_sys::window() {
|
||||
let navigator = window.navigator().clipboard();
|
||||
let _ = navigator.write_text(private_key);
|
||||
self.key_copied = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn submit_registration(&mut self, ctx: &Context<Self>) {
|
||||
self.processing = true;
|
||||
|
||||
let server_url = ctx.props().config.server_url.clone();
|
||||
let email = self.email.clone();
|
||||
let public_key = self.generated_public_key.as_ref().unwrap().clone();
|
||||
let link = ctx.link().clone();
|
||||
|
||||
wasm_bindgen_futures::spawn_local(async move {
|
||||
let _url = format!("{}/api/register", server_url);
|
||||
let _body = serde_json::json!({
|
||||
"email": email,
|
||||
"public_key": public_key
|
||||
});
|
||||
|
||||
// In a real implementation, make HTTP request here
|
||||
web_sys::console::log_1(&format!("Registering: {} with key: {}", email, public_key).into());
|
||||
|
||||
// Simulate API call
|
||||
let timeout = Timeout::new(2000, move || {
|
||||
link.send_message(RegistrationMsg::RegistrationComplete);
|
||||
});
|
||||
timeout.forget();
|
||||
});
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user