1use crate::authenticator::Authenticator;
16use crate::error::WalletKitError;
17use crate::issuers::pop_backend_client::ManageRecoveryBindingRequest;
18use crate::issuers::pop_backend_client::RecoveryBindingResponse;
19use crate::issuers::PopBackendClient;
20use crate::user_agent::UserAgentBuilder;
21use crate::Environment;
22use alloy_core::primitives::keccak256;
23use alloy_core::primitives::Address;
24use std::string::String;
25#[derive(Debug, PartialEq, Eq, uniffi::Record)]
27pub struct RecoveryBinding {
28 pub recovery_agent: Option<String>,
30 pub pending_recovery_agent: Option<String>,
32 pub execute_after: Option<String>,
34}
35
36impl From<RecoveryBindingResponse> for RecoveryBinding {
37 fn from(response: RecoveryBindingResponse) -> Self {
38 Self {
39 recovery_agent: response.recovery_agent,
40 pending_recovery_agent: response.pending_recovery_agent,
41 execute_after: response.execute_after,
42 }
43 }
44}
45
46#[derive(uniffi::Object)]
51pub struct RecoveryBindingManager {
52 pop_backend_client: PopBackendClient,
53}
54
55#[uniffi::export]
56impl RecoveryBindingManager {
57 #[uniffi::constructor]
63 pub fn new(
64 environment: &Environment,
65 user_agent_builder: &UserAgentBuilder,
66 ) -> Result<Self, WalletKitError> {
67 let base_url = match environment {
68 Environment::Staging => "https://app.stage.orb.worldcoin.org",
69 Environment::Production => "https://app.orb.worldcoin.org",
70 }
71 .to_string();
72 Self::new_with_base_url(base_url.as_str(), user_agent_builder)
73 }
74
75 #[uniffi::constructor]
81 pub fn new_with_base_url(
82 base_url: &str,
83 user_agent_builder: &UserAgentBuilder,
84 ) -> Result<Self, WalletKitError> {
85 let user_agent = user_agent_builder.build().to_string();
86 let pop_backend_client =
87 PopBackendClient::new(base_url.to_string(), user_agent);
88 Ok(Self { pop_backend_client })
89 }
90}
91
92#[uniffi::export(async_runtime = "tokio")]
93impl RecoveryBindingManager {
94 pub async fn bind_recovery_agent(
110 &self,
111 authenticator: &Authenticator,
112 sub: String,
113 recovery_agent_address: String,
114 ) -> Result<(), WalletKitError> {
115 let challenge = self.pop_backend_client.get_challenge().await?;
116 let leaf_index = authenticator.leaf_index();
117 let sig_recovery_update = authenticator
118 .danger_sign_initiate_recovery_agent_update(recovery_agent_address.clone())
119 .await?;
120 let request = ManageRecoveryBindingRequest {
121 sub,
122 leaf_index,
123 signature: format!("0x{}", hex::encode(sig_recovery_update.signature)),
124 nonce: sig_recovery_update.nonce.to_string(),
125 recovery_agent: recovery_agent_address.clone(),
126 };
127 let security_token = Self::generate_recovery_agent_security_token(
128 authenticator,
129 &request,
130 &challenge,
131 )?;
132
133 self.pop_backend_client
134 .bind_recovery_agent(request, security_token, challenge)
135 .await?;
136 Ok(())
137 }
138
139 pub async fn unbind_recovery_agent(
151 &self,
152 authenticator: &Authenticator,
153 sub: String,
154 ) -> Result<(), WalletKitError> {
155 let leaf_index = authenticator.leaf_index();
156 let recovery_agent = Address::ZERO.to_string();
157 let sig_recovery_update = authenticator
158 .danger_sign_initiate_recovery_agent_update(recovery_agent.clone())
159 .await?;
160 let request = ManageRecoveryBindingRequest {
161 sub,
162 leaf_index,
163 signature: format!("0x{}", hex::encode(sig_recovery_update.signature)),
164 nonce: sig_recovery_update.nonce.to_string(),
165 recovery_agent,
166 };
167 let challenge = self.pop_backend_client.get_challenge().await?;
168 let security_token = Self::generate_recovery_agent_security_token(
169 authenticator,
170 &request,
171 &challenge,
172 )?;
173 self.pop_backend_client
174 .unbind_recovery_agent(request, security_token, challenge)
175 .await?;
176 Ok(())
177 }
178
179 pub async fn get_recovery_binding(
190 &self,
191 leaf_index: u64,
192 ) -> Result<RecoveryBinding, WalletKitError> {
193 let recovery_binding = self
194 .pop_backend_client
195 .get_recovery_binding(leaf_index)
196 .await?;
197 Ok(recovery_binding.into())
198 }
199}
200
201impl RecoveryBindingManager {
202 fn generate_recovery_agent_security_token(
205 authenticator: &Authenticator,
206 request: &ManageRecoveryBindingRequest,
207 challenge: &str,
208 ) -> Result<String, WalletKitError> {
209 let message_bytes =
210 Self::create_bytes_to_sign(challenge, request.leaf_index, &request.sub)?;
211 let commitment = keccak256(&message_bytes);
212 let signature: Vec<u8> =
213 authenticator.danger_sign_challenge(commitment.to_vec())?;
214 Ok(format!("0x{}", hex::encode(signature)))
215 }
216
217 fn create_bytes_to_sign(
223 challenge: &str,
224 leaf_index: u64,
225 sub: &str,
226 ) -> Result<Vec<u8>, WalletKitError> {
227 let challenge_bytes =
228 hex::decode(challenge.trim_start_matches("0x")).map_err(|e| {
229 WalletKitError::Generic {
230 error: e.to_string(),
231 }
232 })?;
233
234 let leaf_index_bytes = leaf_index.to_be_bytes();
235
236 let sub_bytes = hex::decode(sub.trim_start_matches("0x")).map_err(|e| {
237 WalletKitError::Generic {
238 error: e.to_string(),
239 }
240 })?;
241
242 let mut concatenated = Vec::new();
243 concatenated.extend_from_slice(&challenge_bytes);
244 concatenated.extend_from_slice(&leaf_index_bytes);
245 concatenated.extend_from_slice(&sub_bytes);
246
247 Ok(concatenated)
248 }
249}
250
251#[cfg(test)]
252#[cfg(all(not(target_arch = "wasm32"), feature = "embed-zkeys"))]
253mod tests {
254 use super::*;
255 use crate::authenticator::artifacts::caching::CachingZkArtifacts;
256 use crate::storage::tests_utils::{temp_root_path, InMemoryStorageProvider};
257 use crate::storage::CredentialStore;
258 use mockito::ServerGuard;
259 use std::sync::Arc;
260
261 #[tokio::test]
262 async fn test_recovery_agent_token_generator_success() {
263 let mut pop_api_server = mockito::Server::new_async().await;
264 let sub = "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"
265 .to_string();
266
267 let challenge_url_path = "/api/v1/challenge".to_string();
269 let challenge =
270 "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2"
271 .to_string();
272 let challenge_mock = pop_api_server
273 .mock("GET", challenge_url_path.as_str())
274 .with_status(200)
275 .with_body(format!("{{\"challenge\": \"{challenge}\"}}"))
276 .create_async()
277 .await;
278
279 let url_path = "/api/v1/recovery-binding".to_string();
281 let recovery_agent = "0x1000000000000000000000000000000000000000".to_string();
282 let private_key =
283 "d1995ace62b15d907bfb351ffe3cac57a8a84089a1b034101d2d7c78da415d58";
284 let private_key_bytes = hex::decode(private_key).unwrap();
285 let (mock_eth_server, eth_mock) = create_mock_eth_server().await;
286 let rpc_url = mock_eth_server.url();
287 let authenticator =
288 create_test_authenticator(&private_key_bytes, rpc_url).await;
289 let leaf_index = authenticator.leaf_index();
290 let mock = pop_api_server
291 .mock("POST", url_path.as_str())
292 .match_header(
293 "X-Auth-Signature",
294 mockito::Matcher::Regex(".*".to_string()),
295 )
296 .match_header("X-Auth-Challenge", challenge.as_str())
297 .match_body(mockito::Matcher::PartialJson(serde_json::json!({
298 "sub": sub.as_str(),
299 "leafIndex": leaf_index,
300 "recoveryAgent": recovery_agent.as_str(),
301
302 })))
303 .with_status(201)
304 .with_body("{}")
305 .create_async()
306 .await;
307
308 let recovery_binding_manager = RecoveryBindingManager::new_with_base_url(
309 pop_api_server.url().as_str(),
310 &UserAgentBuilder::new().with_walletkit_segment(),
311 )
312 .unwrap();
313
314 let result = recovery_binding_manager
315 .bind_recovery_agent(&authenticator, sub.clone(), recovery_agent.clone())
316 .await;
317 assert!(
318 result.is_ok(),
319 "Expected success, but got error: {result:?}"
320 );
321 challenge_mock.assert_async().await;
322
323 mock.assert_async().await;
324 eth_mock.assert_async().await;
325 drop(pop_api_server);
326 drop(mock_eth_server);
327 }
328
329 #[tokio::test]
330 async fn test_recovery_bindings_signature() {
331 let sub = "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"
332 .to_string();
333 let challenge =
334 "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2"
335 .to_string();
336 let private_key =
337 "d1995ace62b15d907bfb351ffe3cac57a8a84089a1b034101d2d7c78da415d58";
338 let private_key_bytes = hex::decode(private_key).unwrap();
339 let (mock_eth_server, eth_mock) = create_mock_eth_server().await;
340 let rpc_url = mock_eth_server.url();
341 let authenticator =
342 create_test_authenticator(&private_key_bytes, rpc_url).await;
343 let leaf_index = authenticator.leaf_index();
344 let message_bytes =
345 RecoveryBindingManager::create_bytes_to_sign(&challenge, leaf_index, &sub)
346 .unwrap();
347 log::info!("message_bytes: {:?}", hex::encode(message_bytes.clone()));
348 assert_eq!(hex::encode(message_bytes.clone()), "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2000000000000002aabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890");
349 let signature = "0x01".to_string();
350 let nonce = "0x02".to_string();
351 let recovery_agent = "0x1000000000000000000000000000000000000000".to_string();
352 let request = ManageRecoveryBindingRequest {
353 sub: sub.clone(),
354 leaf_index,
355 signature: signature.clone(),
356 nonce: nonce.clone(),
357 recovery_agent: recovery_agent.clone(),
358 };
359 let security_token =
360 RecoveryBindingManager::generate_recovery_agent_security_token(
361 &authenticator,
362 &request,
363 &challenge,
364 )
365 .unwrap();
366
367 assert!(
368 !security_token.is_empty(),
369 "Expected success, but got error: {security_token:?}"
370 );
371 let expect_signature = "0x72ec312737276c94e3ac32ab1c393a63b9474480d3a9eb434b8bf6927b7222ef7eb1fea0812ff62a7fb144db9631751e505969162a9c590cabb27bf0bd5005581c";
372 assert_eq!(security_token, expect_signature);
373 eth_mock.assert_async().await;
374 drop(mock_eth_server);
375 }
376
377 async fn create_test_authenticator(seed: &[u8], rpc_url: String) -> Authenticator {
378 let _ = rustls::crypto::ring::default_provider().install_default();
379 let store = create_test_credential_store();
380
381 let artifacts =
382 Arc::new(CachingZkArtifacts::new(Arc::new(store.paths().unwrap())));
383
384 let authenticator = Authenticator::init_with_defaults(
385 seed.to_vec(),
386 Some(rpc_url.clone()),
387 &Environment::Staging,
388 None,
389 artifacts,
390 store.clone(),
391 )
392 .await
393 .unwrap();
394
395 authenticator
396 }
397
398 fn create_test_credential_store() -> Arc<CredentialStore> {
399 let root = temp_root_path();
400 let provider = InMemoryStorageProvider::new(&root);
401 Arc::new(
402 CredentialStore::from_provider(&provider).expect("create credential store"),
403 )
404 }
405
406 async fn create_mock_eth_server() -> (ServerGuard, mockito::Mock) {
407 let mut mock_eth_server = mockito::Server::new_async().await;
408 let mock = mock_eth_server
409 .mock("POST", "/")
410 .with_status(200)
411 .with_header("content-type", "application/json")
412 .with_body(
413 serde_json::json!({
414 "jsonrpc": "2.0",
415 "id": 1,
416 "result": "0x000000000000000000000000000000000000000000000000000000000000002a"
417 })
418 .to_string(),
419 )
420 .expect_at_least(1)
421 .expect_at_most(2)
422 .create_async()
423 .await;
424 (mock_eth_server, mock)
425 }
426}