backend.rs (15679B)
1 use nostr::{PublicKey, RelayUrl, UnsignedEvent}; 2 use radroots_identity::RadrootsIdentityPublic; 3 use radroots_nostr_connect::prelude::{ 4 RadrootsNostrConnectMethod, RadrootsNostrConnectPermissions, RadrootsNostrConnectRequest, 5 RadrootsNostrConnectRequestMessage, 6 }; 7 use radroots_nostr_signer::prelude::{ 8 RadrootsNostrLocalSignerAvailability, RadrootsNostrLocalSignerCapability, 9 RadrootsNostrRemoteSessionSignerCapability, RadrootsNostrSignerAuthorizationOutcome, 10 RadrootsNostrSignerBackend, RadrootsNostrSignerBackendCapabilities, 11 RadrootsNostrSignerCapability, RadrootsNostrSignerConnectEvaluation, 12 RadrootsNostrSignerConnectionDraft, RadrootsNostrSignerConnectionId, 13 RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerConnectionStatus, 14 RadrootsNostrSignerError, RadrootsNostrSignerManager, RadrootsNostrSignerPendingRequest, 15 RadrootsNostrSignerPublishTransition, RadrootsNostrSignerPublishWorkflowRecord, 16 RadrootsNostrSignerRequestAuditRecord, RadrootsNostrSignerRequestDecision, 17 RadrootsNostrSignerRequestEvaluation, RadrootsNostrSignerSessionLookup, 18 RadrootsNostrSignerSignOutput, RadrootsNostrSignerWorkflowId, 19 }; 20 21 use crate::app::MycSignerContext; 22 use crate::error::MycError; 23 24 #[derive(Clone)] 25 pub struct MycSignerBackend { 26 signer: MycSignerContext, 27 } 28 29 impl MycSignerBackend { 30 pub fn new(signer: MycSignerContext) -> Self { 31 Self { signer } 32 } 33 34 fn manager(&self) -> Result<RadrootsNostrSignerManager, RadrootsNostrSignerError> { 35 self.signer 36 .load_signer_manager() 37 .map_err(convert_runtime_signer_error) 38 } 39 40 fn configured_signer_identity(&self) -> RadrootsIdentityPublic { 41 self.signer.signer_public_identity() 42 } 43 44 fn local_signer_capability(&self) -> RadrootsNostrLocalSignerCapability { 45 let public_identity = self.configured_signer_identity(); 46 RadrootsNostrLocalSignerCapability::new( 47 public_identity.id.clone(), 48 public_identity, 49 RadrootsNostrLocalSignerAvailability::SecretBacked, 50 ) 51 } 52 } 53 54 impl RadrootsNostrSignerBackend for MycSignerBackend { 55 fn signer_identity(&self) -> Result<Option<RadrootsIdentityPublic>, RadrootsNostrSignerError> { 56 Ok(Some(self.configured_signer_identity())) 57 } 58 59 fn set_signer_identity( 60 &self, 61 signer_identity: RadrootsIdentityPublic, 62 ) -> Result<(), RadrootsNostrSignerError> { 63 let configured = self.configured_signer_identity(); 64 if configured.id != signer_identity.id 65 || configured.public_key_hex != signer_identity.public_key_hex 66 || configured.public_key_npub != signer_identity.public_key_npub 67 { 68 return Err(RadrootsNostrSignerError::InvalidState(format!( 69 "runtime-backed myc signer backend cannot switch signer identity from `{}` to `{}`", 70 configured.id, signer_identity.id 71 ))); 72 } 73 self.manager()?.set_signer_identity(signer_identity) 74 } 75 76 fn capabilities( 77 &self, 78 ) -> Result<RadrootsNostrSignerBackendCapabilities, RadrootsNostrSignerError> { 79 let remote_sessions = self 80 .manager()? 81 .list_connections()? 82 .into_iter() 83 .filter(|record| record.status == RadrootsNostrSignerConnectionStatus::Active) 84 .map(|record| RadrootsNostrRemoteSessionSignerCapability::from(&record)) 85 .collect(); 86 Ok(RadrootsNostrSignerBackendCapabilities::new( 87 Some(self.local_signer_capability()), 88 remote_sessions, 89 )) 90 } 91 92 fn list_connections( 93 &self, 94 ) -> Result<Vec<RadrootsNostrSignerConnectionRecord>, RadrootsNostrSignerError> { 95 self.manager()?.list_connections() 96 } 97 98 fn get_connection( 99 &self, 100 connection_id: &RadrootsNostrSignerConnectionId, 101 ) -> Result<Option<RadrootsNostrSignerConnectionRecord>, RadrootsNostrSignerError> { 102 self.manager()?.get_connection(connection_id) 103 } 104 105 fn list_publish_workflows( 106 &self, 107 ) -> Result<Vec<RadrootsNostrSignerPublishWorkflowRecord>, RadrootsNostrSignerError> { 108 self.manager()?.list_publish_workflows() 109 } 110 111 fn get_publish_workflow( 112 &self, 113 workflow_id: &RadrootsNostrSignerWorkflowId, 114 ) -> Result<Option<RadrootsNostrSignerPublishWorkflowRecord>, RadrootsNostrSignerError> { 115 self.manager()?.get_publish_workflow(workflow_id) 116 } 117 118 fn find_connections_by_client_public_key( 119 &self, 120 client_public_key: &PublicKey, 121 ) -> Result<Vec<RadrootsNostrSignerConnectionRecord>, RadrootsNostrSignerError> { 122 self.manager()? 123 .find_connections_by_client_public_key(client_public_key) 124 } 125 126 fn find_connection_by_connect_secret( 127 &self, 128 connect_secret: &str, 129 ) -> Result<Option<RadrootsNostrSignerConnectionRecord>, RadrootsNostrSignerError> { 130 self.manager()? 131 .find_connection_by_connect_secret(connect_secret) 132 } 133 134 fn lookup_session( 135 &self, 136 client_public_key: &PublicKey, 137 connect_secret: Option<&str>, 138 ) -> Result<RadrootsNostrSignerSessionLookup, RadrootsNostrSignerError> { 139 self.manager()? 140 .lookup_session(client_public_key, connect_secret) 141 } 142 143 fn evaluate_connect_request( 144 &self, 145 client_public_key: PublicKey, 146 request: RadrootsNostrConnectRequest, 147 ) -> Result<RadrootsNostrSignerConnectEvaluation, RadrootsNostrSignerError> { 148 self.manager()? 149 .evaluate_connect_request(client_public_key, request) 150 } 151 152 fn register_connection( 153 &self, 154 draft: RadrootsNostrSignerConnectionDraft, 155 ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> { 156 self.manager()?.register_connection(draft) 157 } 158 159 fn set_granted_permissions( 160 &self, 161 connection_id: &RadrootsNostrSignerConnectionId, 162 granted_permissions: RadrootsNostrConnectPermissions, 163 ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> { 164 self.manager()? 165 .set_granted_permissions(connection_id, granted_permissions) 166 } 167 168 fn approve_connection( 169 &self, 170 connection_id: &RadrootsNostrSignerConnectionId, 171 granted_permissions: RadrootsNostrConnectPermissions, 172 ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> { 173 self.manager()? 174 .approve_connection(connection_id, granted_permissions) 175 } 176 177 fn reject_connection( 178 &self, 179 connection_id: &RadrootsNostrSignerConnectionId, 180 reason: Option<String>, 181 ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> { 182 self.manager()?.reject_connection(connection_id, reason) 183 } 184 185 fn revoke_connection( 186 &self, 187 connection_id: &RadrootsNostrSignerConnectionId, 188 reason: Option<String>, 189 ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> { 190 self.manager()?.revoke_connection(connection_id, reason) 191 } 192 193 fn update_relays( 194 &self, 195 connection_id: &RadrootsNostrSignerConnectionId, 196 relays: Vec<RelayUrl>, 197 ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> { 198 self.manager()?.update_relays(connection_id, relays) 199 } 200 201 fn require_auth_challenge( 202 &self, 203 connection_id: &RadrootsNostrSignerConnectionId, 204 auth_url: &str, 205 ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> { 206 self.manager()? 207 .require_auth_challenge(connection_id, auth_url) 208 } 209 210 fn set_pending_request( 211 &self, 212 connection_id: &RadrootsNostrSignerConnectionId, 213 request_message: RadrootsNostrConnectRequestMessage, 214 ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> { 215 self.manager()? 216 .set_pending_request(connection_id, request_message) 217 } 218 219 fn authorize_auth_challenge( 220 &self, 221 connection_id: &RadrootsNostrSignerConnectionId, 222 ) -> Result<RadrootsNostrSignerAuthorizationOutcome, RadrootsNostrSignerError> { 223 self.manager()?.authorize_auth_challenge(connection_id) 224 } 225 226 fn restore_pending_auth_challenge( 227 &self, 228 connection_id: &RadrootsNostrSignerConnectionId, 229 pending_request: RadrootsNostrSignerPendingRequest, 230 ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> { 231 self.manager()? 232 .restore_pending_auth_challenge(connection_id, pending_request) 233 } 234 235 fn begin_connect_secret_publish_finalization( 236 &self, 237 connection_id: &RadrootsNostrSignerConnectionId, 238 ) -> Result<RadrootsNostrSignerPublishTransition, RadrootsNostrSignerError> { 239 self.manager()? 240 .begin_connect_secret_publish_finalization(connection_id) 241 .map(RadrootsNostrSignerPublishTransition::begun) 242 } 243 244 fn begin_auth_replay_publish_finalization( 245 &self, 246 connection_id: &RadrootsNostrSignerConnectionId, 247 ) -> Result<RadrootsNostrSignerPublishTransition, RadrootsNostrSignerError> { 248 self.manager()? 249 .begin_auth_replay_publish_finalization(connection_id) 250 .map(RadrootsNostrSignerPublishTransition::begun) 251 } 252 253 fn mark_publish_workflow_published( 254 &self, 255 workflow_id: &RadrootsNostrSignerWorkflowId, 256 ) -> Result<RadrootsNostrSignerPublishTransition, RadrootsNostrSignerError> { 257 self.manager()? 258 .mark_publish_workflow_published(workflow_id) 259 .map(RadrootsNostrSignerPublishTransition::marked_published) 260 } 261 262 fn finalize_publish_workflow( 263 &self, 264 workflow_id: &RadrootsNostrSignerWorkflowId, 265 ) -> Result<RadrootsNostrSignerPublishTransition, RadrootsNostrSignerError> { 266 let connection = self.manager()?.finalize_publish_workflow(workflow_id)?; 267 Ok(RadrootsNostrSignerPublishTransition::finalized( 268 workflow_id.clone(), 269 connection, 270 )) 271 } 272 273 fn cancel_publish_workflow( 274 &self, 275 workflow_id: &RadrootsNostrSignerWorkflowId, 276 ) -> Result<RadrootsNostrSignerPublishTransition, RadrootsNostrSignerError> { 277 self.manager()? 278 .cancel_publish_workflow(workflow_id) 279 .map(RadrootsNostrSignerPublishTransition::cancelled) 280 } 281 282 fn mark_authenticated( 283 &self, 284 connection_id: &RadrootsNostrSignerConnectionId, 285 ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> { 286 self.manager()?.mark_authenticated(connection_id) 287 } 288 289 fn mark_connect_secret_consumed( 290 &self, 291 connection_id: &RadrootsNostrSignerConnectionId, 292 ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> { 293 self.manager()?.mark_connect_secret_consumed(connection_id) 294 } 295 296 fn evaluate_request( 297 &self, 298 connection_id: &RadrootsNostrSignerConnectionId, 299 request_message: RadrootsNostrConnectRequestMessage, 300 ) -> Result<RadrootsNostrSignerRequestEvaluation, RadrootsNostrSignerError> { 301 self.manager()? 302 .evaluate_request(connection_id, request_message) 303 } 304 305 fn evaluate_auth_replay_publish_workflow( 306 &self, 307 workflow_id: &RadrootsNostrSignerWorkflowId, 308 ) -> Result<RadrootsNostrSignerRequestEvaluation, RadrootsNostrSignerError> { 309 self.manager()? 310 .evaluate_auth_replay_publish_workflow(workflow_id) 311 } 312 313 fn record_request( 314 &self, 315 connection_id: &RadrootsNostrSignerConnectionId, 316 request_id: &str, 317 method: RadrootsNostrConnectMethod, 318 decision: RadrootsNostrSignerRequestDecision, 319 message: Option<String>, 320 ) -> Result<RadrootsNostrSignerRequestAuditRecord, RadrootsNostrSignerError> { 321 self.manager()? 322 .record_request(connection_id, request_id, method, decision, message) 323 } 324 325 fn sign_unsigned_event( 326 &self, 327 unsigned_event: UnsignedEvent, 328 ) -> Result<RadrootsNostrSignerSignOutput, RadrootsNostrSignerError> { 329 let event = self 330 .signer 331 .signer_identity() 332 .sign_unsigned_event(unsigned_event, "myc signer backend event") 333 .map_err(|error| RadrootsNostrSignerError::Sign(error.to_string()))?; 334 Ok(RadrootsNostrSignerSignOutput::new( 335 RadrootsNostrSignerCapability::LocalAccount(Box::new(self.local_signer_capability())), 336 event, 337 )) 338 } 339 } 340 341 fn convert_runtime_signer_error(error: MycError) -> RadrootsNostrSignerError { 342 match error { 343 MycError::SignerState(source) => source, 344 other => RadrootsNostrSignerError::InvalidState(other.to_string()), 345 } 346 } 347 348 #[cfg(test)] 349 mod tests { 350 use std::path::PathBuf; 351 352 use nostr::Keys; 353 use radroots_identity::RadrootsIdentity; 354 use radroots_nostr_signer::prelude::{ 355 RadrootsNostrSignerBackend, RadrootsNostrSignerConnectionDraft, 356 }; 357 358 use crate::app::MycRuntime; 359 use crate::config::MycConfig; 360 361 fn write_identity(path: &std::path::Path, secret_key: &str) { 362 let identity = RadrootsIdentity::from_secret_key_str(secret_key).expect("identity"); 363 crate::identity_files::store_encrypted_identity(path, &identity).expect("save identity"); 364 } 365 366 fn test_runtime() -> MycRuntime { 367 let temp = tempfile::tempdir().expect("tempdir").keep(); 368 let mut config = MycConfig::default(); 369 config.paths.state_dir = PathBuf::from(&temp).join("state"); 370 config.paths.signer_identity_path = PathBuf::from(&temp).join("signer.json"); 371 config.paths.user_identity_path = PathBuf::from(&temp).join("user.json"); 372 write_identity( 373 &config.paths.signer_identity_path, 374 "1111111111111111111111111111111111111111111111111111111111111111", 375 ); 376 write_identity( 377 &config.paths.user_identity_path, 378 "2222222222222222222222222222222222222222222222222222222222222222", 379 ); 380 MycRuntime::bootstrap(config).expect("runtime") 381 } 382 383 #[test] 384 fn runtime_backed_backend_projects_local_and_remote_capabilities() { 385 let runtime = test_runtime(); 386 let backend = runtime.signer_backend(); 387 388 let initial = backend.capabilities().expect("capabilities"); 389 assert!( 390 initial 391 .local_signer 392 .expect("local signer capability") 393 .is_secret_backed() 394 ); 395 assert!(initial.remote_sessions.is_empty()); 396 397 let connection = backend 398 .register_connection(RadrootsNostrSignerConnectionDraft::new( 399 Keys::generate().public_key(), 400 runtime.user_public_identity(), 401 )) 402 .expect("register connection"); 403 404 let capabilities = backend.capabilities().expect("capabilities after approval"); 405 assert_eq!(capabilities.remote_sessions.len(), 1); 406 assert_eq!( 407 capabilities.remote_sessions[0].connection_id, 408 connection.connection_id 409 ); 410 } 411 412 #[test] 413 fn runtime_backed_backend_rejects_signer_identity_drift() { 414 let runtime = test_runtime(); 415 let backend = runtime.signer_backend(); 416 let other_identity = RadrootsIdentity::generate().to_public(); 417 418 let error = backend 419 .set_signer_identity(other_identity) 420 .expect_err("identity drift should be rejected"); 421 422 assert!(error.to_string().contains("cannot switch signer identity")); 423 } 424 }