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
nostr.nip19Parse 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
- Rust
- Android
- Swift
- TypeScript
Nip19Identifier::parseNip19Identifier::encodeNip19Identifier.parseNip19Identifier.encodePublicKey.encodeNpubNip19Identifier.init(encoded:)Nip19Identifier.encoded()Nip19Identifier.parseNip19Identifier.stringifyNip19Identifier 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.
- Rust
- Android
- Swift
- TypeScript
let identifier = Nip19Identifier::parse(encoded)?;
let canonical = identifier.encode()?;
val identifier = Nip19Identifier.parse(encoded)
val canonical = identifier.encode()
Parse incoming intents or QR data on a bounded worker before updating UI state.
let identifier = try Nip19Identifier(encoded: encoded)
let canonical = try identifier.encoded()
const identifier = Nip19Identifier.parse(encoded);
const canonical = Nip19Identifier.stringify(identifier);
Author a NIP-01 event
nostr.author-eventCreate, 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
- Rust
- Android
- Swift
- TypeScript
NostrEventDraft::newLocalIdentity::sign_eventSignedNostrEvent::from_jsonNostrEventDraftSoftchatIdentity.signSignedNostrEvent.parseNostrEventDraft.initSoftchatIdentity.signSignedNostrEvent.init(json:)NostrEventDraft.createSoftchatIdentity.signSignedNostrEvent.parseThe 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.
- Rust
- Android
- Swift
- TypeScript
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()?)?;
val draft = NostrEventDraft(
createdAt = 1_700_000_000,
kind = NostrEventKind.SHORT_TEXT_NOTE,
tags = listOf(NostrTag.of("p", recipient.hex, "", "")),
content = "hello",
)
val event = identity.sign(draft)
val received = SignedNostrEvent.parse(event.toJson())
Persist event.toJson() or a complete lossless schema.
let draft = try NostrEventDraft(
createdAt: 1_700_000_000,
kind: .shortTextNote,
tags: [.init("p", recipient.hex, "", "")],
content: "hello"
)
let event = try identity.sign(draft)
let received = try SignedNostrEvent(json: event.canonicalJSON())
const draft = NostrEventDraft.create({
createdAt: 1_700_000_000,
kind: NostrEventKind.shortTextNote,
tags: [["p", recipient, "", ""]],
content: "hello",
});
const event = identity.sign(draft);
const received = SignedNostrEvent.parse(SignedNostrEvent.stringify(event));
- Rust
- Android
- Swift
- TypeScript
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.
- Rust
- Android
- Swift
- TypeScript
NIP-44 v2
nostr.nip44Encrypt 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
- Rust
- Android
- Swift
- TypeScript
LocalIdentity::encrypt_utf8LocalIdentity::decrypt_utf8Nip44EncryptedMessageSoftchatIdentity.encryptSoftchatIdentity.decryptNip44EncryptedMessageSoftchatIdentity.encryptSoftchatIdentity.decryptNip44EncryptedMessageSoftchatIdentity.encryptSoftchatIdentity.decryptNip44EncryptedMessageEncrypt 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.
- Rust
- Android
- Swift
- TypeScript
let encrypted = alice.encrypt_utf8(&bob_public, plaintext)?;
let plaintext = bob.decrypt_utf8(&encrypted)?;
val encrypted = alice.encrypt(bob.publicKey, plaintext)
val plaintext = bob.decrypt(encrypted)
The AAR automatically selects the narrow JNI byte-array path. Do not call cryptographic operations from the main thread.
let encrypted = try alice.encrypt(plaintext, for: bob.publicKey)
let plaintext = try bob.decrypt(encrypted)
const encrypted = alice.encrypt(bob.publicKey, plaintext);
const plaintext = bob.decrypt(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.
- Rust
- Android
- Swift
- TypeScript
- Rust
- Android
- Swift
- TypeScript
- Rust
- Android
- Swift
- TypeScript
- Rust
- Android
- Swift
- TypeScript
- Rust
- Android
- Swift
- TypeScript
- Rust
- Android
- Swift
- TypeScript
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.