Skip to main content

Nostr values and cryptography

This family establishes the trust boundary for keys, identifiers, events, and encrypted payloads. Parse once, keep the validated value, and preserve exact raw tag arrays—including trailing empty fields.

NIP-19 identifiers

Operation contractnostr.nip19

Parse and encode bounded NIP-19 display identifiers.

Inputs and exact bounds
  • at most 5,000 identifier characters
  • at most 8 relay hints
Output
A validated identifier retaining unknown TLVs.
Trust and authentication
Display identifiers are untrusted input, not authentication.
Stable errors
invalid_nip19_identifier · handling and retry rules
Ownership, lifecycle, and execution rules
Ownership
Rust owns protocol semantics, Android account SQLite/use-case state, and optional native relay I/O; the host owns account lifecycle, HTTP/media/background effects, files, and UI.
Retry
Retry only host transport or explicitly retryable session work; validation and authentication failures are permanent for the same input.
Lifecycle
Language ownership is automatic; explicitly cancel or erase only at the operation-specific authority boundary.
Concurrency
Serialize mutable session capabilities; immutable values follow the language type system.
Cancellation
The host owns async cancellation; use cancel, shutdown, close, or erase only where exposed.
Exact public symbols
Nip19Identifier::parseNip19Identifier::encode

Nip19Identifier parses and encodes the key, event, profile, and relay-hint forms used by Softchat. Mixed-case Bech32 is rejected. Unknown TLVs are retained where the typed representation supports them, so a read/write round trip does not silently discard extensions.

let identifier = Nip19Identifier::parse(encoded)?;
let canonical = identifier.encode()?;

Author a NIP-01 event

Operation contractnostr.author-event

Create, sign, parse, and verify exact NIP-01 events.

Inputs and exact bounds
  • portable timestamp at most 9,007,199,254,740,991
  • canonical event JSON at most 512 KiB
Output
An exact verified signed event.
Trust and authentication
Received events are untrusted until ID recomputation and signature verification succeed.
Stable errors
invalid_event_json, invalid_event_id, invalid_event_signature, event_too_large · handling and retry rules
Ownership, lifecycle, and execution rules
Ownership
Rust owns protocol semantics, Android account SQLite/use-case state, and optional native relay I/O; the host owns account lifecycle, HTTP/media/background effects, files, and UI.
Retry
Retry only host transport or explicitly retryable session work; validation and authentication failures are permanent for the same input.
Lifecycle
Language ownership is automatic; explicitly cancel or erase only at the operation-specific authority boundary.
Concurrency
Serialize mutable session capabilities; immutable values follow the language type system.
Cancellation
The host owns async cancellation; use cancel, shutdown, close, or erase only where exposed.
Exact public symbols
NostrEventDraft::newLocalIdentity::sign_eventSignedNostrEvent::from_json

The caller supplies createdAt; the SDK never hides a clock in signing. Signing computes the canonical event ID from [0, pubkey, created_at, kind, tags, content] and produces a verified immutable event.

let draft = NostrEventDraft::new(
1_700_000_000,
NostrEventKind::SHORT_TEXT_NOTE,
vec![NostrTag::new([
"p".to_owned(),
recipient.to_hex(),
String::new(),
String::new(),
])?],
"hello",
)?;
let event = identity.sign_event(draft)?;
let received = SignedNostrEvent::from_json(&event.to_json()?)?;

Verify before use

Parsing a signed event checks canonical lowercase hex, the 512-KiB bound, portable kind and timestamp bounds, the recomputed event ID, and its Schnorr signature. A parsed event is authenticated; raw JSON is not.

NIP-44 v2

Operation contractnostr.nip44

Encrypt and authenticate NIP-44 v2 messages.

Inputs and exact bounds
  • writer UTF-8 plaintext at most 65,535 bytes
  • reader plaintext at most 327,680 bytes
  • encoded payload at most 512 KiB
Output
A NIP-44 message or authenticated UTF-8 plaintext.
Trust and authentication
Ciphertext and claimed sender remain untrusted until decrypt succeeds.
Stable errors
empty_plaintext, plaintext_too_large, ciphertext_too_large, decryption_failed · handling and retry rules
Ownership, lifecycle, and execution rules
Ownership
Rust owns protocol semantics, Android account SQLite/use-case state, and optional native relay I/O; the host owns account lifecycle, HTTP/media/background effects, files, and UI.
Retry
Retry only host transport or explicitly retryable session work; validation and authentication failures are permanent for the same input.
Lifecycle
Language ownership is automatic; explicitly cancel or erase only at the operation-specific authority boundary.
Concurrency
Serialize mutable session capabilities; immutable values follow the language type system.
Cancellation
The host owns async cancellation; use cancel, shutdown, close, or erase only where exposed.
Exact public symbols
LocalIdentity::encrypt_utf8LocalIdentity::decrypt_utf8Nip44EncryptedMessage

Encrypt UTF-8 with the sender identity and the validated recipient public key. The immutable result includes the sender public key and NIP-44 payload. Decryption maps authentication, padding, and decoding failures to one redacted error.

let encrypted = alice.encrypt_utf8(&bob_public, plaintext)?;
let plaintext = bob.decrypt_utf8(&encrypted)?;

The three accepted payload sizes show fixed crossing/crypto cost and scaling. Each card includes the frozen old-library comparison when an equivalent baseline exists.

Readers accept both legacy and current extended-length encodings. Writers remain compatible with released clients. Never expose distinct failures that could become a padding or authentication oracle.