1NIP-552======34Android Signer Application5--------------------------67`draft` `optional`89## Rationale1011This 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.1213## Terminology1415- **user**: A person trying to use Nostr.16- **client**: A user-facing application that sends signing requests.17- **signer**: An Android application that holds the user's private key and answers requests from _client_.18- **user-pubkey**: The public key representing _user_, returned by `get_public_key`.19- **package name**: The Android package name of the _signer_ (e.g. `com.example.signer`), used by _client_ to address subsequent requests.2021All pubkeys in this NIP are in hex format.2223## Communication methods2425A _client_ can talk to the _signer_ in three ways:2627- **Intents** — used by Android clients. The _signer_ opens, the _user_ approves or rejects the request manually, and the result is returned to _client_.28- **Content Resolver** — used by Android clients. Runs in the background without opening the _signer_, but only works for permissions the _user_ has previously chosen to remember.29- **Web** — used by web clients. The result is returned via a `callbackUrl` or copied to the clipboard.3031## Setup3233To detect and call the _signer_, an Android _client_ must declare the `nostrsigner` scheme in its `AndroidManifest.xml`:3435```xml36<queries>37 <intent>38 <action android:name="android.intent.action.VIEW" />39 <category android:name="android.intent.category.BROWSABLE" />40 <data android:scheme="nostrsigner" />41 </intent>42</queries>43```4445It can then check whether a _signer_ is installed:4647```kotlin48fun isExternalSignerInstalled(context: Context): Boolean {49 val intent = Intent(Intent.ACTION_VIEW, Uri.parse("nostrsigner:"))50 return context.packageManager.queryIntentActivities(intent, 0).isNotEmpty()51}52```5354## Initiating a connection55561. _client_ sends a `get_public_key` request.572. _signer_ responds with the `user-pubkey` and its `package name`.583. _client_ stores both, and SHOULD NOT call `get_public_key` again while the _user_ stays logged in.594. _client_ addresses all further requests to that `package name`.6061A _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`:6263```json64[{ "type": "sign_event", "kind": 22242 }, { "type": "nip44_decrypt" }]65```6667## Methods6869Every request has a `type` (the method name) and a payload. The payload is passed differently per transport (see below) but its meaning is shared:7071| Method | Payload | Extra params | Result |72| ------------------- | --------------- | ------------------------- | ------------------------------- |73| `get_public_key` | _(empty)_ | `permissions` | `user-pubkey` |74| `sign_event` | event JSON | `current_user` | the signature, and signed event |75| `nip04_encrypt` | plaintext | `pubkey`, `current_user` | the ciphertext |76| `nip44_encrypt` | plaintext | `pubkey`, `current_user` | the ciphertext |77| `nip04_decrypt` | ciphertext | `pubkey`, `current_user` | the plaintext |78| `nip44_decrypt` | ciphertext | `pubkey`, `current_user` | the plaintext |79| `decrypt_zap_event` | event JSON | `current_user` | the decrypted event JSON |8081- `current_user` is the `user-pubkey` currently logged in to _client_, in hex format.82- `pubkey` is the public key of the other party used for encryption/decryption, in hex format.83- `id` is an optional client-chosen string echoed back in the result, used to match responses when several requests are sent without waiting.8485## Using Intents8687The payload is the `nostrsigner:` URI data; all other params are intent extras. The result is returned via `registerForActivityResult` / `rememberLauncherForActivityResult`.8889```kotlin90val intent = Intent(Intent.ACTION_VIEW, Uri.parse("nostrsigner:$payload")).apply {91 `package` = signerPackageName // omit only for get_public_key92 putExtra("type", "sign_event")93 putExtra("id", event.id) // optional, echoed back94 putExtra("current_user", userPubkey)95}96launcher.launch(intent)97```9899The result is read from the returned intent's extras:100101| Extra | Description |102| ---------- | -------------------------------------------------------- |103| `result` | the method result (pubkey, signature, ciphertext, etc.) |104| `id` | the `id` sent in the request |105| `event` | the signed event JSON (`sign_event` only) |106| `package` | the _signer_ package name (`get_public_key` only) |107| `rejected` | `true` when the _user_ rejected the request |108109A `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`.110111```kotlin112onResult = { result ->113 if (result.resultCode != Activity.RESULT_OK) {114 // signer error (e.g. crash)115 } else if (result.data?.getBooleanExtra("rejected", false) == true) {116 // user rejected the request117 } else {118 val value = result.data?.getStringExtra("result")119 }120}121```122123To 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.124125```kotlin126intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP)127128// signer answers with:129intent.putExtra("results", listOf(Result(package = signerPackageName, result = signature, id = intentId)).toJson())130```131132## Using Content Resolver133134The _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]`:135136```kotlin137val result = context.contentResolver.query(138 Uri.parse("content://com.example.signer.SIGN_EVENT"),139 listOf(eventJson, "", loggedInUserPubkey),140 null, null, null,141)142```143144The 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:145146- the _user_ did not enable "remember my choice" for this request,147- the `user-pubkey` is not present in the _signer_,148- the request `type` is not recognized, or149- the _user_ chose to always reject this request (a `rejected` column is returned — _client_ SHOULD NOT then fall back to an Intent).150151```kotlin152if (result == null) return153if (result.getColumnIndex("rejected") > -1) return154if (result.moveToFirst()) {155 val value = result.getString(result.getColumnIndex("result"))156 // for sign_event also: result.getColumnIndex("event")157}158```159160Clients SHOULD store the `user-pubkey` locally and avoid calling `get_public_key` after the user is logged in.161162## Using Web Applications163164Web 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:165166```167nostrsigner:<payload>?type=<method>&pubkey=<hex_pubkey>&callbackUrl=https://example.com/?result=168```169170Query parameters:171172- `type` — the method name.173- `pubkey` — the other party's hex pubkey (encryption/decryption methods).174- `callbackUrl` — where the _signer_ appends the result. If omitted, the result is copied to the clipboard.175- `returnType` — `signature` or `event`.176- `compressionType` — `none` (default) or `gzip`. With `gzip` the returned event is `"Signer1"` + Base64(gzip(event json)); useful because intents and URLs have length limits.177178```js179window.href = `nostrsigner:${eventJson}?compressionType=none&returnType=signature&type=sign_event&callbackUrl=https://example.com/?event=`;180```181182> Consider using [NIP-46: Nostr Remote Signing](46.md) 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.183184### Example185186```html187<!DOCTYPE html>188<html lang="en">189<head>190 <meta charset="UTF-8">191 <meta name="viewport" content="width=device-width, initial-scale=1.0">192 <title>Document</title>193</head>194<body>195 <h1>Test</h1>196197 <script>198 window.onload = function() {199 var params = new URL(window.location.href).searchParams;200 var result = params.get("event");201 if (result) alert(result);202203 var json = { kind: 1, content: "test" };204 var encodedJson = encodeURIComponent(JSON.stringify(json));205 var anchor = document.createElement("a");206 anchor.href = `nostrsigner:${encodedJson}?compressionType=none&returnType=signature&type=sign_event&callbackUrl=https://example.com/?event=`;207 anchor.textContent = "Open External Signer";208 document.body.appendChild(anchor);209 }210 </script>211</body>212</html>213```