This is a Relayer server implementation in Rust for email-based account recovery.
You can run the relayer either on your local environments or cloud instances (we are using GCP).
-
Clone the repo, https://github.com/zkemail/ether-email-auth.
-
Install dependencies.
cd ether-email-auth
and runyarn
.
-
If you have not deployed common contracts, build contract artifacts and deploy required contracts.
cd packages/contracts
and runforge build
.- Set the env file in
packages/contracts/.env
, an example env file is as follows,
LOCALHOST_RPC_URL=http://127.0.0.1:8545 SEPOLIA_RPC_URL=https://sepolia.base.org MAINNET_RPC_URL=https://mainnet.base.org PRIVATE_KEY="" CHAIN_ID=84532 RPC_URL="https://sepolia.base.org" SIGNER=0x6293a80bf4bd3fff995a0cab74cbf281d922da02 # Signer for the dkim oracle on IC (Don't change this) ETHERSCAN_API_KEY= # CHAIN_NAME="base_sepolia"
- Run
forge script script/DeployCommons.s.sol:Deploy -vvvv --rpc-url $RPC_URL --broadcast
to getECDSAOwnedDKIMRegistry
,Verifier
,EmailAuth implementation
andSimpleWallet implementation
.
-
Install PostgreSQL and create a database.
psql -U <admin_user> -d postgres
to login to administrative PostgreSQL user. Replace<admin_user>
with the administrative PostgreSQL user (commonlypostgres
).- Create a new user,
CREATE USER my_new_user WITH PASSWORD 'my_secure_password';
,ALTER USER my_new_user CREATEDB;
. - Exit
psql
and now create a new database,psql -U new_user -d postgres
followed byCREATE DATABASE my_new_database;
.
-
Run the prover.
cd packages/prover
andpython local.py
, let this run async.
-
Run the relayer.
cd packages/relayer
.- Set the env file, an example env file is as follows,
EMAIL_ACCOUNT_RECOVERY_VERSION_ID=1 # Address of the deployed wallet contract. PRIVATE_KEY= # Private key for Relayer's account. CHAIN_RPC_PROVIDER=https://sepolia.base.org CHAIN_RPC_EXPLORER=https://sepolia.basescan.org CHAIN_ID=84532 # Chain ID of the testnet. # IMAP + SMTP (Settings will be provided by your email provider) IMAP_DOMAIN_NAME=imap.gmail.com IMAP_PORT=993 AUTH_TYPE=password SMTP_DOMAIN_NAME=smtp.gmail.com LOGIN_ID= # IMAP login id - usually your email address. LOGIN_PASSWORD="" # IMAP password - usually your email password. PROVER_ADDRESS="http://localhost:8080" # Address of the prover. DATABASE_URL= "postgres://new_user:my_secure_password@localhost/my_new_database" WEB_SERVER_ADDRESS="127.0.0.1:4500" EMAIL_TEMPLATES_PATH= # Absolute path to packages/relayer/eml_templates CANISTER_ID="q7eci-dyaaa-aaaak-qdbia-cai" PEM_PATH="./.ic.pem" IC_REPLICA_URL="https://a4gq6-oaaaa-aaaab-qaa4q-cai.raw.icp0.io/?id=q7eci-dyaaa-aaaak-qdbia-cai" ERROR_EMAIL_ADDR="" # System user email address receiving error notification for user JSON_LOGGER=false
- Generate the
.ic.pem
file and password.- Create the
.ic.pem
file using OpenSSL:
openssl genpkey -algorithm RSA -out .ic.pem -aes-256-cbc -pass pass:your_password
- If you need a password, you can generate a random password using:
openssl rand -base64 32
- Create the
-
You should have your entire setup up and running!
NOTE: You need to turn on IMAP on the email id you’d be using for the relayer.
If you intend to use a Gmail address, you need to configure your Google account and Gmail as follows:
Gmail -> See all settings -> Forwarding and POP/IMAP -> IMAP access -> Enable IMAP.
Note that from June 2024, IMAP will be enabled by default.
Refer to the following help link.
Refer to the following help link. If you do not see the 'App passwords' option, try searching for 'app pass' in the search box to select it.
- Install
kubectl
on your system (https://kubernetes.io/docs/tasks/tools/) - Setup k8s config of
zkemail
cluster on GKE,gcloud container clusters get-credentials zkemail --region us-central1 --project (your_project_name)
- One time steps (already done)
- Create a static IP (https://cloud.google.com/vpc/docs/reserve-static-external-ip-address#gcloud)
- Map the static IP with a domain from Google Domain
- Create a
Managed Certificate
by applyingkubernetes/managed-cert.yml
(update the domain accordingly)
- (Optional) Delete
db.yml
,ingress.yml
andrelayer.yml
if applied already - (Optional) Build the Relayer’s Docker image and publish it.
- Set the config in the respective manifests (Here, you can set the image of the relayer in
relayer.yml
, latest image already present in the config.) - Apply
db.yml
- Apply
relayer.yml
, ssh into the pod and runnohup cargo run &
, this step should be done under a min to pass the liveness check. - Apply
ingress.yml
It has the following tables in the DB.
-
credentials
: a table to store an account code for each pair of the wallet address and the guardian's email address.account_code TEXT PRIMARY KEY
account_eth_addr TEXT NOT NULL
guardian_email_addr TEXT NOT NULL
is_set BOOLEAN NOT NULL DEFAULT FALSE
-
requests
: a table to store requests from the caller of REST APIs.request_id BIGINT PRIMARY KEY
account_eth_addr TEXT, NOT NULL
controller_eth_addr TEXT NOT NULL
guardian_email_addr TEXT NOT NULL
is_for_recovery BOOLEAN NOT NULL DEFAULT FALSE
template_idx INT NOT NULL
is_processed BOOLEAN NOT NULL DEFAULT FALSE
is_success BOOLEAN
email_nullifier TEXT
account_salt TEXT
It exposes the following REST APIs.
-
GET requestStatus
- Receive
request_id
. - Retrieve a record with
request_id
from therequests
table. If such a record does not exist, return 0. - If
is_processed
is false, return 1. Otherwise, return 2,is_success
,email_nullifier
,account_salt
.
- Receive
-
POST getAccountSalt
- Receive
account_code
andemail_addr
. - Compute
account_salt
from the givenaccount_code
andemail_addr
. - Return
account_salt
.
- Receive
-
POST acceptanceRequest
- Receive
controller_eth_addr
,guardian_email_addr
,account_code
,template_idx
, andsubject
. - Let
subject_template
be thetemplate_idx
-th template inacceptanceSubjectTemplates()
ofcontroller_eth_addr
. - If
subject
does not match withsubject_template
return a 400 response. Letsubject_params
be the parsed values. - Extract
account_eth_addr
from the givensubject
by followingsubject_template
. - If the contract of
account_eth_addr
is not deployed, return a 400 response. - If a record with
account_code
exists in thecredentials
table, return a 400 response. - Randomly generate a
request_id
. If a record withrequest_id
exists in therequests
table, regenerate a newrequest_id
. - If a record with
account_eth_addr
,guardian_email_addr
andis_set=true
exists in thecredentials
table,- Insert
(request_id, account_eth_addr, controller_eth_addr, guardian_email_addr, false, template_idx, false)
into therequests
table. - Send
guardian_email_addr
an error email to say thataccount_eth_addr
tries to set you to a guardian, which is rejected since you are already its guardian. - Return a 200 response along with
request_id
andsubject_params
to prevent a malicious client user from learning if the pair of theaccount_eth_addr
and theguardian_email_addr
is already set or not.
- Insert
- Insert
(account_code, account_eth_addr, controller_eth_addr, guardian_email_addr, false)
into thecredentials
table. - Insert
(request_id, account_eth_addr, controller_eth_addr, guardian_email_addr, false, template_idx)
into therequests
table. - Send an email as follows.
- To:
guardian_email_addr
- Subject: if the domain of
guardian_email_addr
signs the To field,subject
. Otherwise,subject + " Code " + hex(account_code)"
. - Reply-to:
relayer_email_addr_before_domain + "+code" + hex(account_code) + "@" + relayer_email_addr_domain
. - Body: Any message, but it MUST contain
"#" + digit(request_id)
.
- To:
- Return a 200 response along with
request_id
andsubject_params
.
- Receive
-
POST recoveryRequest
- Receive
controller_eth_addr
,guardian_email_addr
,template_idx
, andsubject
. - Let
subject_template
be thetemplate_idx
-th template inrecoverySubjectTemplates()
ofaccount_eth_addr
. - If the
subject
does not match withsubject_template
return a 400 response. Letsubject_params
be the parsed values. - Extract
account_eth_addr
from the givensubject
by followingsubject_template
. - If the contract of
account_eth_addr
is not deployed, return a 400 response. - Randomly generate a
request_id
. If a record withrequest_id
exists in therequests
table, regenerate a newrequest_id
. - If a record with
account_eth_addr
,guardian_email_addr
, andis_set=true
exists in thecredentials
table,- Insert
(request_id, account_eth_addr, controller_eth_addr, guardian_email_addr, true, template_idx, false)
into therequests
table. - Send an email as follows.
- To:
guardian_email_addr
- Subject: if the domain of
guardian_email_addr
signs the To field,subject
. Otherwise,subject + " Code " + hex(account_code)"
. - Reply-to:
relayer_email_addr_before_domain ~~+ "+code" + hex(account_code)~~ + "@" + relayer_email_addr_domain
. - Body: Any message, but it MUST contain
"#" + digit(request_id)
.
- To:
- Return a 200 response along with
request_id
andsubject_params
.
- Insert
- If a record with
account_eth_addr
,guardian_email_addr
, andis_set=false
exists in thecredentials
table,- Insert
(request_id, account_eth_addr, guardian_email_addr, true, template_idx, false)
into therequests
table. - Send an email as follows.
- To:
guardian_email_addr
- Subject: A message to say that
account_eth_addr
requests your account recovery, but you have not approved being its guardian.
- To:
- Return a 200 response along with
request_id
andsubject_params
.
- Insert
- If a record with
account_eth_addr
,guardian_email_addr
does not exist in thecredentials
table,- Insert
(request_id, account_eth_addr, guardian_email_addr, true, template_idx, false)
into therequests
table. - Send an email as follows.
- To:
guardian_email_addr
- Subject: if the domain of
guardian_email_addr
signs the To field,subject
. Otherwise,subject + " Code "
. - Reply-to:
relayer_email_addr_before_domain + "@" + relayer_email_addr_domain
. - Body: Any message, but it MUST contain
"#" + digit(request_id)
. Also, the message asks the guardian to reply to this email with the guardian’s account code after" Code "
in the subject.
- To:
- Return a 200 response along with
request_id
andsubject_params
.
- Insert
- Receive
-
POST completeRequest
- Receive
account_eth_addr
,controller_eth_addr
, andcomplete_calldata
. - If the contract of
acciybt_eth_addr
is not deployed, return a 400 response. - Call the
completeRecovery
function in the contract ofcontroller_eth_addr
with passingaccount_eth_addr
andcomplete_calldata
. - If the transaction fails, return a 400 response. Otherwise, return a 200 response.
- Receive
When receiving a new email, the relayer handles it as follows.
- Extract
guardian_email_addr
from the From field,raw_subject
from the Subject field, and"#" + digit(request_id)
from the email body. - If no record with
request_id
exists in therequests
table, sendguardian_email_addr
an email to tell that the givenrequest_id
does not exist. - If the invitation code for
account_code
exists in the email header,- If a record with
account_code
exists in thecredentials
table, assert thatguardian_email_addr
is the same as the extracted one. - If no record with
account_code
exists in thecredentials
table, assert that theEmailAuth
contract whose address corresponds toaccount_code
andguardian_email_addr
is already deployed. Also, insert(account_code, account_eth_addr, guardian_email_addr, true)
into thecredentials
table, whereaccount_eth_addr
is the owner of that deployedEmailAuth
contract. Note that this step is for a guardian who sends an email to a new relayer due to the old relayer’s censorship.
- If a record with
- Let
email_domain
be a domain ofguardian_email_addr
. - Fetch a public key of
email_domain
from DNS and compute itspublic_key_hash
. - Let
dkim
be the output ofdkim()
ofaccount_eth_addr
. - If
DKIM(dkim).isDKIMPublicKeyHashValid(email_domain, public_key_hash)
is false, call the DKIM oracle and update thedkim
contract. - If
is_for_recovery
is false,- Let
subject_template
be thetemplate_idx
-th template inacceptanceSubjectTemplates()
ofcontroller_eth_addr
. - If
subject
does not match withsubject_template
, sendguardian_email_addr
an error email. - Parse
subject
to getsubject_params
andskiped_subject_prefix
. - Let
templateId
bekeccak256(EMAIL_ACCOUNT_RECOVERY_VERSION_ID, "ACCEPTANCE", templateIdx)
. - Generate a proof for the circuit and construct
email_proof
. - Construct
email_auth_msg
and callEmailAccountRecovery(controller_eth_addr).handleAcceptance(email_auth_msg, template_idx)
. - If the transaction fails, send
guardian_email_addr
an error email and update a record withrequest_id
in therequests
table to(is_processed=true, is_success=false, email_nullifier=email_proof.email_nullifier, account_salt=email_proof.email_nullifier, is_code_exist=email_proof.is_code_exist)
. - Update a record with
account_code
in thecredentials
table tois_set=true
. - Send
guardian_email_addr
a success email and update a record withrequest_id
in therequests
table to(is_processed=true, is_success=true, email_nullifier=email_proof.email_nullifier, account_salt=email_proof.email_nullifier, is_code_exist=email_proof.is_code_exist)
.
- Let
- If
is_for_recovery
is true,- Let
subject_template
be thetemplate_idx
-th template inrecoverySubjectTemplates()
ofcontroller_eth_addr
. - If
subject
does not match withsubject_template
, sendguardian_email_addr
an error email. - Parse
subject
to getsubject_params
andskiped_subject_prefix
. - Let
templateId
bekeccak256(EMAIL_ACCOUNT_RECOVERY_VERSION_ID, "RECOVERY", templateIdx)
. - Generate a proof for the circuit and construct
email_proof
. - Construct
email_auth_msg
and callEmailAccountRecovery(controller_eth_addr).handleRecovery(email_auth_msg, template_idx)
. - If the transaction fails, send
guardian_email_addr
an error email and update a record withrequest_id
in therequests
table to(is_processed=true, is_success=false, email_nullifier=email_proof.email_nullifier, account_salt=email_proof.account_salt, is_code_exist=email_proof.is_code_exist)
. - Send
guardian_email_addr
a success email and update a record withrequest_id
in therequests
table to(is_processed=true, is_success=true, email_nullifier=email_proof.email_nullifier, account_salt=email_proof.email_nullifier, is_code_exist=email_proof.is_code_exist)
.
- Let