myc

Self-custodial remote signer for Radroots apps
git clone https://radroots.dev/git/myc.git
Log | Files | Refs | README | LICENSE

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 }