Nostr NIPS 55
NIP-55
Android Signer Application
draft
optional
This NIP describes a method for 2-way communication between an Android signer and any Nostr client on Android. The Android signer is an Android Application and the client can be a web client or an Android application.
Usage for Android applications
The Android signer uses Intents and Content Resolvers to communicate between applications.
To be able to use the Android signer in your application you should add this to your AndroidManifest.xml:
<queries>
<intent>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="nostrsigner" />
</intent>
</queries>
Then you can use this function to check if there’s a signer application installed:
fun isExternalSignerInstalled(context: Context): Boolean {
val intent =
Intent().apply {
action = Intent.ACTION_VIEW
data = Uri.parse("nostrsigner:")
}
val infos = context.packageManager.queryIntentActivities(intent, 0)
return infos.size > 0
}
Using Intents
To get the result back from the Signer Application you should use registerForActivityResult
or rememberLauncherForActivityResult
in Kotlin. If you are using another framework check the documentation of your framework or a third party library to get the result.
val launcher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult(),
onResult = { result ->
if (result.resultCode != Activity.RESULT_OK) {
Toast.makeText(
context,
"Sign request rejected",
Toast.LENGTH_SHORT
).show()
} else {
val signature = activityResult.data?.getStringExtra("signature")
// Do something with signature ...
}
}
)
Create the Intent using the nostrsigner scheme:
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("nostrsigner:$content"))
Set the Signer package name:
intent.`package` = "com.example.signer"
Send the Intent:
launcher.launch(intent)
Methods
get_public_key
params:
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("nostrsigner:")) intent.`package` = "com.example.signer" intent.putExtra("type", "get_public_key") // You can send some default permissions for the user to authorize for ever val permissions = listOf( Permission( type = "sign_event", kind = 22242 ), Permission( type = "nip44_decrypt" ) ) intent.putExtra("permissions", permissions.toJson()) context.startActivity(intent)
result:
If the user approved intent it will return the pubkey in the signature field
val pubkey = intent.data?.getStringExtra("signature") // The package name of the signer application val packageName = intent.data?.getStringExtra("package")
sign_event
params:
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("nostrsigner:$eventJson")) intent.`package` = "com.example.signer" intent.putExtra("type", "sign_event") // To handle results when not waiting between intents intent.putExtra("id", event.id) // Send the current logged in user pubkey intent.putExtra("current_user", pubkey) context.startActivity(intent)
result:
If the user approved intent it will return the signature, id and event fields
val signature = intent.data?.getStringExtra("signature") // The id you sent val id = intent.data?.getStringExtra("id") val signedEventJson = intent.data?.getStringExtra("event")
nip04_encrypt
params:
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("nostrsigner:$plaintext")) intent.`package` = "com.example.signer" intent.putExtra("type", "nip04_encrypt") // to control the result in your application in case you are not waiting the result before sending another intent intent.putExtra("id", "some_id") // Send the current logged in user pubkey intent.putExtra("current_user", account.keyPair.pubkey) // Send the hex pubkey that will be used for encrypting the data intent.putExtra("pubkey", pubkey) context.startActivity(intent)
result:
If the user approved intent it will return the signature and id fields
val encryptedText = intent.data?.getStringExtra("signature") // the id you sent val id = intent.data?.getStringExtra("id")
nip44_encrypt
params:
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("nostrsigner:$plaintext")) intent.`package` = "com.example.signer" intent.putExtra("type", "nip44_encrypt") // to control the result in your application in case you are not waiting the result before sending another intent intent.putExtra("id", "some_id") // Send the current logged in user pubkey intent.putExtra("current_user", account.keyPair.pubkey) // Send the hex pubkey that will be used for encrypting the data intent.putExtra("pubkey", pubkey) context.startActivity(intent)
result:
If the user approved intent it will return the signature and id fields
val encryptedText = intent.data?.getStringExtra("signature") // the id you sent val id = intent.data?.getStringExtra("id")
nip04_decrypt
params:
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("nostrsigner:$encryptedText")) intent.`package` = "com.example.signer" intent.putExtra("type", "nip04_decrypt") // to control the result in your application in case you are not waiting the result before sending another intent intent.putExtra("id", "some_id") // Send the current logged in user pubkey intent.putExtra("current_user", account.keyPair.pubkey) // Send the hex pubkey that will be used for decrypting the data intent.putExtra("pubkey", pubkey) context.startActivity(intent)
result:
If the user approved intent it will return the signature and id fields
val plainText = intent.data?.getStringExtra("signature") // the id you sent val id = intent.data?.getStringExtra("id")
nip44_decrypt
params:
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("nostrsigner:$encryptedText")) intent.`package` = "com.example.signer" intent.putExtra("type", "nip04_decrypt") // to control the result in your application in case you are not waiting the result before sending another intent intent.putExtra("id", "some_id") // Send the current logged in user pubkey intent.putExtra("current_user", account.keyPair.pubkey) // Send the hex pubkey that will be used for decrypting the data intent.putExtra("pubkey", pubkey) context.startActivity(intent)
result:
If the user approved intent it will return the signature and id fields
val plainText = intent.data?.getStringExtra("signature") // the id you sent val id = intent.data?.getStringExtra("id")
get_relays
params:
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("nostrsigner:")) intent.`package` = "com.example.signer" intent.putExtra("type", "get_relays") // to control the result in your application in case you are not waiting the result before sending another intent intent.putExtra("id", "some_id") // Send the current logged in user pubkey intent.putExtra("current_user", account.keyPair.pubkey) context.startActivity(intent)
result:
If the user approved intent it will return the signature and id fields
val relayJsonText = intent.data?.getStringExtra("signature") // the id you sent val id = intent.data?.getStringExtra("id")
decrypt_zap_event
params:
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("nostrsigner:$eventJson")) intent.`package` = "com.example.signer" intent.putExtra("type", "decrypt_zap_event") // to control the result in your application in case you are not waiting the result before sending another intent intent.putExtra("id", "some_id") // Send the current logged in user pubkey intent.putExtra("current_user", account.keyPair.pubkey) context.startActivity(intent)
result:
If the user approved intent it will return the signature and id fields
val eventJson = intent.data?.getStringExtra("signature") // the id you sent val id = intent.data?.getStringExtra("id")
Using Content Resolver
To get the result back from Signer Application you should use contentResolver.query in Kotlin. If you are using another framework check the documentation of your framework or a third party library to get the result.
If the user did not check the “remember my choice” option, the pubkey is not in Signer Application or the signer type is not recognized the contentResolver
will return null
For the SIGN_EVENT type Signer Application returns two columns “signature” and “event”. The column event is the signed event json
For the other types Signer Application returns the column “signature”
If the user chose to always reject the event, signer application will return the column “rejected” and you should not open signer application
Methods
get_public_key
params:
val result = context.contentResolver.query( Uri.parse("content://com.example.signer.GET_PUBLIC_KEY"), listOf("login"), null, null, null )
result:
Will return the pubkey in the signature column
if (result == null) return if (result.moveToFirst()) { val index = it.getColumnIndex("signature") if (index < 0) return val pubkey = it.getString(index) }
sign_event
params:
val result = context.contentResolver.query( Uri.parse("content://com.example.signer.SIGN_EVENT"), listOf("$eventJson", "", "${logged_in_user_pubkey}"), null, null, null )
result:
Will return the signature and the event columns
if (result == null) return if (result.moveToFirst()) { val index = it.getColumnIndex("signature") val indexJson = it.getColumnIndex("event") val signature = it.getString(index) val eventJson = it.getString(indexJson) }
nip04_encrypt
params:
val result = context.contentResolver.query( Uri.parse("content://com.example.signer.NIP04_ENCRYPT"), listOf("$plainText", "${hex_pub_key}", "${logged_in_user_pubkey}"), null, null, null )
result:
Will return the signature column
if (result == null) return if (result.moveToFirst()) { val index = it.getColumnIndex("signature") val encryptedText = it.getString(index) }
nip44_encrypt
params:
val result = context.contentResolver.query( Uri.parse("content://com.example.signer.NIP44_ENCRYPT"), listOf("$plainText", "${hex_pub_key}", "${logged_in_user_pubkey}"), null, null, null )
result:
Will return the signature column
if (result == null) return if (result.moveToFirst()) { val index = it.getColumnIndex("signature") val encryptedText = it.getString(index) }
nip04_decrypt
params:
val result = context.contentResolver.query( Uri.parse("content://com.example.signer.NIP04_DECRYPT"), listOf("$encryptedText", "${hex_pub_key}", "${logged_in_user_pubkey}"), null, null, null )
result:
Will return the signature column
if (result == null) return if (result.moveToFirst()) { val index = it.getColumnIndex("signature") val encryptedText = it.getString(index) }
nip44_decrypt
params:
val result = context.contentResolver.query( Uri.parse("content://com.example.signer.NIP44_DECRYPT"), listOf("$encryptedText", "${hex_pub_key}", "${logged_in_user_pubkey}"), null, null, null )
result:
Will return the signature column
if (result == null) return if (result.moveToFirst()) { val index = it.getColumnIndex("signature") val encryptedText = it.getString(index) }
get_relays
params:
val result = context.contentResolver.query( Uri.parse("content://com.example.signer.GET_RELAYS"), listOf("${logged_in_user_pubkey}"), null, null, null )
result:
Will return the signature column
if (result == null) return if (result.moveToFirst()) { val index = it.getColumnIndex("signature") val relayJsonText = it.getString(index) }
decrypt_zap_event
params:
val result = context.contentResolver.query( Uri.parse("content://com.example.signer.DECRYPT_ZAP_EVENT"), listOf("$eventJson", "", "${logged_in_user_pubkey}"), null, null, null )
result:
Will return the signature column
if (result == null) return if (result.moveToFirst()) { val index = it.getColumnIndex("signature") val eventJson = it.getString(index) }
Usage for Web Applications
Since web applications can’t receive a result from the intent, you should add a modal to paste the signature or the event json or create a callback url.
If you send the callback url parameter, Signer Application will send the result to the url.
If you don’t send a callback url, Signer Application will copy the result to the clipboard.
You can configure the returnType
to be signature or event.
Android intents and browser urls have limitations, so if you are using the returnType
of event consider using the parameter compressionType=gzip that will return “Signer1” + Base64 gzip encoded event json
Methods
get_public_key
params:
window.href = `nostrsigner:?compressionType=none&returnType=signature&type=get_public_key&callbackUrl=https://example.com/?event=`;
sign_event
params:
window.href = `nostrsigner:${eventJson}?compressionType=none&returnType=signature&type=sign_event&callbackUrl=https://example.com/?event=`;
nip04_encrypt
params:
window.href = `nostrsigner:${plainText}?pubkey=${hex_pub_key}&compressionType=none&returnType=signature&type=nip04_encrypt&callbackUrl=https://example.com/?event=`;
nip44_encrypt
params:
window.href = `nostrsigner:${plainText}?pubkey=${hex_pub_key}&compressionType=none&returnType=signature&type=nip44_encrypt&callbackUrl=https://example.com/?event=`;
nip04_decrypt
params:
window.href = `nostrsigner:${encryptedText}?pubkey=${hex_pub_key}&compressionType=none&returnType=signature&type=nip04_decrypt&callbackUrl=https://example.com/?event=`;
nip44_decrypt
params:
window.href = `nostrsigner:${encryptedText}?pubkey=${hex_pub_key}&compressionType=none&returnType=signature&type=nip44_decrypt&callbackUrl=https://example.com/?event=`;
get_relays
params:
window.href = `nostrsigner:?compressionType=none&returnType=signature&type=get_relays&callbackUrl=https://example.com/?event=`;
decrypt_zap_event
params:
window.href = `nostrsigner:${eventJson}?compressionType=none&returnType=signature&type=decrypt_zap_event&callbackUrl=https://example.com/?event=`;
Example
<!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 url = new URL(window.location.href);
var params = url.searchParams;
if (params) {
var param1 = params.get("event");
if (param1) alert(param1)
}
let json = {
kind: 1,
content: "test"
}
let encodedJson = encodeURIComponent(JSON.stringify(json))
var newAnchor = document.createElement("a");
newAnchor.href = `nostrsigner:${encodedJson}?compressionType=none&returnType=signature&type=sign_event&callbackUrl=https://example.com/?event=`;
newAnchor.textContent = "Open External Signer";
document.body.appendChild(newAnchor)
}
</script>
</body>
</html>
Source: nostr-protocol/nips/55.md version: 7bb8997 2024-07-02T16:02:31+08:00