web-push-libs / webpush-java

Web Push library for Java
MIT License
323 stars 112 forks source link

Documentation Improvements #8

Open thihara opened 8 years ago

thihara commented 8 years ago

Just a few things that aren't clear at once by reading the documentation is what the PublicKey and userAuth are.

I believe userAuth is the key generated by the push messaging service from the browser side. Is the PublicKey somehow our public key?

Also please confirm that the PushService's default constructor is to be used with non-chrome browsers (Firefox at the moment.). Seem to be the case but I'd prefer to confirm and update the docs.

martijndwars commented 8 years ago

Both userPublicKey and userAuth are generated by the browser. More details can be found in the specification. Specifically, userPublicKey and userAuth correspond to the following two keys:

  • Generate a new P-256 ECDH key pair. Store the private key in an internal slot associated with the subscription; this value must not be made available to applications. The public key is also stored in an internal slot and can be retrieved by calling the getKey method of the PushSubscription with an argument of p256dh.
  • Generate a new authentication secret, which is a sequence of octets as defined in [WEBPUSH-ENCRYPTION]. Store the authentication secret in an internal slot associated with the subscription. This key can be retrieved by calling the getKey method of the PushSubscription with an argument of auth.

I use the following JavaScript code to send both keys to the server. Note that this snippet uses the Fetch API, which is an experimental technology, but you probably get the idea.

    var key = subscription.getKey ? subscription.getKey('p256dh') : '';
    var auth = subscription.getKey ? subscription.getKey('auth') : '';

    return fetch('/profile/subscription', {
        credentials: 'include',
        headers: {
            'Content-Type': 'application/json',
            'X-CSRF-TOKEN': $('meta[name="_csrf"]').attr('content')
        },
        method: 'POST',
        body: JSON.stringify({
            endpoint: subscription.endpoint,
            key: key ? btoa(String.fromCharCode.apply(null, new Uint8Array(key))) : '',
            auth: auth ? btoa(String.fromCharCode.apply(null, new Uint8Array(auth))) : ''
        })
    });

The full code for registering a service worker, subscribing to push messages, and sending the keys to the server can be found here. I'll try to update the README to make things more clear.

martijndwars commented 8 years ago

Also please confirm that the PushService's default constructor is to be used with non-chrome browsers (Firefox at the moment.). Seem to be the case but I'd prefer to confirm and update the docs.

Chrome's Push API uses Google Cloud Messaging (GCM) and GCM requires an API key on the server (and gcm_sender_id and manifest on the client, full setup instructions here). As the Web Push Protocol matures Chrome will move towards using the Web Push Protocol and the API key will become superfluous. I'll update the README to reflect this information.

TL;DR. You are correct. If you are only targeting FireFox, you can use the no-args constructor.

eliihen commented 8 years ago

@MartijnDwars Thank you for the example of the javascript code, it saved me a lot of time. Recommend you put it in some documentation somewhere :)

However, could you please also post the way you parse the key serverside? I am doing the following, but no luck:

final byte[] publicBytes = Base64.decode(input.getUserPubKey());
final X509EncodedKeySpec pubKeySpec = new X509EncodedKeySpec(publicBytes);

// Get ECDH algorithm from bouncyCastle
final KeyFactory keyFactory = KeyFactory.getInstance("ECDH", BouncyCastleProvider.PROVIDER_NAME);
final PublicKey userPublicKey = keyFactory.generatePublic(pubKeySpec);

On the last line it throws the following:

java.security.spec.InvalidKeySpecException: encoded key spec not recognised
    at org.bouncycastle.jcajce.provider.asymmetric.util.BaseKeyFactorySpi.engineGeneratePublic(Unknown Source)
    at org.bouncycastle.jcajce.provider.asymmetric.ec.KeyFactorySpi.engineGeneratePublic(Unknown Source)
    at java.security.KeyFactory.generatePublic(KeyFactory.java:328)

I used your method of base64 encoding the key before sending it, and one examle of a key looks like this:

BPta2KYHoeh15Cro4ZbM3TbGV6FOhL9kzJ66+YtxS17J4gToS3LmOSVVOhHzIyux62Lq4+NzAXUvoD3HwJptMDg=

