use std::fmt;
use candid::{CandidType, Principal};
use serde::Deserialize;
use serde_bytes::ByteBuf;
use simple_asn1::ASN1EncodeErr;
use crate::{
delegation::{
create_delegation, create_delegation_hash, create_user_canister_pubkey, generate_seed,
DelegationError,
},
eth::{recover_eth_address, EthAddress, EthError, EthSignature},
hash,
rand::generate_nonce,
settings::Settings,
signature_map::SignatureMap,
siwe::{SiweMessage, SiweMessageError},
time::get_current_time,
with_settings, SIWE_MESSAGES,
};
const MAX_SIGS_TO_PRUNE: usize = 10;
pub fn prepare_login(address: &EthAddress) -> Result<(SiweMessage, String), EthError> {
let nonce = generate_nonce();
let message = SiweMessage::new(address, &nonce);
SIWE_MESSAGES.with_borrow_mut(|siwe_messages| {
siwe_messages.insert(message.clone(), address, &nonce);
});
Ok((message, nonce))
}
#[derive(Clone, Debug, CandidType, Deserialize)]
pub struct LoginDetails {
pub expiration: u64,
pub user_canister_pubkey: ByteBuf,
}
pub enum LoginError {
EthError(EthError),
SiweMessageError(SiweMessageError),
AddressMismatch,
DelegationError(DelegationError),
ASN1EncodeErr(ASN1EncodeErr),
}
impl From<EthError> for LoginError {
fn from(err: EthError) -> Self {
LoginError::EthError(err)
}
}
impl From<SiweMessageError> for LoginError {
fn from(err: SiweMessageError) -> Self {
LoginError::SiweMessageError(err)
}
}
impl From<DelegationError> for LoginError {
fn from(err: DelegationError) -> Self {
LoginError::DelegationError(err)
}
}
impl From<ASN1EncodeErr> for LoginError {
fn from(err: ASN1EncodeErr) -> Self {
LoginError::ASN1EncodeErr(err)
}
}
impl fmt::Display for LoginError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
LoginError::EthError(e) => write!(f, "{}", e),
LoginError::SiweMessageError(e) => write!(f, "{}", e),
LoginError::AddressMismatch => write!(f, "Recovered address does not match"),
LoginError::DelegationError(e) => write!(f, "{}", e),
LoginError::ASN1EncodeErr(e) => write!(f, "{}", e),
}
}
}
pub fn login(
signature: &EthSignature,
address: &EthAddress,
session_key: ByteBuf,
signature_map: &mut SignatureMap,
canister_id: &Principal,
nonce: &str,
) -> Result<LoginDetails, LoginError> {
SIWE_MESSAGES.with_borrow_mut(|siwe_messages| {
siwe_messages.prune_expired();
let message = siwe_messages.get(address, nonce)?;
let message_string: String = message.clone().into();
let result = match recover_eth_address(&message_string, signature) {
Ok(recovered_address) => {
if recovered_address != address.as_str() {
Err(LoginError::AddressMismatch)
} else {
Ok(())
}
}
Err(e) => Err(LoginError::EthError(e)),
};
siwe_messages.remove(address, nonce);
result?;
let expiration = with_settings!(|settings: &Settings| {
message
.issued_at
.saturating_add(settings.session_expires_in)
});
let seed = generate_seed(address);
signature_map.prune_expired(get_current_time(), MAX_SIGS_TO_PRUNE);
let delegation = create_delegation(session_key, expiration)?;
let delegation_hash = create_delegation_hash(&delegation);
signature_map.put(hash::hash_bytes(seed), delegation_hash);
let user_canister_pubkey = create_user_canister_pubkey(canister_id, seed.to_vec())?;
Ok(LoginDetails {
expiration,
user_canister_pubkey: ByteBuf::from(user_canister_pubkey),
})
})
}