Skip to content

Commit e7c9686

Browse files
committed
feat: public key resolution in parsed email
1 parent 7c89e70 commit e7c9686

File tree

5 files changed

+182
-20
lines changed

5 files changed

+182
-20
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,4 @@ wasm-bindgen-futures = "0.4.45"
4141
js-sys = "0.3.72"
4242
serde-wasm-bindgen = "0.6.5"
4343
rand = "0.8.5"
44+
base64 = "0.22.1"

src/cryptos.rs

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,19 @@
11
//! Cryptographic functions.
22
3+
#[cfg(target_arch = "wasm32")]
4+
use crate::EmailHeaders;
35
use crate::{field_to_hex, hex_to_field};
6+
use anyhow::Result;
47
use ethers::types::Bytes;
58
use halo2curves::ff::Field;
69
use poseidon_rs::{poseidon_bytes, poseidon_fields, Fr, PoseidonError};
710
use rand_core::RngCore;
11+
#[cfg(target_arch = "wasm32")]
12+
use regex::Regex;
13+
#[cfg(target_arch = "wasm32")]
14+
use rsa::pkcs8::DecodePublicKey;
15+
#[cfg(target_arch = "wasm32")]
16+
use rsa::traits::PublicKeyParts;
817
use serde::{
918
de::{self, Visitor},
1019
Deserialize, Deserializer, Serialize, Serializer,
@@ -612,3 +621,82 @@ pub fn calculate_account_salt(email_addr: &str, account_code: &str) -> String {
612621
// Convert account salt to hexadecimal representation
613622
field_to_hex(&account_salt.0)
614623
}
624+
625+
#[cfg(target_arch = "wasm32")]
626+
/// Fetches the public key from DNS records using the DKIM signature in the email headers.
627+
///
628+
/// # Arguments
629+
///
630+
/// * `email_headers` - An `EmailHeaders` object containing the headers of the email.
631+
///
632+
/// # Returns
633+
///
634+
/// A `Result` containing a vector of bytes representing the public key, or an error if the key is not found.
635+
pub async fn fetch_public_key(email_headers: EmailHeaders) -> Result<Vec<u8>> {
636+
let mut selector = String::new();
637+
let mut domain = String::new();
638+
639+
// Extract the selector and domain from the DKIM-Signature header
640+
if let Some(headers) = email_headers.get_header("DKIM-Signature") {
641+
if let Some(header) = headers.first() {
642+
let s_re = Regex::new(r"s=([^;]+);").unwrap();
643+
let d_re = Regex::new(r"d=([^;]+);").unwrap();
644+
645+
selector = s_re
646+
.captures(header)
647+
.and_then(|cap| cap.get(1))
648+
.map_or("", |m| m.as_str())
649+
.to_string();
650+
domain = d_re
651+
.captures(header)
652+
.and_then(|cap| cap.get(1))
653+
.map_or("", |m| m.as_str())
654+
.to_string();
655+
}
656+
}
657+
658+
println!("Selector: {}, Domain: {}", selector, domain);
659+
660+
// Fetch the DNS TXT record for the domain key
661+
let response = reqwest::get(format!(
662+
"https://dns.google/resolve?name={}._domainkey.{}&type=TXT",
663+
selector, domain
664+
))
665+
.await?;
666+
let data: serde_json::Value = response.json().await?;
667+
668+
// Extract the 'p' value from the Answer section
669+
let mut p_value = None;
670+
if let Some(answers) = data.get("Answer").and_then(|a| a.as_array()) {
671+
for answer in answers {
672+
if let Some(data) = answer.get("data").and_then(|d| d.as_str()) {
673+
let parts: Vec<&str> = data.split(';').collect();
674+
for part in parts {
675+
let key_value: Vec<&str> = part.trim().split('=').collect();
676+
if key_value.len() == 2 && key_value[0].trim() == "p" {
677+
p_value = Some(key_value[1].trim().to_string());
678+
break;
679+
}
680+
}
681+
}
682+
}
683+
}
684+
685+
if let Some(public_key_b64) = p_value {
686+
// Decode the base64 string to get the public key bytes
687+
let public_key_bytes = base64::decode(public_key_b64)?;
688+
689+
// Load the public key from DER format
690+
let public_key = rsa::RsaPublicKey::from_public_key_der(&public_key_bytes)?;
691+
692+
// Extract the modulus from the public key
693+
let modulus = public_key.n();
694+
695+
// Convert the modulus to a byte array in big-endian order
696+
let modulus_bytes = modulus.to_bytes_be();
697+
698+
Ok(modulus_bytes)
699+
} else {
700+
Err(anyhow::anyhow!("Public key not found"))
701+
}
702+
}

src/parse_email.rs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,17 @@
22
33
use std::collections::HashMap;
44

5+
#[cfg(target_arch = "wasm32")]
6+
use crate::cryptos::fetch_public_key;
57
use anyhow::Result;
68
use cfdkim::canonicalize_signed_email;
79
#[cfg(not(target_arch = "wasm32"))]
810
use cfdkim::resolve_public_key;
911
use hex;
1012
use itertools::Itertools;
1113
use mailparse::{parse_mail, ParsedMail};
14+
#[cfg(target_arch = "wasm32")]
15+
use regex::Regex;
1216
use rsa::traits::PublicKeyParts;
1317
use serde::{Deserialize, Serialize};
1418
use zk_regex_apis::extract_substrs::{
@@ -51,6 +55,10 @@ impl ParsedEmail {
5155
// Initialize a logger for the function scope.
5256
let logger = slog::Logger::root(slog::Discard, slog::o!());
5357

58+
// Extract all headers
59+
let parsed_mail = parse_mail(raw_email.as_bytes())?;
60+
let headers: EmailHeaders = EmailHeaders::new_from_mail(&parsed_mail);
61+
5462
// Resolve the public key from the raw email bytes.
5563
#[cfg(not(target_arch = "wasm32"))]
5664
let public_key = match resolve_public_key(&logger, raw_email.as_bytes()).await? {
@@ -59,16 +67,12 @@ impl ParsedEmail {
5967
};
6068

6169
#[cfg(target_arch = "wasm32")]
62-
let public_key = Vec::new();
70+
let public_key = fetch_public_key(headers.clone()).await?;
6371

6472
// Canonicalize the signed email to separate the header, body, and signature.
6573
let (canonicalized_header, canonicalized_body, signature_bytes) =
6674
canonicalize_signed_email(raw_email.as_bytes())?;
6775

68-
// Extract all headers
69-
let parsed_mail = parse_mail(raw_email.as_bytes())?;
70-
let headers: EmailHeaders = EmailHeaders::new_from_mail(&parsed_mail);
71-
7276
// Construct the `ParsedEmail` instance.
7377
let parsed_email = ParsedEmail {
7478
canonicalized_header: String::from_utf8(canonicalized_header)?, // Convert bytes to string, may return an error if not valid UTF-8.

src/wasm.rs

Lines changed: 83 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,108 @@
1+
#[cfg(target_arch = "wasm32")]
12
use js_sys::Promise;
3+
#[cfg(target_arch = "wasm32")]
24
use rand::rngs::OsRng;
5+
#[cfg(target_arch = "wasm32")]
36
use serde_wasm_bindgen::to_value;
7+
#[cfg(target_arch = "wasm32")]
48
use wasm_bindgen::prelude::*;
59

10+
#[cfg(target_arch = "wasm32")]
611
use crate::{hex_to_field, AccountCode, AccountSalt, PaddedEmailAddr, ParsedEmail};
712

813
#[wasm_bindgen]
914
#[allow(non_snake_case)]
15+
#[cfg(target_arch = "wasm32")]
16+
/// Parses a raw email string into a structured `ParsedEmail` object.
17+
///
18+
/// This function utilizes the `ParsedEmail::new_from_raw_email` method to parse the email,
19+
/// and then serializes the result for JavaScript interoperability.
20+
///
21+
/// # Arguments
22+
///
23+
/// * `raw_email` - A `String` representing the raw email to be parsed.
24+
///
25+
/// # Returns
26+
///
27+
/// A `Promise` that resolves with the serialized `ParsedEmail` or rejects with an error message.
1028
pub async fn parseEmail(raw_email: String) -> Promise {
11-
let parsed_email = ParsedEmail::new_from_raw_email(&raw_email)
12-
.await
13-
.expect("Failed to parse email");
14-
let parsed_email = to_value(&parsed_email).expect("Failed to serialize ParsedEmail");
15-
Promise::resolve(&parsed_email)
29+
match ParsedEmail::new_from_raw_email(&raw_email).await {
30+
Ok(parsed_email) => match to_value(&parsed_email) {
31+
Ok(serialized_email) => Promise::resolve(&serialized_email),
32+
Err(_) => Promise::reject(&JsValue::from_str("Failed to serialize ParsedEmail")),
33+
},
34+
Err(_) => Promise::reject(&JsValue::from_str("Failed to parse email")),
35+
}
1636
}
1737

1838
#[wasm_bindgen]
1939
#[allow(non_snake_case)]
20-
pub async fn generateAccountCode() -> JsValue {
21-
to_value(&AccountCode::new(OsRng)).expect("Failed to serialize AccountCode")
40+
#[cfg(target_arch = "wasm32")]
41+
/// Generates a new `AccountCode` using a secure random number generator.
42+
///
43+
/// This function creates a new `AccountCode` and serializes it for JavaScript interoperability.
44+
///
45+
/// # Returns
46+
///
47+
/// A `Promise` that resolves with the serialized `AccountCode` or rejects with an error message.
48+
pub async fn generateAccountCode() -> Promise {
49+
match to_value(&AccountCode::new(OsRng)) {
50+
Ok(serialized_code) => Promise::resolve(&serialized_code),
51+
Err(_) => Promise::reject(&JsValue::from_str("Failed to serialize AccountCode")),
52+
}
2253
}
2354

2455
#[wasm_bindgen]
2556
#[allow(non_snake_case)]
26-
pub async fn generateAccountSalt(email_addr: String, account_code: String) -> JsValue {
57+
#[cfg(target_arch = "wasm32")]
58+
/// Generates an `AccountSalt` using a padded email address and an account code.
59+
///
60+
/// This function converts the email address to a padded format, parses the account code,
61+
/// and generates an `AccountSalt`, which is then serialized for JavaScript interoperability.
62+
///
63+
/// # Arguments
64+
///
65+
/// * `email_addr` - A `String` representing the email address.
66+
/// * `account_code` - A `String` representing the account code in hexadecimal format.
67+
///
68+
/// # Returns
69+
///
70+
/// A `Promise` that resolves with the serialized `AccountSalt` or rejects with an error message.
71+
pub async fn generateAccountSalt(email_addr: String, account_code: String) -> Promise {
2772
let email_addr = PaddedEmailAddr::from_email_addr(&email_addr);
28-
let account_code =
29-
AccountCode::from(hex_to_field(&account_code).expect("Failed to parse AccountCode"));
30-
let account_salt =
31-
AccountSalt::new(&email_addr, account_code).expect("Failed to generate AccountSalt");
32-
to_value(&account_salt).expect("Failed to serialize AccountSalt")
73+
let account_code = match hex_to_field(&account_code) {
74+
Ok(field) => AccountCode::from(field),
75+
Err(_) => return Promise::reject(&JsValue::from_str("Failed to parse AccountCode")),
76+
};
77+
let account_salt = match AccountSalt::new(&email_addr, account_code) {
78+
Ok(salt) => salt,
79+
Err(_) => return Promise::reject(&JsValue::from_str("Failed to generate AccountSalt")),
80+
};
81+
match to_value(&account_salt) {
82+
Ok(serialized_salt) => Promise::resolve(&serialized_salt),
83+
Err(_) => Promise::reject(&JsValue::from_str("Failed to serialize AccountSalt")),
84+
}
3385
}
3486

3587
#[wasm_bindgen]
3688
#[allow(non_snake_case)]
37-
pub async fn padEmailAddr(email_addr: String) -> JsValue {
89+
#[cfg(target_arch = "wasm32")]
90+
/// Pads an email address to a fixed length format.
91+
///
92+
/// This function converts the email address to a padded format and serializes it
93+
/// for JavaScript interoperability.
94+
///
95+
/// # Arguments
96+
///
97+
/// * `email_addr` - A `String` representing the email address to be padded.
98+
///
99+
/// # Returns
100+
///
101+
/// A `Promise` that resolves with the serialized padded email address or rejects with an error message.
102+
pub async fn padEmailAddr(email_addr: String) -> Promise {
38103
let padded_email_addr = PaddedEmailAddr::from_email_addr(&email_addr);
39-
to_value(&padded_email_addr).expect("Failed to serialize padded_email_addr")
104+
match to_value(&padded_email_addr) {
105+
Ok(serialized_addr) => Promise::resolve(&serialized_addr),
106+
Err(_) => Promise::reject(&JsValue::from_str("Failed to serialize padded_email_addr")),
107+
}
40108
}

0 commit comments

Comments
 (0)