I am using oracle jdk 8

martijndwars commented 8 years ago

Hi @esphen,

I first send the subscription to the server (i.e. the endpoint, key, and auth in push.js). When I need to send a notification, I retrieve the key and auth as follows. Note the getAuthAsBytes and getUserPublicKey methods (you'll probably be interested in the latter).

import org.bouncycastle.jce.ECNamedCurveTable;
import org.bouncycastle.jce.spec.ECNamedCurveParameterSpec;
import org.bouncycastle.jce.spec.ECPublicKeySpec;
import org.bouncycastle.math.ec.ECPoint;

class Subscription {
    ... // Getters and setters for e.g. endpoint

    public void setAuth(String auth) {
        this.auth = auth;
    }

    public String getAuth() {
        return auth;
    }

    // Convert the base64 auth string to byte[]
    public byte[] getAuthAsBytes() {
        return Base64.getDecoder().decode(getAuth());
    }

    public void setKey(String key) {
        this.key = key;
    }

    public String getKey() {
        return key;
    }

    // Convert the base64 key string to byte[]
    public byte[] getKeyAsBytes() {
        return Base64.getDecoder().decode(getKey());
    }

    // Convert the string key to a PublicKey object
    public PublicKey getUserPublicKey() throws NoSuchAlgorithmException, InvalidKeySpecException, NoSuchProviderException {
        KeyFactory kf = KeyFactory.getInstance("ECDH", "BC");
        ECNamedCurveParameterSpec ecSpec = ECNamedCurveTable.getParameterSpec("secp256r1");
        ECPoint point = ecSpec.getCurve().decodePoint(getKeyAsBytes());
        ECPublicKeySpec pubSpec = new ECPublicKeySpec(point, ecSpec);

        return kf.generatePublic(pubSpec);
    }
}

To send a notification I first construct a notification object as follows:

        new Notification(
            subscription.getEndpoint(),
            subscription.getUserPublicKey(),
            subscription.getAuthAsBytes(),
            payload
        );

and then I send it using:

pushService.send(notification);
eliihen commented 8 years ago

Thanks! It seems to work as intended now.

@MartijnDwars would you be willing to merge a doc folder with a UsageExample.md, detailing how to use the library from the user agent, all the way to the backend? I could write a suggestion based on the information in this thread.

martijndwars commented 8 years ago

@MartijnDwars would you be willing to merge a doc folder with a UsageExample.md, detailing how to use the library from the user agent, all the way to the backend? I could write a suggestion based on the information in this thread.

Definitely. I'm open to PRs! :)

nkmittal commented 5 years ago

I am using the code exactly as mentioned in your "https://github.com/web-push-libs/webpush-java/wiki/Usage-Example". I am getting error specifically when generating pubic key in the following code (calling Java APIs from my Scala code):

val kf: KeyFactory = KeyFactory.getInstance("ECDH", BouncyCastleProvider.PROVIDER_NAME)
val ecSpec: ECNamedCurveParameterSpec = ECNamedCurveTable.getParameterSpec("prime256v1")
val point: ECPoint = ecSpec.getCurve().decodePoint(Base64.getUrlDecoder().decode(v.keys.p256dh))
val pubSpec: ECPublicKeySpec = new ECPublicKeySpec(point, ecSpec)
kf.generatePublic(pubSpec)

I am using 1.62 version for bcprov-jdk15on and bcpkix-jdk15on. I also tried ECNamedCurveTable.getParameterSpec("secp256r1") but same error. Error:

2019-06-14 06:47:52,331 [error] a.d.TaskInvocation - Unknown KeySpec type: org.bouncycastle.jce.spec.ECPublicKeySpec
java.security.spec.InvalidKeySpecException: Unknown KeySpec type: org.bouncycastle.jce.spec.ECPublicKeySpec
    at org.bouncycastle.jce.provider.asymmetric.ec.KeyFactory.engineGeneratePublic(Unknown Source)
    at java.security.KeyFactory.generatePublic(KeyFactory.java:328)