Paso 1 de 6
Encuentra el firmante
Una app de Android declara el esquema nostrsigner en las queries de su manifest y comprueba que alguna app maneja intents nostrsigner:.
NIP-55
BorradorComportamiento
Permite que las apps de Android (y las páginas web abiertas en Android) pidan a una app firmante separada que firme eventos y cifre o descifre mensajes, para que solo el firmante tenga la clave secreta. Las solicitudes pasan por intents, un content resolver en segundo plano o enlaces nostrsigner:.
Edita el formulario o el JSON: ambos se mantienen sincronizados, la insignia indica si el resultado es válido y, al tocar cualquier campo, se explica qué hace.
Paso 1 de 6
Una app de Android declara el esquema nostrsigner en las queries de su manifest y comprueba que alguna app maneja intents nostrsigner:.
Listo. Presiona reproducir o avanza un paso para enviar el primer mensaje.
| # | De | Para | Mensaje |
|---|---|---|---|
| 1 | App de Nostr | App firmante | La app abre un intent nostrsigner: con type get_public_key. Puede listar permisos para preautorizar, como firmar kind 22242 o nip44_decrypt. |
| 2 | Usuario | App firmante | Se abre el firmante y el usuario elige qué cuenta compartir y qué permisos recordar. |
| 3 | App firmante | App de Nostr | El firmante devuelve la pubkey del usuario en result y su nombre de paquete en package. La app guarda ambos y dirige todas las solicitudes posteriores a ese paquete. |
| 4 | App de Nostr | App firmante | El JSON del evento va en la URI nostrsigner:; type, un id opcional y current_user van como extras del intent. |
| 5 | Usuario | App firmante | El usuario aprueba o rechaza. Un rechazo vuelve como RESULT_OK con rejected = true; cualquier otro código de resultado significa que el firmante falló. |
| 6 | App firmante | App de Nostr | Extras: result (la firma), id (repetido) y event (el JSON del evento firmado). |
| 7 | App de Nostr | App firmante | Para permisos recordados, la app consulta content://<package>.SIGN_EVENT con selectionArgs [payload, pubkey, current_user] y el firmante nunca se abre. |
| 8 | App firmante | App de Nostr | Un cursor con result (y event para sign_event). null o una columna rejected significa "no recordado" o "rechazar siempre"; no recurras a un intent después de un rechazo. |
| 9 | App de Nostr | App firmante | Las páginas web no pueden recibir resultados de intents, así que el firmante añade el resultado a callbackUrl o lo copia al portapapeles. compressionType gzip devuelve "Signer1" + base64(gzip(event)). |
Ningún capítulo del curso cubre este NIP todavía. El editor de arriba es la forma más rápida de empezar.
Firma remota a través de relays; recomendada para clientes web.
El equivalente en extensión de navegador para escritorio.
nip44_encrypt y nip44_decrypt producen y leen payloads NIP-44.
Las apps suelen preautorizar la firma de eventos de autenticación kind 22242.
El texto de las especificaciones se cita en inglés del repositorio nostr-protocol/nips. Nuestras explicaciones y la interfaz están traducidas. Código fuente en GitHub en el commit 0046368
This NIP describes a method for 2-way communication between an Android signer and any Nostr client running on the same device, so that the client never needs to handle the user's private key. The signer is an Android application; the client may be another Android application or a web page.
get_public_key.com.example.signer), used by client to address subsequent requests.All pubkeys in this NIP are in hex format.
A client can talk to the signer in three ways:
callbackUrl or copied to the clipboard.To detect and call the signer, an Android client must declare the nostrsigner scheme in its AndroidManifest.xml:
<queries>
<intent>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="nostrsigner" />
</intent>
</queries>
It can then check whether a signer is installed:
fun isExternalSignerInstalled(context: Context): Boolean {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("nostrsigner:"))
return context.packageManager.queryIntentActivities(intent, 0).isNotEmpty()
}
get_public_key request.user-pubkey and its package name.get_public_key again while the user stays logged in.package name.A client MAY pass a list of permissions with get_public_key so the user can pre-authorize methods (used by the Content Resolver to answer in the background). Each permission is an object with a type (a method name) and, for sign_event, an optional kind:
[{ "type": "sign_event", "kind": 22242 }, { "type": "nip44_decrypt" }]
Every request has a type (the method name) and a payload. The payload is passed differently per transport (see below) but its meaning is shared:
| Method | Payload | Extra params | Result |
|---|---|---|---|
get_public_key |
(empty) | permissions |
user-pubkey |
sign_event |
event JSON | current_user |
the signature, and signed event |
nip04_encrypt |
plaintext | pubkey, current_user |
the ciphertext |
nip44_encrypt |
plaintext | pubkey, current_user |
the ciphertext |
nip04_decrypt |
ciphertext | pubkey, current_user |
the plaintext |
nip44_decrypt |
ciphertext | pubkey, current_user |
the plaintext |
decrypt_zap_event |
event JSON | current_user |
the decrypted event JSON |
current_user is the user-pubkey currently logged in to client, in hex format.pubkey is the public key of the other party used for encryption/decryption, in hex format.id is an optional client-chosen string echoed back in the result, used to match responses when several requests are sent without waiting.The payload is the nostrsigner: URI data; all other params are intent extras. The result is returned via registerForActivityResult / rememberLauncherForActivityResult.
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("nostrsigner:$payload")).apply {
`package` = signerPackageName // omit only for get_public_key
putExtra("type", "sign_event")
putExtra("id", event.id) // optional, echoed back
putExtra("current_user", userPubkey)
}
launcher.launch(intent)
The result is read from the returned intent's extras:
| Extra | Description |
|---|---|
result |
the method result (pubkey, signature, ciphertext, etc.) |
id |
the id sent in the request |
event |
the signed event JSON (sign_event only) |
package |
the signer package name (get_public_key only) |
rejected |
true when the user rejected the request |
A resultCode other than Activity.RESULT_OK means the signer failed (e.g. it crashed), not that the user rejected the request. When the user rejects, the signer returns RESULT_OK with a rejected extra set to true.
onResult = { result ->
if (result.resultCode != Activity.RESULT_OK) {
// signer error (e.g. crash)
} else if (result.data?.getBooleanExtra("rejected", false) == true) {
// user rejected the request
} else {
val value = result.data?.getStringExtra("result")
}
}
To sign multiple events without reopening the signer for each one, client adds the flags below and the signer returns an array of results. The signer must declare android:launchMode="singleTop" for this to work.
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP)
// signer answers with:
intent.putExtra("results", listOf(Result(package = signerPackageName, result = signature, id = intentId)).toJson())
The client queries content://<package-name>.<TYPE> (e.g. SIGN_EVENT, NIP44_ENCRYPT). The query's selectionArgs carry the payload and params in the order [payload, pubkey, current_user]:
val result = context.contentResolver.query(
Uri.parse("content://com.example.signer.SIGN_EVENT"),
listOf(eventJson, "", loggedInUserPubkey),
null, null, null,
)
The cursor returns a result column (and, for sign_event, an event column with the signed event JSON). The query returns null, or a rejected column is present, when:
user-pubkey is not present in the signer,type is not recognized, orrejected column is returned — client SHOULD NOT then fall back to an Intent).if (result == null) return
if (result.getColumnIndex("rejected") > -1) return
if (result.moveToFirst()) {
val value = result.getString(result.getColumnIndex("result"))
// for sign_event also: result.getColumnIndex("event")
}
Clients SHOULD store the user-pubkey locally and avoid calling get_public_key after the user is logged in.
Web clients can't receive a result from an intent, so the signer returns the result via a callbackUrl (appended to the URL) or, if none is given, by copying it to the clipboard. The request is a nostrsigner: URL whose path is the payload and whose query string carries the params:
nostrsigner:<payload>?type=<method>&pubkey=<hex_pubkey>&callbackUrl=https://example.com/?result=
Query parameters:
type — the method name.pubkey — the other party's hex pubkey (encryption/decryption methods).callbackUrl — where the signer appends the result. If omitted, the result is copied to the clipboard.returnType — signature or event.compressionType — none (default) or gzip. With gzip the returned event is "Signer1" + Base64(gzip(event json)); useful because intents and URLs have length limits.window.href = `nostrsigner:${eventJson}?compressionType=none&returnType=signature&type=sign_event&callbackUrl=https://example.com/?event=`;
Consider using NIP-46: Nostr Remote Signing for web applications. With the approach here the web client can't call the signer in the background, so the user sees a popup for every request.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<h1>Test</h1>
<script>
window.onload = function() {
var params = new URL(window.location.href).searchParams;
var result = params.get("event");
if (result) alert(result);
var json = { kind: 1, content: "test" };
var encodedJson = encodeURIComponent(JSON.stringify(json));
var anchor = document.createElement("a");
anchor.href = `nostrsigner:${encodedJson}?compressionType=none&returnType=signature&type=sign_event&callbackUrl=https://example.com/?event=`;
anchor.textContent = "Open External Signer";
document.body.appendChild(anchor);
}
</script>
</body>
</html>