Generate a VAPID keys in Java and pass them to JavaScript PushManager

4k views Asked by At

I’m trying to use web push notifications with the web push protocol in my app. In order to use the Push API with VAPID I need an applicationServerKey.

The PushManager subscribe method takes a VAPID key (public key alone) as a parameter and will give a subscription end point and keys to push messages.

To generate VAPID keys, I have been using node.js (google web-push package) and openssl till now. But in my use case VAPID keys should be generated within Java and passed to JavaScript to subscribe from the browser.

I am trying with the code below in Java to generate VAPID keys. I am able to create keys successfully but when I pass the generated public key (base64-encoded string), the subscribe method returns an error saying:

Unable to register service worker. DOMException: Failed to execute 'subscribe' on 'PushManager': The provided applicationServerKey is not valid..

Please help me resolve this issue. Below is my Java code:

ECNamedCurveParameterSpec parameterSpec = 
ECNamedCurveTable.getParameterSpec("prime256v1");
KeyPairGenerator keyPairGenerator = 
KeyPairGenerator.getInstance("ECDH", "BC");
keyPairGenerator.initialize(parameterSpec);
KeyPair serverKey = keyPairGenerator.generateKeyPair();

PrivateKey priv = serverKey.getPrivate();
PublicKey pub = serverKey.getPublic();`
System.out.println(Base64.toBase64String(pub.getEncoded()));
4

There are 4 answers

0
jkb016 On

With bountycastle you can generate vapid keys with this code :

    ECNamedCurveParameterSpec parameterSpec = ECNamedCurveTable.getParameterSpec("prime256v1");
    KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("ECDH", "BC");
    keyPairGenerator.initialize(parameterSpec);
    KeyPair keyPair = keyPairGenerator.generateKeyPair();
    ECPublicKey publicKey = (ECPublicKey) keyPair.getPublic();
    ECPrivateKey privateKey = (ECPrivateKey) keyPair.getPrivate();
    String publicKeyString = Base64.getUrlEncoder().withoutPadding().encodeToString(publicKey.getQ().getEncoded(false));
    System.out.println(publicKeyString);
    String privateKeyString = Base64.getUrlEncoder().withoutPadding().encodeToString(privateKey.getD().toByteArray());
    System.out.println(privateKeyString);
0
Horcrux7 On

This is a solution with poor Java 8 that handle the random length of the BigIntegers.

    KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance( "EC" );
    keyPairGenerator.initialize( new ECGenParameterSpec( "secp256r1" ), new SecureRandom() );
    KeyPair keyPair = keyPairGenerator.generateKeyPair();

    ECPublicKey publicKey = (ECPublicKey)keyPair.getPublic();
    ECPoint ecp = publicKey.getW();

    byte[] applicationServerKey = new byte[65];
    applicationServerKey[0] = 4;

    // copy getAffineX() to the target
    byte[] affine = ecp.getAffineX().toByteArray(); // typical 31 to 33 bytes
    int pos = 1;
    int off, length;
    off = affine.length - 32;
    if( off >=  0 ) {
        // there are leading zero values which we cut
        length = 32;
    } else {
        pos -= off;
        length = 32 + off;
        off = 0;
    }
    System.arraycopy( affine, off, applicationServerKey, pos, length );

    // copy getAffineY() to the target
    affine = ecp.getAffineY().toByteArray(); // typical 31 to 33 bytes
    pos = 33;
    off = affine.length - 32;
    if( off >=  0 ) {
        // there are leading zero values which we cut
        length = 32;
    } else {
        pos -= off;
        length = 32 + off;
        off = 0;
    }
    System.arraycopy( affine, off, applicationServerKey, pos, length );

    return Base64.getEncoder().encodeToString( applicationServerKey );

or more simpler with the Java buildin encoding:

    ECPublicKey publicKey = ...
    byte[] keyBytes = publicKey.getEncoded();
    // 26 -> X509 overhead, length ever 91, results in 65 bytes
    keyBytes = Arrays.copyOfRange( keyBytes, 26, 91 ); 

    return this.applicationServerKey = Base64.getEncoder().encodeToString( keyBytes );
1
user1498970 On

Having spent hours on this, I thought I would share the solution I worked out from gleaning several web sites, avoiding using Bouncy Castle which generated lots of other issues. Try the below:

KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC");
ECGenParameterSpec spec = new ECGenParameterSpec("secp256r1");
keyPairGenerator.initialize(spec, new SecureRandom()); 
KeyPair keyPair = keyPairGenerator.generateKeyPair();

ECPublicKey publicKey = (ECPublicKey)   keyPair.getPublic();
ECPoint ecp = publicKey.getW();

byte[] x = ecp.getAffineX().toByteArray();
byte[] y = ecp.getAffineY().toByteArray();
// Convert 04 to bytes
String s= "04";
int len = s.length();
byte[] firstBit = new byte[len / 2];
for (int i = 0; i < len; i += 2) 
{
    firstBit[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + 
Character.digit(s.charAt(i+1), 16));
}

ByteArrayOutputStream outputStream = new ByteArrayOutputStream( );
outputStream.write(firstBit);
outputStream.write(x);
outputStream.write(y);


publicKeyBytes = outputStream.toByteArray( );

Base64 encoder = new Base64(-1,null,true);
byte[] encodedBytes = encoder.encode(publicKeyBytes);
String publicKeyBase64 = new String(encodedBytes, StandardCharsets.UTF_8);
1
Mathi kumar On

Please refer below link for answer from MartijnDwars. https://github.com/web-push-libs/webpush-java/issues/30

you can use Utils.savePublicKey to convert your Java-generated PublicKey to a byte[]. This byte[] is then passed to the PushManager.subscribe method.

It may be more convenient to base64 encode the byte[] in Java and base64 decode the string in JavaScript. For example, after generating the keypair in Java:

KeyPair keyPair = generateKeyPair();
byte[] publicKey = Utils.savePublicKey((ECPublicKey) keyPair.getPublic());
String publicKeyBase64 = BaseEncoding.base64Url().encode(publicKey);
System.out.println("PublicKey = " + publicKeyBase64);
// PublicKey = BPf36QAqZNNvvnl9kkpTDerXUOt6Nm6P4x9GEvmFVFKgVyCVWy24KUTs6wLQtbV2Ug81utbNnx86_vZzXDyrl88=

Then in JavaScript:

function subscribe() {
    const publicKey = base64UrlToUint8Array('BPf36QAqZNNvvnl9kkpTDerXUOt6Nm6P4x9GEvmFVFKgVyCVWy24KUTs6wLQtbV2Ug81utbNnx86_vZzXDyrl88=');

    navigator.serviceWorker.ready.then(function (serviceWorkerRegistration) {
        serviceWorkerRegistration.pushManager.subscribe({
            userVisibleOnly: true,
            applicationServerKey: publicKey
        })
        .then(function (subscription) {
            return sendSubscriptionToServer(subscription);
        })
        .catch(function (e) {
            if (Notification.permission === 'denied') {
                console.warn('Permission for Notifications was denied');
            } else {
                console.error('Unable to subscribe to push.', e);
            }
        });
    });
}

function base64UrlToUint8Array(base64UrlData) {
    const padding = '='.repeat((4 - base64UrlData.length % 4) % 4);
    const base64 = (base64UrlData + padding)
        .replace(/\-/g, '+')
        .replace(/_/g, '/');

    const rawData = atob(base64);
    const buffer = new Uint8Array(rawData.length);

    for (let i = 0; i < rawData.length; ++i) {
        buffer[i] = rawData.charCodeAt(i);
    }

    return buffer;
}