Skip to main content
The AccountManager struct handles all cryptographic key operations for Hoot, including key generation, secure storage using platform-specific keychains, and unwrapping gift-wrapped Nostr events.

Struct fields

loaded_keys
Vec<Keys>
Vector of loaded Nostr keypairs currently available in memory

Methods

new()

Creates a new AccountManager instance with no loaded keys.
pub fn new() -> Self
return
AccountManager
A new AccountManager instance with an empty loaded_keys vector

generate_new_keys_and_save()

Generates a new Nostr keypair and saves it securely to the system keychain and database.
pub fn generate_new_keys_and_save(&mut self, db: &Db) -> Result<Keys>
db
&Db
required
Database instance for storing the public key reference
return
Result<Keys>
The newly generated Keys object if successful

Behavior

  1. Generates a new random Nostr keypair
  2. Saves the private key to the system keychain (platform-specific secure storage)
  3. Saves the public key to the database pubkeys table
  4. Adds the keys to the loaded_keys vector
  5. Returns the generated keys
Private keys are stored in:
  • macOS: Keychain via Security Framework
  • Linux: Secret Service API (or file-based fallback)
  • Windows: Credential Manager

save_keys()

Saves an existing keypair to secure storage and the database.
pub fn save_keys(&mut self, db: &Db, keys: &Keys) -> Result<()>
db
&Db
required
Database instance for storing the public key reference
keys
&Keys
required
Nostr keypair to save
return
Result<()>
Ok(()) if successful, or an error if storage fails

load_keys()

Loads all keypairs from the database and system keychain.
pub fn load_keys(&mut self, db: &Db) -> Result<Vec<Keys>>
db
&Db
required
Database instance to query for public keys
return
Result<Vec<Keys>>
Vector of successfully loaded Keys objects

Behavior

  1. Queries the database for all saved public keys
  2. For each public key, retrieves the corresponding private key from the system keychain
  3. Constructs Keys objects from the private keys
  4. Stores the loaded keys in the loaded_keys field
  5. Returns the vector of loaded keys
If a private key cannot be retrieved from the keychain (e.g., due to permission issues), that key is skipped with an error log, and loading continues for remaining keys.

delete_key()

Removes a keypair from both the database and system keychain.
pub fn delete_key(&mut self, db: &Db, key: &Keys) -> Result<()>
db
&Db
required
Database instance for removing the public key reference
key
&Keys
required
Keypair to delete
return
Result<()>
Ok(()) if successful, or an error if deletion fails

Behavior

  1. Removes the public key from the database pubkeys table
  2. Deletes the private key from the system keychain
  3. Removes the keys from the loaded_keys vector in memory

unwrap_gift_wrap()

Decrypts and unwraps a gift-wrapped Nostr event (NIP-59).
pub fn unwrap_gift_wrap(&mut self, gift_wrap: &Event) -> Result<UnwrappedGift>
gift_wrap
&Event
required
The gift-wrapped event (kind 1059) to unwrap
return
Result<UnwrappedGift>
The unwrapped gift containing the rumor (unsigned event) and sender information

Behavior

  1. Extracts the target public key from the gift wrap’s p tag
  2. Searches loaded_keys for a matching keypair
  3. Uses the private key to decrypt the sealed event
  4. Unwraps the seal to reveal the rumor (the actual message content)
  5. Verifies that the seal signer matches the rumor’s pubkey
  6. Returns the UnwrappedGift containing the rumor and sender
This method will fail if:
  • The gift wrap does not contain a p tag
  • No loaded key matches the recipient public key
  • The cryptographic unwrapping fails
  • The seal signer doesn’t match the rumor pubkey (security check)

create_auth_event()

Creates a signed authentication event (NIP-42) for relay authentication.
pub fn create_auth_event(keys: &Keys, relay_url: &str, challenge: &str) -> Result<Event>
keys
&Keys
required
Keypair to sign the authentication event with
relay_url
&str
required
WebSocket URL of the relay requesting authentication
challenge
&str
required
Challenge string provided by the relay in its AUTH message
return
Result<Event>
A signed authentication event (kind 22242) containing relay and challenge tags
This is a static method that doesn’t require a mutable AccountManager instance.

validate_nsec()

Validates and parses a bech32-encoded Nostr private key (nsec).
pub fn validate_nsec(input: &str) -> Result<Keys, String>
input
&str
required
The nsec string to validate (e.g., “nsec1…”)
return
Result<Keys, String>
A Keys object if valid, or an error message string if invalid

Return values

  • Ok(Keys) - Valid nsec, returns the parsed keypair
  • Err("Please enter a private key") - Empty input
  • Err("Invalid nsec format") - Invalid bech32 encoding or incorrect format
This is a standalone function, not a method on AccountManager.

Example usage

Setting up accounts on first run

use hoot::account_manager::AccountManager;
use hoot::db::Db;

let mut account_manager = AccountManager::new();
let db = Db::new(db_path)?;

// Generate and save a new keypair
let keys = account_manager.generate_new_keys_and_save(&db)?;
println!("New public key: {}", keys.public_key().to_hex());
println!("New private key (nsec): {}", keys.secret_key().to_bech32()?);

Loading existing accounts

let mut account_manager = AccountManager::new();
let keys = account_manager.load_keys(&db)?;

println!("Loaded {} account(s)", keys.len());
for key in &keys {
    println!("  - {}", key.public_key().to_hex());
}

Importing an existing key

use hoot::account_manager::validate_nsec;

// Validate user input
match validate_nsec(&user_input) {
    Ok(keys) => {
        // Save the imported keys
        account_manager.save_keys(&db, &keys)?;
        println!("Successfully imported key");
    }
    Err(e) => {
        eprintln!("Invalid key: {}", e);
    }
}

Unwrapping received messages

// Received a gift-wrapped event from a relay
if event.kind == Kind::GiftWrap {
    match account_manager.unwrap_gift_wrap(&event) {
        Ok(unwrapped) => {
            println!("From: {}", unwrapped.sender);
            println!("Message ID: {}", unwrapped.rumor.id);
            println!("Content: {}", unwrapped.rumor.content);
            
            // Store the unwrapped rumor in the database
            db.store_event(&event, Some(&unwrapped), recipient_pubkey)?;
        }
        Err(e) => {
            eprintln!("Failed to unwrap: {}", e);
            // May not be for us, or we don't have the key
        }
    }
}

Authenticating with a relay (NIP-42)

let relay_url = "wss://relay.example.com";
let challenge = "random-challenge-string";

// Create auth event
let auth_event = AccountManager::create_auth_event(
    &keys,
    relay_url,
    &challenge
)?;

// Send to relay
relay_pool.send_auth(relay_url, auth_event)?;

Deleting an account

// Remove a keypair and all associated data
account_manager.delete_key(&db, &keys_to_remove)?;
println!("Account deleted from keychain and database");

Security considerations

  • Private keys never leave secure storage except when in memory during active use
  • Platform-specific keychains provide OS-level encryption and access control
  • Gift wrap verification ensures the seal signer matches the rumor pubkey before accepting
  • Automatic cleanup removes keys from memory when AccountManager is dropped
  • MailMessage - For creating gift-wrapped messages
  • RelayPool - For authentication and message transmission
  • Database - For storing public key references

Build docs developers (and LLMs) love