Identity and external signers
A local identity is an account-scoped authority. Import exactly 32 secret
bytes once, clear the caller buffer, retain the opaque identity for the
authenticated session, and call the idempotent erase() only on logout or
authority loss. Normal language ownership handles ordinary cleanup.
Local identity lifecycle
- Rust
- Kotlin
- Android
- Swift
- TypeScript
let identity = LocalIdentity::from_secret_bytes(&secret)?;
secret.fill(0);
let author = identity.public_key();
// Use throughout this account session.
drop(identity); // logout; Rust ownership drops the secret-bearing state
Use LocalIdentity in Rust. It is not cloneable and relies on deterministic
Rust ownership rather than an erase() method. Put it in an Option when an
account session needs to take and drop authority explicitly.
LocalIdentityHandle is the coarse generated-binding owner, not the preferred
native API.
val identity = SoftchatIdentity.import(secret)
secret.fill(0)
val author = identity.publicKey
identity.erase()
identity.erase() // safe and idempotent
Do not use use {}: an identity is not a per-operation closeable resource.
class AccountSession(secret: ByteArray) {
val identity = SoftchatIdentity.import(secret)
init { secret.fill(0) }
fun logout() = identity.erase()
}
Acquire bytes from the app’s secure authority. The SDK does not own Android Keystore policy or account persistence.
let identity = try SoftchatIdentity(secretKey: secret)
secret.resetBytes(in: secret.indices)
let author = try identity.publicKey
identity.erase()
ARC owns normal lifetime. erase() is an explicit authority transition, not
an alternative to ARC.
const identity = SoftchatIdentity.import(secret);
secret.fill(0);
const author = identity.publicKey;
identity.erase();
identity.erase();
Do not retain the original Uint8Array; imported authority remains inside the
opaque Wasm-backed object.
- Rust
- Kotlin
- Android
- Swift
- TypeScript
The public-key accessor is cached in foreign facades. Its benchmark is below the useful clock floor and is recorded only as an API-shape check.
- Rust
- Kotlin
- Android
- Swift
- TypeScript
External signing
Use an external signer when a secret must remain in hardware, a host service, an OS key store, or a browser extension. Capability discovery is explicit: event signing, NIP-44 encryption, and NIP-44 decryption are independent.
The host callback receives a bounded request and returns a signature or cryptographic result. Softchat completes and validates the event or message; the callback must not duplicate event-ID construction.
- Rust
- Kotlin
- Android
- Swift
- TypeScript
Implement ExternalSigner, declare ExternalSignerCapabilities, then call
sign_event_with_external, encrypt_with_external, or
decrypt_with_external. Return ExternalSignerFailure::Cancelled for user
cancellation and Rejected for a provider refusal.
let event = sign_event_with_external(&provider, draft)?;
let encrypted = encrypt_with_external(&provider, &recipient, "hello")?;
let plaintext = decrypt_with_external(&provider, &encrypted)?;
The authored Kotlin facade currently exposes the explicit event-signing request/completion path:
val request = Softchat.prepareExternalEventSigning(publicKey, draft)
val signatureHex = provider.signEventId(request.eventId.hex)
val event = request.complete(signatureHex)
Keep provider capability discovery in app-owned ExternalSignerCapabilities.
External NIP-44 callbacks are currently native-Rust-only.
Bridge BiometricPrompt, Keystore, or a remote signer in the app layer. Keep
activity and cancellation lifecycle in Android, then complete the bounded
ExternalEventSigningRequest only after the provider returns a signature.
let request = try prepareExternalEventSigning(
publicKey: publicKey,
draft: draft
)
let signature = try provider.sign(eventID: request.eventID)
let event = try request.complete(signatureHex: signature.hex)
Keep Secure Enclave, LocalAuthentication, and extension transport outside the SDK. The authored Swift facade currently exposes event signing; external NIP-44 callbacks are native-Rust-only.
const event = signEventWithExternal({
publicKey,
capabilities: {
signEvents: true,
nip44Encrypt: false,
nip44Decrypt: false,
},
sign: ({ draft }) => extension.signEvent(draft),
}, draft);
This callback is synchronous in the current facade. Resolve asynchronous extension UX before entering the SDK operation.
Safety checklist
- Never log, stringify, clone broadly, or persist secret bytes.
- Never use example fixture keys for an account.
- Never cache derived conversation keys to optimize a benchmark.
- Treat an erased identity as permanently invalid; create a new one after reauthentication.
- Keep one consistent caller/actor when an application permits concurrent access.