app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

identities.rs (19038B)


      1 use harvestcircle_application::{AppStateRepository, BoxFuture, IdentityRepository};
      2 use harvestcircle_domain::{
      3     IdentityCreatedAt, IdentityLabel, LocalKeyringBinding, NostrIdentity, NostrIdentityReference,
      4     PublicKey, SafeError, SafeErrorCode, SafeMessage, SignerAvailability, UnixTimestamp,
      5 };
      6 use sqlx::Row;
      7 
      8 use crate::db::{corrupt_storage, map_transaction_error, storage_unavailable};
      9 use crate::{Database, HARVESTCIRCLE_IDENTITY_CAPACITY};
     10 
     11 const IDENTITY_PROJECTION: &str = "SELECT substr(identity.public_key, 1, 33) AS public_key, length(identity.public_key) AS public_key_bytes, \
     12      substr(CAST(identity.npub AS BLOB), 1, 64) AS npub, length(CAST(identity.npub AS BLOB)) AS npub_bytes, \
     13      substr(CAST(binding.binding_kind AS BLOB), 1, 17) AS binding_kind, \
     14      length(CAST(binding.binding_kind AS BLOB)) AS binding_kind_bytes, \
     15      substr(CAST(binding.availability AS BLOB), 1, 19) AS availability, \
     16      length(CAST(binding.availability AS BLOB)) AS availability_bytes, \
     17      CASE WHEN identity.label IS NULL THEN NULL ELSE substr(CAST(identity.label AS BLOB), 1, 81) END AS label, \
     18      CASE WHEN identity.label IS NULL THEN NULL ELSE length(CAST(identity.label AS BLOB)) END AS label_bytes, \
     19      identity.created_at_unix_s, identity.last_used_at_unix_s \
     20      FROM account_identities AS identity JOIN local_signer_bindings AS binding \
     21      ON binding.account_public_key = identity.public_key";
     22 
     23 impl IdentityRepository for Database {
     24     fn list_identities(&self) -> BoxFuture<'_, Result<Vec<NostrIdentity>, SafeError>> {
     25         Box::pin(async move {
     26             self.host()
     27                 .transaction(|transaction| {
     28                     Box::pin(async move {
     29                         let sql = format!(
     30                             "{IDENTITY_PROJECTION} ORDER BY identity.created_at_unix_s, identity.public_key LIMIT {}",
     31                             HARVESTCIRCLE_IDENTITY_CAPACITY + 1
     32                         );
     33                         let rows = sqlx::query(sqlx::AssertSqlSafe(sql.as_str()))
     34                             .fetch_all(&mut *transaction)
     35                             .await
     36                             .map_err(|_| corrupt_storage())?;
     37                         if rows.len() > HARVESTCIRCLE_IDENTITY_CAPACITY {
     38                             return Err(corrupt_storage());
     39                         }
     40                         rows.iter().map(decode_identity).collect()
     41                     })
     42                 })
     43                 .await
     44                 .map_err(map_transaction_error)
     45         })
     46     }
     47 
     48     fn find_identity(
     49         &self,
     50         public_key: PublicKey,
     51     ) -> BoxFuture<'_, Result<Option<NostrIdentity>, SafeError>> {
     52         Box::pin(async move {
     53             self.host()
     54                 .transaction(|transaction| {
     55                     Box::pin(async move {
     56                         let sql =
     57                             format!("{IDENTITY_PROJECTION} WHERE identity.public_key = ? LIMIT 2");
     58                         let rows = sqlx::query(sqlx::AssertSqlSafe(sql.as_str()))
     59                             .bind(public_key.as_bytes().as_slice())
     60                             .fetch_all(&mut *transaction)
     61                             .await
     62                             .map_err(|_| corrupt_storage())?;
     63                         match rows.as_slice() {
     64                             [] => Ok(None),
     65                             [row] => decode_identity(row).map(Some),
     66                             _ => Err(corrupt_storage()),
     67                         }
     68                     })
     69                 })
     70                 .await
     71                 .map_err(map_transaction_error)
     72         })
     73     }
     74 
     75     fn insert_identity<'a>(
     76         &'a self,
     77         identity: &'a NostrIdentity,
     78     ) -> BoxFuture<'a, Result<(), SafeError>> {
     79         Box::pin(async move {
     80             let encoded = EncodedIdentity::try_from(identity)?;
     81             self.host()
     82                 .transaction(|transaction| {
     83                     Box::pin(async move {
     84                         let existing: i64 = sqlx::query_scalar(
     85                             "SELECT EXISTS(SELECT 1 FROM account_identities WHERE public_key = ?)",
     86                         )
     87                         .bind(encoded.public_key.as_slice())
     88                         .fetch_one(&mut *transaction)
     89                         .await
     90                         .map_err(|_| storage_unavailable())?;
     91                         match existing {
     92                             0 => {}
     93                             1 => return Err(identity_exists()),
     94                             _ => return Err(corrupt_storage()),
     95                         }
     96                         let admitted: i64 = sqlx::query_scalar(
     97                             "SELECT count(*) FROM (SELECT 1 FROM account_identities LIMIT 257)",
     98                         )
     99                         .fetch_one(&mut *transaction)
    100                         .await
    101                         .map_err(|_| storage_unavailable())?;
    102                         if usize::try_from(admitted)
    103                             .ok()
    104                             .is_none_or(|count| count >= HARVESTCIRCLE_IDENTITY_CAPACITY)
    105                         {
    106                             return Err(identity_capacity_exhausted());
    107                         }
    108                         let result = sqlx::query(
    109                             "INSERT INTO account_identities \
    110                              (public_key, npub, label, created_at_unix_s, last_used_at_unix_s) \
    111                              VALUES (?, ?, ?, ?, ?)",
    112                         )
    113                         .bind(encoded.public_key.as_slice())
    114                         .bind(&encoded.npub)
    115                         .bind(&encoded.label)
    116                         .bind(encoded.created_at)
    117                         .bind(encoded.last_used_at)
    118                         .execute(&mut *transaction)
    119                         .await;
    120                         match result {
    121                             Ok(result) if result.rows_affected() == 1 => {}
    122                             Err(error) if is_unique_violation(&error) => {
    123                                 return Err(identity_exists());
    124                             }
    125                             Ok(_) | Err(_) => return Err(storage_unavailable()),
    126                         }
    127                         let result = sqlx::query(
    128                             "INSERT INTO local_signer_bindings \
    129                              (account_public_key, binding_public_key, binding_kind, availability) \
    130                              VALUES (?, ?, 'local_secret', ?)",
    131                         )
    132                         .bind(encoded.public_key.as_slice())
    133                         .bind(encoded.public_key.as_slice())
    134                         .bind(encoded.key_availability)
    135                         .execute(&mut *transaction)
    136                         .await
    137                         .map_err(|_| storage_unavailable())?;
    138                         if result.rows_affected() != 1 {
    139                             return Err(storage_unavailable());
    140                         }
    141                         Ok(())
    142                     })
    143                 })
    144                 .await
    145                 .map_err(map_transaction_error)
    146         })
    147     }
    148 
    149     fn update_identity<'a>(
    150         &'a self,
    151         identity: &'a NostrIdentity,
    152     ) -> BoxFuture<'a, Result<(), SafeError>> {
    153         Box::pin(async move {
    154             let encoded = EncodedIdentity::try_from(identity)?;
    155             self.host()
    156                 .transaction(|transaction| {
    157                     Box::pin(async move {
    158                         let result = sqlx::query(
    159                             "UPDATE account_identities SET npub = ?, label = ?, \
    160                              created_at_unix_s = ?, last_used_at_unix_s = ? WHERE public_key = ?",
    161                         )
    162                         .bind(&encoded.npub)
    163                         .bind(&encoded.label)
    164                         .bind(encoded.created_at)
    165                         .bind(encoded.last_used_at)
    166                         .bind(encoded.public_key.as_slice())
    167                         .execute(&mut *transaction)
    168                         .await
    169                         .map_err(|_| storage_unavailable())?;
    170                         if result.rows_affected() == 0 {
    171                             return Err(identity_not_found());
    172                         }
    173                         if result.rows_affected() != 1 {
    174                             return Err(corrupt_storage());
    175                         }
    176                         let result = sqlx::query(
    177                             "UPDATE local_signer_bindings SET availability = ? \
    178                              WHERE account_public_key = ? AND binding_public_key = ? \
    179                              AND binding_kind = 'local_secret'",
    180                         )
    181                         .bind(encoded.key_availability)
    182                         .bind(encoded.public_key.as_slice())
    183                         .bind(encoded.public_key.as_slice())
    184                         .execute(&mut *transaction)
    185                         .await
    186                         .map_err(|_| storage_unavailable())?;
    187                         if result.rows_affected() != 1 {
    188                             return Err(corrupt_storage());
    189                         }
    190                         Ok(())
    191                     })
    192                 })
    193                 .await
    194                 .map_err(map_transaction_error)
    195         })
    196     }
    197 
    198     fn remove_identity(&self, public_key: PublicKey) -> BoxFuture<'_, Result<(), SafeError>> {
    199         Box::pin(async move {
    200             self.host()
    201                 .transaction(|transaction| {
    202                     Box::pin(async move {
    203                         let result =
    204                             sqlx::query("DELETE FROM account_identities WHERE public_key = ?")
    205                                 .bind(public_key.as_bytes().as_slice())
    206                                 .execute(&mut *transaction)
    207                                 .await
    208                                 .map_err(|_| storage_unavailable())?;
    209                         match result.rows_affected() {
    210                             1 => Ok(()),
    211                             0 => Err(identity_not_found()),
    212                             _ => Err(corrupt_storage()),
    213                         }
    214                     })
    215                 })
    216                 .await
    217                 .map_err(map_transaction_error)
    218         })
    219     }
    220 }
    221 
    222 impl AppStateRepository for Database {
    223     fn load_selected_identity(&self) -> BoxFuture<'_, Result<Option<PublicKey>, SafeError>> {
    224         Box::pin(async move {
    225             self.host()
    226                 .transaction(|transaction| {
    227                     Box::pin(async move {
    228                         let rows = sqlx::query(
    229                             "SELECT CASE WHEN selected_public_key IS NULL THEN NULL \
    230                              ELSE substr(selected_public_key, 1, 33) END AS selected_public_key, \
    231                              CASE WHEN selected_public_key IS NULL THEN NULL \
    232                              ELSE length(selected_public_key) END AS selected_bytes \
    233                              FROM runtime_state WHERE singleton = 1 LIMIT 2",
    234                         )
    235                         .fetch_all(&mut *transaction)
    236                         .await
    237                         .map_err(|_| corrupt_storage())?;
    238                         let [row] = rows.as_slice() else {
    239                             return Err(corrupt_storage());
    240                         };
    241                         let value = row
    242                             .try_get::<Option<Vec<u8>>, _>("selected_public_key")
    243                             .map_err(|_| corrupt_storage())?;
    244                         let length = row
    245                             .try_get::<Option<i64>, _>("selected_bytes")
    246                             .map_err(|_| corrupt_storage())?;
    247                         match (value, length) {
    248                             (None, None) => Ok(None),
    249                             (Some(value), Some(32)) => public_key_from_bytes(&value).map(Some),
    250                             _ => Err(corrupt_storage()),
    251                         }
    252                     })
    253                 })
    254                 .await
    255                 .map_err(map_transaction_error)
    256         })
    257     }
    258 
    259     fn save_selected_identity(
    260         &self,
    261         public_key: Option<PublicKey>,
    262     ) -> BoxFuture<'_, Result<(), SafeError>> {
    263         Box::pin(async move {
    264             self.host()
    265                 .transaction(|transaction| {
    266                     Box::pin(async move {
    267                         if let Some(public_key) = public_key {
    268                             let exists: i64 = sqlx::query_scalar(
    269                                 "SELECT EXISTS(SELECT 1 FROM account_identities WHERE public_key = ?)",
    270                             )
    271                             .bind(public_key.as_bytes().as_slice())
    272                             .fetch_one(&mut *transaction)
    273                             .await
    274                             .map_err(|_| storage_unavailable())?;
    275                             if exists != 1 {
    276                                 return Err(identity_not_found());
    277                             }
    278                         }
    279                         let selected = public_key.map(|key| key.as_bytes().to_vec());
    280                         let result = sqlx::query(
    281                             "UPDATE runtime_state SET selected_public_key = ? WHERE singleton = 1",
    282                         )
    283                         .bind(selected)
    284                         .execute(&mut *transaction)
    285                         .await
    286                         .map_err(|_| storage_unavailable())?;
    287                         if result.rows_affected() != 1 {
    288                             return Err(corrupt_storage());
    289                         }
    290                         Ok(())
    291                     })
    292                 })
    293                 .await
    294                 .map_err(map_transaction_error)
    295         })
    296     }
    297 }
    298 
    299 struct EncodedIdentity {
    300     public_key: [u8; 32],
    301     npub: String,
    302     key_availability: &'static str,
    303     label: Option<String>,
    304     created_at: i64,
    305     last_used_at: Option<i64>,
    306 }
    307 
    308 impl TryFrom<&NostrIdentity> for EncodedIdentity {
    309     type Error = SafeError;
    310 
    311     fn try_from(identity: &NostrIdentity) -> Result<Self, Self::Error> {
    312         let binding = identity
    313             .signer_binding()
    314             .as_local_keyring()
    315             .ok_or_else(storage_unavailable)?;
    316         Ok(Self {
    317             public_key: *identity.public_key().as_bytes(),
    318             npub: identity.npub().as_str().to_owned(),
    319             key_availability: encode_key_availability(binding.availability()),
    320             label: identity.label().map(|label| label.as_str().to_owned()),
    321             created_at: identity.created_at().timestamp().as_seconds(),
    322             last_used_at: identity.last_used_at().map(UnixTimestamp::as_seconds),
    323         })
    324     }
    325 }
    326 
    327 fn decode_identity(row: &sqlx::sqlite::SqliteRow) -> Result<NostrIdentity, SafeError> {
    328     let public_key_bytes = row
    329         .try_get::<Vec<u8>, _>("public_key")
    330         .map_err(|_| corrupt_storage())?;
    331     if row
    332         .try_get::<i64, _>("public_key_bytes")
    333         .map_err(|_| corrupt_storage())?
    334         != 32
    335     {
    336         return Err(corrupt_storage());
    337     }
    338     let public_key = public_key_from_bytes(&public_key_bytes)?;
    339     let npub = bounded_utf8(row, "npub", "npub_bytes", 63)?.ok_or_else(corrupt_storage)?;
    340     let binding_kind =
    341         bounded_utf8(row, "binding_kind", "binding_kind_bytes", 16)?.ok_or_else(corrupt_storage)?;
    342     if binding_kind != "local_secret" {
    343         return Err(corrupt_storage());
    344     }
    345     let availability =
    346         bounded_utf8(row, "availability", "availability_bytes", 18)?.ok_or_else(corrupt_storage)?;
    347     let availability = decode_key_availability(&availability)?;
    348     let label = bounded_utf8(row, "label", "label_bytes", 80)?
    349         .map(|value| IdentityLabel::parse(&value).map_err(|_| corrupt_storage()))
    350         .transpose()?;
    351     let created_at = UnixTimestamp::from_seconds(
    352         row.try_get("created_at_unix_s")
    353             .map_err(|_| corrupt_storage())?,
    354     )
    355     .ok_or_else(corrupt_storage)?;
    356     let last_used_at = row
    357         .try_get::<Option<i64>, _>("last_used_at_unix_s")
    358         .map_err(|_| corrupt_storage())?
    359         .map(|value| UnixTimestamp::from_seconds(value).ok_or_else(corrupt_storage))
    360         .transpose()?;
    361     NostrIdentity::new(
    362         NostrIdentityReference::verify(public_key, npub).map_err(|_| corrupt_storage())?,
    363         LocalKeyringBinding::new(public_key, availability),
    364         label,
    365         IdentityCreatedAt::new(created_at),
    366         last_used_at,
    367     )
    368     .map_err(|_| corrupt_storage())
    369 }
    370 
    371 fn bounded_utf8(
    372     row: &sqlx::sqlite::SqliteRow,
    373     value_column: &str,
    374     length_column: &str,
    375     maximum: usize,
    376 ) -> Result<Option<String>, SafeError> {
    377     let value = row
    378         .try_get::<Option<Vec<u8>>, _>(value_column)
    379         .map_err(|_| corrupt_storage())?;
    380     let length = row
    381         .try_get::<Option<i64>, _>(length_column)
    382         .map_err(|_| corrupt_storage())?;
    383     match (value, length) {
    384         (None, None) => Ok(None),
    385         (Some(value), Some(length))
    386             if usize::try_from(length)
    387                 .ok()
    388                 .is_some_and(|length| length <= maximum && length == value.len()) =>
    389         {
    390             String::from_utf8(value)
    391                 .map(Some)
    392                 .map_err(|_| corrupt_storage())
    393         }
    394         _ => Err(corrupt_storage()),
    395     }
    396 }
    397 
    398 fn public_key_from_bytes(bytes: &[u8]) -> Result<PublicKey, SafeError> {
    399     let bytes: [u8; 32] = bytes.try_into().map_err(|_| corrupt_storage())?;
    400     PublicKey::from_bytes(bytes).map_err(|_| corrupt_storage())
    401 }
    402 
    403 const fn encode_key_availability(value: SignerAvailability) -> &'static str {
    404     match value {
    405         SignerAvailability::Available => "available",
    406         SignerAvailability::CredentialMissing => "credential_missing",
    407         SignerAvailability::StoreUnavailable => "store_unavailable",
    408     }
    409 }
    410 
    411 fn decode_key_availability(value: &str) -> Result<SignerAvailability, SafeError> {
    412     match value {
    413         "available" => Ok(SignerAvailability::Available),
    414         "credential_missing" => Ok(SignerAvailability::CredentialMissing),
    415         "store_unavailable" => Ok(SignerAvailability::StoreUnavailable),
    416         _ => Err(corrupt_storage()),
    417     }
    418 }
    419 
    420 fn is_unique_violation(error: &sqlx::Error) -> bool {
    421     error
    422         .as_database_error()
    423         .is_some_and(sqlx::error::DatabaseError::is_unique_violation)
    424 }
    425 
    426 const fn identity_exists() -> SafeError {
    427     SafeError::new(
    428         SafeErrorCode::IdentityAlreadyExists,
    429         SafeMessage::new("That identity is already saved."),
    430     )
    431 }
    432 
    433 const fn identity_not_found() -> SafeError {
    434     SafeError::new(
    435         SafeErrorCode::IdentityNotFound,
    436         SafeMessage::new("That identity is not saved."),
    437     )
    438 }
    439 
    440 const fn identity_capacity_exhausted() -> SafeError {
    441     SafeError::new(
    442         SafeErrorCode::InvalidApplicationState,
    443         SafeMessage::new("The saved identity capacity is exhausted."),
    444     )
    445 }