greenart7c3 / Amber

MIT License
92 stars 5 forks source link

SIGN_EVENT on web applications? #4

Closed benalleng closed 7 months ago

benalleng commented 9 months ago

For the SIGN_EVENT type amber returns two columns "signature" and "event". The column event is the signed event json

Does the web intent have support for returning the signed event and not only the signature as there currently aren't signerTypes for web?

greenart7c3 commented 9 months ago

Right now for web applications you can press the show details button and click copy raw json I'm still testing how to send data back to the web clients Maybe we can use a url callback

newAnchor.href = `intent:${encodedJson}#Intent;scheme=nostrsigner;S.type=sign_event;S.callbackUrl=https://example.com/externalSigner?event=;end`;

Something like this

greenart7c3 commented 9 months ago

Changed the readme for web applications I'm not a web dev so i don't know if this is the best aproach to sending intents to a android app I'm generating a new build of the application with the changes from the example in the readme

https://github.com/greenart7c3/Amber#support-for-web-applications

benalleng commented 9 months ago

Sweet! been testing out amber for support in a non-nostr web app and having a callbackurl should really reduce the friction of all the clipboard shenanigans

benalleng commented 9 months ago

Working on a PoC if you want to take a look at the possibilities for Amber! https://github.com/saving-satoshi/saving-satoshi/pull/485

Preview Flow

dipunm commented 9 months ago

I think we should be using an encoded payload like base64 or web safe base64. https://base64.guru/standards/base64url

You never know what compatibility and security issues we could have with random characters in a hyperlink.

greenart7c3 commented 9 months ago

Sending the event json can lead to a large url that breaks the limit of the browsers So to fix this i'm now sending it gzip compressed

you can decompress it with something like:

const decodedData = atob(encodedString);
const gzipedDataArray = Uint8Array.from(decodedData, c => c.charCodeAt(0))
const ungzipedData = pako.ungzip(gzipedDataArray);

console.log( new TextDecoder().decode(ungzipedData))
dipunm commented 9 months ago

🤔 sounds like a little versioning or flagging might be worth doing here.

Being able to choose or change the compression type in the future or even upgrade how the data is encoded seems like a potential future requirement.

That, or we just try to get a shit load of eyes on this so that we can be confident with a single solution.

Something like a single beginning character would suffice cashu uses base64 encoded tokens and prefixes with "cashu" and "A" where A is the version number.

So you get cashuAey1.....

We may not need the prefix aspect, or at least, I don't understand it's usefulness if we're using a custom http scheme.

dipunm commented 9 months ago

How you handle a callback URL is also interesting. Needs some testing, but if you pass back data using the anchor #, then you have no data size limits, but the server doesn't see the data.

This might actually be a good thing. What we want here is not to call the callback URL using a http client, but to launch a browser so that the webpage can do the broadcasting for you. This will let clients that are single page applications with no backends work without requiring server logic hosting costs.

I haven't thought too hard about this, there's no real security reason to do this as far as I know, but it's an option if it becomes useful.

dipunm commented 9 months ago

There are two things there btw:

  1. Callback by launching a web browser to let client side broadcasting of messages instead of requiring a web server.

  2. Using # in the URL (e.g. https://example.com/externalSigner#event=;end;) to allow larger payloads with the caveat that a server cannot read the data (which is typically nice when you want to provide sensitive data to a webpage to handle, but not the server that processed the request).

They don't rely on one another.

greenart7c3 commented 9 months ago

Using # also doesn't work. Android intent also breaks and won't let you send the event. We need to send it with gzip or other compression or send only the signature back. Maybe we can use something in the url callback like if contains #event then send the event else send only the signature

dipunm commented 9 months ago

Yeah, sorry, I applied the # to the callback URL only, not the intent itself.

Sorry if that caused some confusion, but we are on the same page.

greenart7c3 commented 9 months ago

Here's what I did for now: I'm using SignerA... where A is the version number then in the website you have to decode the url with something like decodeURIComponent and then decompress the event. We can also add a option in the intent so clients that don't send large events can use Amber without compressing the data or change it to don't compress the data by default. S.compression=none

newAnchor.href = `intent:${encodedJson}#Intent;scheme=nostrsigner;S.type=sign_event;S.compression=none;S.callbackUrl=https://example.com/externalSigner?event=;end`;

which approach do you think is better?

ildella commented 8 months ago

Hi. I am upgrading my webapp Nostr/Amber signin integration with this new stuff (before I had the user manually paste the signed json)

Now my intent is:

`intent:${encodeURIComponent(eventString)}#Intent;scheme=nostrsigner;S.compressionType=none;S.returnType=signature;S.type=sign_event;S.callbackUrl=${callbackUrl};end`

And I get called back and get an event string like:

c8374b2debd8a5bd5a496d6cbb3098deaef4524df5e461f490813cec9340d3e76c25e8b80c9d58000c9c23701dd629f1b9aaf24419f68c73538739171c4374ab

But I cannot extract anything from that... How can I get the signed event string from that?

Thanks.

benalleng commented 8 months ago

just from first glance it looks like you are just getting the signature S.returnType=signature; try removing that, I think the whole event is the default functionality...?

benalleng commented 8 months ago

Here is what I believe is the relevant code

} else if (intentData.callBackUrl != null) {
        if (intentData.returnType == ReturnType.SIGNATURE) {
            val intent = Intent(Intent.ACTION_VIEW)
            intent.data = Uri.parse(intentData.callBackUrl + Uri.encode(value))
            context.startActivity(intent)
        } else {
            if (intentData.compression == CompressionType.GZIP) {
                // Compress the string using GZIP
                val byteArrayOutputStream = ByteArrayOutputStream()
                val gzipOutputStream = GZIPOutputStream(byteArrayOutputStream)
                gzipOutputStream.write(event.toByteArray())
                gzipOutputStream.close()

                // Convert the compressed data to Base64
                val compressedData = byteArrayOutputStream.toByteArray()
                val encodedString = Base64.getEncoder().encodeToString(compressedData)
                val intent = Intent(Intent.ACTION_VIEW)
                intent.data = Uri.parse(intentData.callBackUrl + Uri.encode("Signer1$encodedString"))
                context.startActivity(intent)
// With no returnType or CompressionType you should get the raw event back
            } else {
                val intent = Intent(Intent.ACTION_VIEW)
                intent.data = Uri.parse(intentData.callBackUrl + Uri.encode(event))
                context.startActivity(intent)
            }
        }
ildella commented 8 months ago

just from first glance it looks like you are just getting the signature S.returnType=signature; try removing that, I think the whole event is the default functionality...?

yes, just tried that, actually changed to "event" and now I can parse the hex string.

benalleng commented 7 months ago

With the addition of the base64 encoding of events I feel as though this issue can safely be closed @greenart7c3 what do you think?

greenart7c3 commented 7 months ago

Sure, I'll close this for now