Skip to content

Commit

Permalink
Simplify magic link generation + include email in signature (#45)
Browse files Browse the repository at this point in the history
  • Loading branch information
hpeebles authored Jul 1, 2024
1 parent 11e1f28 commit b197376
Show file tree
Hide file tree
Showing 17 changed files with 193 additions and 263 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions rs/canister/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

## [unreleased]

### Changed

- Simplify magic link generation + include email in signature ([#45](https://github.com/open-chat-labs/ic-sign-in-with-email/pull/45))

## [[0.10.0](https://github.com/open-chat-labs/ic-sign-in-with-email/releases/tag/v0.10.0)] - 2024-06-06

### Changed
Expand Down
14 changes: 4 additions & 10 deletions rs/canister/impl/src/email_sender.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,18 @@
use crate::env;
use candid::Principal;
use email_sender_core::EmailSender;
use magic_links::EncryptedMagicLink;
use magic_links::SignedMagicLink;
use sign_in_with_email_canister::EmailSenderConfig;
use std::sync::OnceLock;
use utils::ValidatedEmail;

static EMAIL_SENDER: OnceLock<Box<dyn EmailSender>> = OnceLock::new();

pub fn init_from_config(config: EmailSenderConfig, identity_canister_id: Principal) {
pub fn init_from_config(config: EmailSenderConfig) {
#[allow(unused_variables)]
match config {
EmailSenderConfig::Aws(aws) => {
#[cfg(feature = "email_sender_aws")]
{
init(email_sender_aws::AwsEmailSender::new(
identity_canister_id,
aws.region,
aws.function_url,
aws.access_key,
Expand All @@ -35,11 +32,8 @@ pub fn init(email_sender: impl EmailSender + 'static) {
.unwrap_or_else(|_| panic!("Email sender already set"));
}

pub async fn send_magic_link(
email: ValidatedEmail,
magic_link: EncryptedMagicLink,
) -> Result<(), String> {
pub async fn send_magic_link(magic_link: SignedMagicLink) -> Result<(), String> {
let sender = EMAIL_SENDER.get().expect("Email sender has not been set");

sender.send(email.into(), magic_link, env::now()).await
sender.send(magic_link, env::now()).await
}
4 changes: 2 additions & 2 deletions rs/canister/impl/src/lifecycle/post_upgrade.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,14 @@ fn post_upgrade(args: InitOrUpgradeArgs) {
if let Some(config) = upgrade_args.email_sender_config {
let rsa_private_key = state
.rsa_private_key()
.cloned()
.clone()
.expect("RSA private key not set");

state.set_email_sender_config(config.decrypt(&rsa_private_key));
}

if let Some(config) = state.email_sender_config().cloned() {
email_sender::init_from_config(config, env::canister_id());
email_sender::init_from_config(config);
} else if state.test_mode() {
email_sender::init(NullEmailSender::default());
}
Expand Down
22 changes: 11 additions & 11 deletions rs/canister/impl/src/queries/http_request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use crate::state::AuthResult;
use crate::{env, get_query_param_value, state};
use ic_cdk::{query, update};
use ic_http_certification::{HttpRequest, HttpResponse};
use magic_links::SignedMagicLink;
use magic_links::DoubleSignedMagicLink;

#[query]
fn http_request(request: HttpRequest) -> HttpResponse {
Expand All @@ -23,17 +23,17 @@ fn handle_http_request(request: HttpRequest, update: bool) -> HttpResponse {
"/auth" => {
let query = request.get_query().unwrap().unwrap_or_default();
let params = querystring::querify(&query);
let ciphertext = get_query_param_value(&params, "c").unwrap();
let encrypted_key = get_query_param_value(&params, "k").unwrap();
let nonce = get_query_param_value(&params, "n").unwrap();
let signature = get_query_param_value(&params, "s").unwrap();
let code = get_query_param_value(&params, "u").unwrap();

let signed_magic_link =
SignedMagicLink::from_hex_strings(&ciphertext, &encrypted_key, &nonce, &signature);

let magic_link_hex = get_query_param_value(&params, "m").unwrap();
let signature1_hex = get_query_param_value(&params, "s1").unwrap();
let signature2_hex = get_query_param_value(&params, "s2").unwrap();
let code = get_query_param_value(&params, "c").unwrap();
let magic_link = DoubleSignedMagicLink::from_hex_strings(
&magic_link_hex,
&signature1_hex,
&signature2_hex,
);
let (status_code, body, upgrade) = match state::mutate(|s| {
s.process_auth_request(signed_magic_link, code, update, env::now())
s.process_auth_request(magic_link, code, update, env::now())
}) {
AuthResult::Success => (
200,
Expand Down
35 changes: 18 additions & 17 deletions rs/canister/impl/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ use crate::{env, Hash};
use canister_sig_util::signature_map::{SignatureMap, LABEL_SIG};
use canister_sig_util::CanisterSigPublicKey;
use ic_cdk::api::set_certified_data;
use magic_links::SignedMagicLink;
use magic_links::DoubleSignedMagicLink;
use rsa::{RsaPrivateKey, RsaPublicKey};
use serde::{Deserialize, Serialize};
use sign_in_with_email_canister::{
Delegation, EmailSenderConfig, SignedDelegation, TimestampMillis, NANOS_PER_MILLISECOND,
};
use std::cell::RefCell;
use utils::{calculate_seed, delegation_signature_msg_hash, ValidatedEmail};
use utils::{calculate_seed, delegation_signature_msg_hash};

thread_local! {
static STATE: RefCell<Option<State>> = RefCell::default();
Expand Down Expand Up @@ -83,8 +83,8 @@ impl State {
self.rsa_private_key.as_ref().map(RsaPublicKey::from)
}

pub fn rsa_private_key(&self) -> Option<&RsaPrivateKey> {
self.rsa_private_key.as_ref()
pub fn rsa_private_key(&self) -> Option<RsaPrivateKey> {
self.rsa_private_key.clone()
}

pub fn set_rsa_private_key(&mut self, private_key: RsaPrivateKey) {
Expand All @@ -105,27 +105,29 @@ impl State {

pub fn process_auth_request(
&mut self,
signed_magic_link: SignedMagicLink,
signed_magic_link: DoubleSignedMagicLink,
code: String,
is_update: bool,
now: TimestampMillis,
) -> AuthResult {
let private_key = self.rsa_private_key.clone().unwrap();
if !signed_magic_link.verify_sigs(
self.rsa_public_key().unwrap(),
self.email_sender_rsa_public_key.clone(),
) {
return AuthResult::LinkInvalid("Invalid signature".to_string());
};

let magic_link =
match signed_magic_link.unwrap(self.email_sender_rsa_public_key.clone(), private_key) {
Ok(m) => m,
Err(error) => return AuthResult::LinkInvalid(error),
};
let magic_link = signed_magic_link.magic_link;

if magic_link.expired(now) {
return AuthResult::LinkExpired;
}
let msg_hash = delegation_signature_msg_hash(magic_link.delegation());
let seed = self.calculate_seed(magic_link.email());

if self
.signature_map
.get_signature_as_cbor(&magic_link.seed(), msg_hash, None)
.get_signature_as_cbor(&seed, msg_hash, None)
.is_ok()
{
if magic_link.code() == code {
Expand All @@ -136,11 +138,10 @@ impl State {
} else if !is_update {
AuthResult::RequiresUpgrade
} else {
self.signature_map
.add_signature(&magic_link.seed(), msg_hash);
let seed = self.calculate_seed(magic_link.email());
self.signature_map.add_signature(&seed, msg_hash);
self.magic_links.mark_success(seed, msg_hash, now);
self.update_root_hash();
self.magic_links
.mark_success(magic_link.seed(), msg_hash, now);

AuthResult::Success
}
Expand Down Expand Up @@ -173,7 +174,7 @@ impl State {
);
}

pub fn calculate_seed(&self, email: &ValidatedEmail) -> Hash {
pub fn calculate_seed(&self, email: &str) -> Hash {
calculate_seed(self.salt.get(), email)
}

Expand Down
30 changes: 17 additions & 13 deletions rs/canister/impl/src/updates/generate_magic_link.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,31 +14,35 @@ async fn generate_magic_link(args: GenerateMagicLinkArgs) -> GenerateMagicLinkRe

let start = env::now();

let (magic_link, encrypted_magic_link) = state::read(|s| {
let seed = s.calculate_seed(&email);
let (signed_magic_link, seed) = state::read(|s| {
let seed = s.calculate_seed(email.as_str());
let magic_link = rng::with_rng(|rng| {
magic_links::generate(seed, args.session_key, args.max_time_to_live, rng, start)
magic_links::generate(
email.to_string(),
args.session_key,
args.max_time_to_live,
rng,
start,
)
});
let public_key = s.rsa_public_key().unwrap();
let encrypted = rng::with_rng(|rng| magic_link.encrypt(public_key, rng));

(magic_link, encrypted)
let rsa_private_key = s.rsa_private_key().unwrap();
(magic_link.sign(rsa_private_key), seed)
});

if let Err(error) = email_sender::send_magic_link(email, encrypted_magic_link).await {
let delegation = signed_magic_link.magic_link.delegation().clone();
let code = signed_magic_link.magic_link.code().to_string();

if let Err(error) = email_sender::send_magic_link(signed_magic_link).await {
FailedToSendEmail(error)
} else {
let seed = magic_link.seed();
let delegation = magic_link.delegation();

state::mutate(|s| {
s.record_magic_link_sent(seed, delegation, env::now());
s.record_magic_link_sent(seed, &delegation, env::now());

Success(GenerateMagicLinkSuccess {
created: start,
user_key: s.der_encode_canister_sig_key(seed),
expiration: delegation.expiration,
code: magic_link.code().to_string(),
code,
})
})
}
Expand Down
18 changes: 8 additions & 10 deletions rs/canister/impl/src/updates/handle_magic_link.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,20 @@ use crate::{
state::{self, AuthResult},
};
use ic_cdk::update;
use magic_links::SignedMagicLink;
use magic_links::DoubleSignedMagicLink;
use sign_in_with_email_canister::{HandleMagicLinkArgs, HandleMagicLinkResponse};

#[update]
async fn handle_magic_link(args: HandleMagicLinkArgs) -> HandleMagicLinkResponse {
let params = querystring::querify(&args.link);
let ciphertext = get_query_param_value(&params, "c").unwrap();
let encrypted_key = get_query_param_value(&params, "k").unwrap();
let nonce = get_query_param_value(&params, "n").unwrap();
let signature = get_query_param_value(&params, "s").unwrap();
let code = get_query_param_value(&params, "u").unwrap();
let magic_link_hex = get_query_param_value(&params, "m").unwrap();
let signature1_hex = get_query_param_value(&params, "s1").unwrap();
let signature2_hex = get_query_param_value(&params, "s2").unwrap();
let code = get_query_param_value(&params, "c").unwrap();
let magic_link =
DoubleSignedMagicLink::from_hex_strings(&magic_link_hex, &signature1_hex, &signature2_hex);

let signed_magic_link =
SignedMagicLink::from_hex_strings(&ciphertext, &encrypted_key, &nonce, &signature);

match state::mutate(|s| s.process_auth_request(signed_magic_link, code, true, env::now())) {
match state::mutate(|s| s.process_auth_request(magic_link, code, true, env::now())) {
AuthResult::Success => HandleMagicLinkResponse::Success,
AuthResult::LinkExpired => HandleMagicLinkResponse::LinkExpired,
AuthResult::LinkInvalid(error) => HandleMagicLinkResponse::LinkInvalid(error),
Expand Down
16 changes: 8 additions & 8 deletions rs/email_sender/aws/lambda/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use aws_lambda_events::sqs::SqsMessage;
use aws_sdk_sesv2::types::builders::{DestinationBuilder, EmailContentBuilder, TemplateBuilder};
use aws_sdk_sesv2::Client as SesClient;
use lambda_runtime::{run, service_fn, Error, LambdaEvent};
use magic_links::MagicLinkMessage;
use magic_links::SignedMagicLink;
use rsa::pkcs1::DecodeRsaPrivateKey;
use rsa::RsaPrivateKey;
use serde::Serialize;
Expand Down Expand Up @@ -47,16 +47,16 @@ async fn process_record(

info!("Processing SQS Message: {body}");

let MagicLinkMessage {
email,
identity_canister_id: _,
magic_link,
} = serde_json::from_str(&body)?;
let magic_link: SignedMagicLink = serde_json::from_str(&body)?;
let email = magic_link.magic_link.email().to_string();

let signed = magic_link.sign(rsa_private_key);

let querystring = signed.build_querystring();
let magic_link = format!("https://oc.app/home{querystring}");
let template_data = TemplateData { magic_link };
let magic_link_url = format!("https://oc.app/home{querystring}");
let template_data = TemplateData {
magic_link: magic_link_url,
};

match ses_client
.send_email()
Expand Down
26 changes: 5 additions & 21 deletions rs/email_sender/aws/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,12 @@ use ic_cdk::api::management_canister::http_request::{
TransformContext, TransformFunc,
};
use ic_cdk::query;
use ic_principal::Principal;
use magic_links::{EncryptedMagicLink, MagicLinkMessage};
use magic_links::SignedMagicLink;
use time::format_description::BorrowedFormatItem;
use time::macros::format_description;
use time::OffsetDateTime;

pub struct AwsEmailSender {
identity_canister_id: Principal,
region: String,
function_url: String,
access_key: String,
Expand All @@ -25,14 +23,12 @@ const LONG_DATETIME: &[BorrowedFormatItem] =

impl AwsEmailSender {
pub fn new(
identity_canister_id: Principal,
region: String,
function_url: String,
access_key: String,
secret_key: String,
) -> AwsEmailSender {
AwsEmailSender {
identity_canister_id,
region,
function_url,
access_key,
Expand All @@ -42,22 +38,15 @@ impl AwsEmailSender {

fn build_args(
&self,
email: String,
magic_link: EncryptedMagicLink,
magic_link: SignedMagicLink,
now_millis: u64,
) -> CanisterHttpRequestArgument {
let datetime =
OffsetDateTime::from_unix_timestamp_nanos(now_millis as i128 * 1_000_000).unwrap();

let host = self.function_url.trim_start_matches("https://");
let url = format!("https://{host}");

let body = serde_json::to_string(&MagicLinkMessage {
email,
identity_canister_id: self.identity_canister_id,
magic_link,
})
.unwrap();
let body = serde_json::to_string(&magic_link).unwrap();

let mut header_map = HeaderMap::new();
header_map.insert(
Expand Down Expand Up @@ -116,13 +105,8 @@ impl AwsEmailSender {

#[async_trait]
impl EmailSender for AwsEmailSender {
async fn send(
&self,
email: String,
magic_link: EncryptedMagicLink,
now_millis: u64,
) -> Result<(), String> {
let args = self.build_args(email, magic_link, now_millis);
async fn send(&self, magic_link: SignedMagicLink, now_millis: u64) -> Result<(), String> {
let args = self.build_args(magic_link, now_millis);

let resp =
ic_cdk::api::management_canister::http_request::http_request(args, 1_000_000_000)
Expand Down
Loading

0 comments on commit b197376

Please sign in to comment.