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 }