跟之前介紹的對Android GCM推播很像(如何向GCM Server傳送資料、如何接收GCM Server發送的Registration ID訊息,以php、Java及JSP為例,以php、Java及JSP為例),事實上Google Chrome Web Push目前也是利用GCM服務來實現的。
Google Chrome Web Push可以寫死彈跳訊息的code在client端(註冊時會記錄),也可以動態訊息給client,不過因為安全的問題,必須使用Google規定的方式來加密訊息。
在這邊就來一步步展示如何做一個簡單的Google Chrome Web Push實現:
- 申請建立GCM服務
在這裡有兩個方式,
一個是透過由Google最近在推的FCM(Firebase Cloud Messaging)建立專案及服務。
一個是透過Google Developer Console來建立專案及服務。- 首先先講透過FCM的方式:
- 先到Firebase官網登入,如果你有Google帳戶且登入的話,進去就是登入狀態了。
- 建立一個專案(Project),或匯入(import)一個你已經建立的Google專案(它可以import自己的Google專案)。
- 到專案的設定頁(請按左上角的齒輪圖案),在上方的Tab中選擇"Cloud Messaging",就可以在"專案金鑰"中看到自己的 "伺服器金鑰 (Server Key)" 及 "寄件者ID (Sender ID)",這兩個值之後會用到。
- 再來講透過GCM的方式:
- 到Google Developer Console建立一個專案,在專案的設定頁可以看到此專案的"專案編號 (Project Number)",對應到FCM的寄件者ID。
- 到API管理員啟用Google Cloud Messaging的Google API
- 然後到"API管理員"那頁去選擇左邊列出來的"憑證"選項,建立一個API金鑰(API Key),對應到FCM的伺服器金鑰。
- 首先先講透過FCM的方式:
- 建立可以註冊及取消註冊的網頁
- 我們建立一個index.html,裡面放一個被Disbale的按鈕(寫著Subscribe),當檢查可以註冊(subscribe)推播時,按鈕會變成可按,按了可以註冊推播。
- 當註冊推播成功後,在畫面上顯示註冊相關訊息,是一個JSON格式的字串,其中Registration_ID:endpoint是一個網址,最後面的字串是此User的Registration_ID (在https://android.googleapis.com/gcm/send//後面)
p256dh: 加密訊息要用的其中之一key
auth: 加密訊息要用的其中之一key
以上三個值都要記住或傳給server紀錄起來。 - index.html的內容如下:
<!DOCTYPE html> <html> <head> <title>推送通知 codelab</title> <link rel="manifest" href="manifest.json"> </head> <body> <h1>推送通知 codelab</h1> <p>這網頁必須使用HTTPS或通過localhost。</p> <p id="pushsubscription">None</p> <button disabled>Subscribe</button> <script src="js/main.js"></script> </body> </html>
- 其中index.html有引入的manifest.json內容如下:
{ "name": 隨傳取", "gcm_sender_id": "就是FCM的SenderID或GCM的Project Number" }
- 其中index.html有引入的main.js (因為沒有做document的ready事件監聽,請放到<body>的最下面)內容如下:
var reg; var sub; var isSubscribed = false; //取得button DOM var subscribeButton = document.querySelector('button'); if ('serviceWorker' in navigator) { //如果有serviceWorker可以使用 console.log('Service Worker is supported'); //註冊sw.js,其中有實作與推播相關的行為 navigator.serviceWorker.register('sw.js').then(function() { return navigator.serviceWorker.ready; }).then(function(serviceWorkerRegistration) { //可以開始註冊推播,將讓冊按鈕變成able狀態 reg = serviceWorkerRegistration; subscribeButton.disabled = false; console.log('Service Worker is ready :^)', reg); }).catch(function(error) { console.log('Service Worker Error :^(', error); }); } //設定註夕按鈕的click行為 subscribeButton.addEventListener('click', function() { if (isSubscribed) { unsubscribe(); } else { subscribe(); } }); function subscribe() { reg.pushManager.subscribe({userVisibleOnly: true}). then(function(pushSubscription){ //註冊推播成功,印出推播相關訊息,改變註冊按鈕狀態 sub = pushSubscription; console.log('Subscribed! Endpoint:', sub.endpoint); subscribeButton.textContent = 'Unsubscribe'; isSubscribed = true; document.getElementById("pushsubscription").innerHTML = JSON.stringify(pushSubscription); }); } function unsubscribe() { sub.unsubscribe().then(function(event) { //取消註冊推播成功,印出推播相關訊息,改變註冊按鈕狀態 subscribeButton.textContent = 'Subscribe'; console.log('Unsubscribed!', event); isSubscribed = false; }).catch(function(error) { console.log('Error unsubscribing', error); subscribeButton.textContent = 'Subscribe'; }); }
- main.js中有引用的sw.js內容如下:
//設定推播相關設定 console.log('Started', self); //設推播的install事件要做的行為A self.addEventListener('install', function(event) { self.skipWaiting(); console.log('Installed', event); }); //設推播的activate事件要做的行為A self.addEventListener('activate', function(event) { console.log('Activated', event); }); //設推播的push事件要做的行為A self.addEventListener('push', function(event) { console.log('Push message', event); var title = 'Push message'; event.waitUntil( //產生Pop up的彈跳訊息事窗,桌機會出現在右下角 //其中,event.data.json()可以得到傳送的json字串(解密後) //myText是要傳的json中我設的一個key self.registration.showNotification(title, { body: event.data.json().myText, icon: 'images/icon.png', tag: 'my-tag' })); });
- 對想要傳送的客製訊息進行加密
Google規定如果要傳送自訂訊息時,必須對訊息內容加密,相關資料在這頁
加密需要三個東西:- 要加密的內容:一個JSON格式的字串
- p256dh : 只中一個Key
- auth : 另一個Key
- cipherText :加密後的訊息,要放在傳送的raw_data中
- encryptionHeader: : 要放在傳送的Encryption Header中,格式是 salt=XXXX
- cryptoKeyHeader: 要放在傳送的Crypto-KeyHeader中,格式是 dh=XXXX
這裡有幾點要注意的步驟:- 這裡我使用了以下lib
- 下載並引入httpcomponents-client-4.3.4。
- 到bouncycastle官網取得並下載bouncycastle的加密相關jar,並依照此頁說明的方式將jar檔放到 $JAVA_HOME$\jre\lib\ext 資料夾中,修改配置文件 jre\lib\security\java.security,在
security.provider.1=sun.security.provider.Sun
security.provider.2=sun.security.rsa.SunRsaSign
security.provider.3=com.sun.net.ssl.internal.ssl.Provider
security.provider.4=com.sun.crypto.provider.SunJCE
security.provider.5=sun.security.jgss.SunProvider
security.provider.6=com.sun.security.sasl.Provider
下面多加一條:
security.provider.請自己填入下一個數字=org.bouncycastle.jce.provider.BouncyCastleProvider - 稍微修改了lib裡面的程式碼,利用它們來取得加密的資料:
Constants.java :package webPush; import java.nio.charset.StandardCharsets; /** * Created by akarve on 4/26/16. */ public final class Constants { private Constants() { // no op } public static final byte[] CONTENT_ENCODING = "Content-Encoding: ".getBytes(StandardCharsets.UTF_8); public static final byte[] AESGCM128 = "aesgcm".getBytes(StandardCharsets.UTF_8); public static final byte[] NONCE = "nonce".getBytes(StandardCharsets.UTF_8); public static final byte[] P256 = "P-256".getBytes(StandardCharsets.UTF_8); public static final int GCM_TAG_LENGTH = 16; // in bytes public static final String SECP256R1 = "secp256r1"; public static final String HMAC_SHA256 = "HmacSHA256"; public static final byte NULL_BYTE = (byte) 0; public static final byte KEY_LENGTH_BYTE = (byte) 65; // This is always 65 // for our curve public static final int MAX_PAYLOAD_LENGTH = 4078; }
EllipticCurveKeyUtil.java :package webPush; import java.math.BigInteger; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.KeyFactory; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; import java.security.PublicKey; import java.security.SecureRandom; import java.security.Security; import java.security.interfaces.ECPrivateKey; import java.security.interfaces.ECPublicKey; import java.security.spec.ECPoint; import java.security.spec.ECPublicKeySpec; import java.security.spec.InvalidKeySpecException; import java.util.Base64; import javax.crypto.KeyAgreement; import org.apache.commons.codec.DecoderException; import org.bouncycastle.jce.ECNamedCurveTable; import org.bouncycastle.jce.ECPointUtil; import org.bouncycastle.jce.spec.ECNamedCurveParameterSpec; import org.bouncycastle.jce.spec.ECNamedCurveSpec; import org.bouncycastle.jce.spec.ECParameterSpec; import org.bouncycastle.jce.spec.ECPrivateKeySpec; import static org.apache.commons.codec.binary.Hex.decodeHex; /** * Created by akarve on 4/26/16. */ public class EllipticCurveKeyUtil { public static final String CRYPTO_TYPE_ECDH = "ECDH"; public static final String PROVIDER_BOUNCY_CASTLE = "BC"; private final KeyFactory _keyFactory; private final ECNamedCurveParameterSpec _ecNamedCurveParameterSpec; private final ECNamedCurveSpec _ecNamedCurveSpec; private final ECParameterSpec _ecParameterSpec; private final KeyPairGenerator _keyPairGenerator; public EllipticCurveKeyUtil() throws NoSuchProviderException, NoSuchAlgorithmException, InvalidAlgorithmParameterException { Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider()); _keyFactory = KeyFactory.getInstance(CRYPTO_TYPE_ECDH, PROVIDER_BOUNCY_CASTLE); _ecNamedCurveParameterSpec = ECNamedCurveTable.getParameterSpec(Constants.SECP256R1); // P256 // curve _ecNamedCurveSpec = new ECNamedCurveSpec(Constants.SECP256R1, _ecNamedCurveParameterSpec.getCurve(), _ecNamedCurveParameterSpec.getG(), _ecNamedCurveParameterSpec.getN()); _ecParameterSpec = ECNamedCurveTable.getParameterSpec(Constants.SECP256R1); // P256 // curve _keyPairGenerator = KeyPairGenerator.getInstance(CRYPTO_TYPE_ECDH, PROVIDER_BOUNCY_CASTLE); _keyPairGenerator.initialize(_ecParameterSpec, new SecureRandom()); } /** * Creates a ECDH keypair from the curve parameters for the p256 curve * * @param x * Affine X for the public key point * @param y * Affine Y for the public key point * @param s * S parameter for the private key * @return ECDH keypair * @throws NoSuchProviderException * @throws NoSuchAlgorithmException * @throws InvalidKeySpecException */ public KeyPair loadECKeyPair(BigInteger x, BigInteger y, BigInteger s) throws NoSuchProviderException, NoSuchAlgorithmException, InvalidKeySpecException { ECPoint ecPoint = new ECPoint(x, y); ECPublicKeySpec pubKeySpec = new ECPublicKeySpec(ecPoint, _ecNamedCurveSpec); ECPrivateKeySpec privateKeySpec = new ECPrivateKeySpec(s, _ecNamedCurveParameterSpec); ECPublicKey publicKey = (ECPublicKey) _keyFactory.generatePublic(pubKeySpec); ECPrivateKey privateKey = (ECPrivateKey) _keyFactory.generatePrivate(privateKeySpec); return new KeyPair(publicKey, privateKey); } /** * Generates a new keypair for ECDH using the p256 curve. Creating a new * keypair per request is expensive, but preferred * * @return ECDH Keypair * @throws NoSuchAlgorithmException * @throws InvalidAlgorithmParameterException * @throws NoSuchProviderException */ public KeyPair generateServerKeyPair() throws NoSuchAlgorithmException, InvalidAlgorithmParameterException, NoSuchProviderException { return _keyPairGenerator.generateKeyPair(); } /** * Converts the ECDH public key to bytes (has to be 65 bytes in length) * * @param publicKey * Public key for ECDH p256 curve * @return bytearray representation of key */ public byte[] publicKeyToBytes(final ECPublicKey publicKey) throws DecoderException { ECPoint point = publicKey.getW(); String x = point.getAffineX().toString(16); String y = point.getAffineY().toString(16); /* * Format is 04 followed by 32 bytes (64 hex) each for the X,Y * coordinates */ StringBuilder sb = new StringBuilder(); sb.append("04"); for (int i = 0; i < 64 - x.length(); i++) { sb.append(0); } sb.append(x); for (int i = 0; i < 64 - y.length(); i++) { sb.append(0); } sb.append(y); return decodeHex(sb.toString().toCharArray()); } /** * Converts the ECDH private key to bytes (has to be 32 bytes in length) * * @param privateKey * Private key for ECDH p256 curve * @return bytearray representation of private key */ public byte[] privateKeyToBytes(final ECPrivateKey privateKey) throws DecoderException { /* * Format is 32 bytes (64 hex) of S */ StringBuilder sb = new StringBuilder(); String s = privateKey.getS().toString(16); for (int i = 0; i < 64 - s.length(); i++) { sb.append(0); } sb.append(s); return decodeHex(sb.toString().toCharArray()); } /** * Creates a public key from the p256dh encoded using URL-safe Base64 * * @param p256dh * p256dh string * @return Public Key * @throws NoSuchProviderException * @throws NoSuchAlgorithmException * @throws InvalidKeySpecException */ public ECPublicKey loadP256Dh(final String p256dh) throws NoSuchProviderException, NoSuchAlgorithmException, InvalidKeySpecException { final byte[] p256dhBytes = Base64.getUrlDecoder().decode(p256dh); final ECPoint point = ECPointUtil.decodePoint(_ecNamedCurveSpec.getCurve(), p256dhBytes); ECPublicKeySpec pubKeySpec = new ECPublicKeySpec(point, _ecNamedCurveSpec); return (ECPublicKey) _keyFactory.generatePublic(pubKeySpec); } /** * Computes the shared secret for ECDH using the server keys and the client * public key * * @param serverKeys * Server keypair * @param clientPublicKey * Client public key * @return p256dh shared secret * @throws NoSuchAlgorithmException * @throws InvalidKeyException * @throws NoSuchProviderException */ public byte[] generateSharedSecret(final KeyPair serverKeys, final PublicKey clientPublicKey) throws NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException { KeyAgreement keyAgreement = KeyAgreement.getInstance(CRYPTO_TYPE_ECDH, PROVIDER_BOUNCY_CASTLE); keyAgreement.init(serverKeys.getPrivate()); keyAgreement.doPhase(clientPublicKey, true); return keyAgreement.generateSecret(); } }
PushApiUtil.java :package webPush; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; import java.security.SecureRandom; import java.util.Arrays; import java.util.Base64; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; import javax.crypto.Mac; import javax.crypto.NoSuchPaddingException; import javax.crypto.SecretKey; import javax.crypto.spec.GCMParameterSpec; import javax.crypto.spec.SecretKeySpec; /** * Created by akarve on 5/4/16. */ public class PushApiUtil { private static final SecureRandom RANDOM_BYTE_GENERATOR = new SecureRandom(); private PushApiUtil() { // no op } /** * Returns an info record. See sections 3.2 and 3.3 of * {https://tools.ietf.org/html/draft-ietf-httpbis-encryption-encoding-00} * * @param serverPublicKey * server public key * @param clientPublicKey * client public key * @param type * info type * @return * @throws IOException */ public static byte[] generateInfo(final byte[] serverPublicKey, final byte[] clientPublicKey, final byte[] type) throws IOException { // The start index for each element within the buffer is: // value | length | start | // --------------------------------------- // 'Content-Encoding: '| 18 | 0 | // type | l | 18 | // null byte | 1 | 18 + l | // 'P-256' | 5 | 19 + l | // info | 135 | 24 + l | ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); outputStream.write(Constants.CONTENT_ENCODING); // Append the string // ��?�Content-Encoding: // ��?? outputStream.write(type); // Append the |type| outputStream.write(Constants.NULL_BYTE); // Append a NULL-byte outputStream.write(Constants.P256); // Append the string ��?�P-256��?? // The context format is: // 0x00 || length(clientPublicKey) || clientPublicKey || // length(serverPublicKey) || serverPublicKey // The lengths are 16-bit, Big Endian, unsigned integers so take 2 bytes // each. // The keys should always be 65 bytes each. The format of the keys is // described in section 4.3.6 of the (sadly not freely linkable) ANSI // X9.62 // specification. outputStream.write(Constants.NULL_BYTE); // Append a NULL-byte outputStream.write(Constants.NULL_BYTE); // Append the length of the // recipient��?��s public key // (here |client_public|) outputStream.write(Constants.KEY_LENGTH_BYTE); // as a two-byte integer // in network byte // order. outputStream.write(clientPublicKey); // Append the raw bytes (65) of the // recipient��?��s // public key. outputStream.write(Constants.NULL_BYTE); // Append the length of the // sender��?��s public key // (here |server_public|) outputStream.write(Constants.KEY_LENGTH_BYTE); // as a two-byte integer // in network byte // order. outputStream.write(serverPublicKey); // Append the raw bytes (65) of the // sender��?��s // public key. return outputStream.toByteArray(); } public static String createEncryptionHeader(final byte[] salt) { // Encode |salt| using the URL-safe base64 encoding, store it in // |encoded_salt|. // Return the result of concatenating (��?�salt=��??, |encoded_salt|). return "salt=" + Base64.getUrlEncoder().encodeToString(salt); } public static String createCryptoKeyHeader(final byte[] serverPublic) { // Encode |server_public| using the URL-safe base64 encoding, store it // in |encoded_server_public|. // Return the result of concatenating (��?�dh=��??, // |encoded_server_public|). return "dh=" + Base64.getUrlEncoder().encodeToString(serverPublic); } /** * Performs an hkdf extract on the message and trims to (lengthToExtract) * * HMAC-based Extract-and-Expand Key Derivation Function (HKDF) * * This is used to derive a secure encryption key from a mostly-secure * shared secret. * * This is a partial implementation of HKDF tailored to our specific * purposes. In particular, for us the value of N will always be 1, and thus * T always equals HMAC-Hash(PRK, info | 0x01). * * See {https://www.rfc-editor.org/rfc/rfc5869.txt} * * @param secretKey * key to perform the HMAC with * @param salt * random salt bytes * @param messageToExtract * message to perform hkdf extract on * @param lengthToExtract * how much to trim the output by * @return hkdf extract * @throws NoSuchAlgorithmException * @throws InvalidKeyException */ public static byte[] hkdfExtract(final byte[] secretKey, final byte[] salt, final byte[] messageToExtract, final int lengthToExtract) throws NoSuchAlgorithmException, InvalidKeyException { Mac outerMac = Mac.getInstance(Constants.HMAC_SHA256); outerMac.init(new SecretKeySpec(salt, Constants.HMAC_SHA256)); byte[] outerResult = outerMac.doFinal(secretKey); Mac innerMac = Mac.getInstance(Constants.HMAC_SHA256); innerMac.init(new SecretKeySpec(outerResult, Constants.HMAC_SHA256)); byte[] message = new byte[messageToExtract.length + 1]; System.arraycopy(messageToExtract, 0, message, 0, messageToExtract.length); message[messageToExtract.length] = (byte) 1; byte[] innerResult = innerMac.doFinal(message); return Arrays.copyOf(innerResult, lengthToExtract); } /** * Encrypts a message such that it can be sent using the Web Push protocol. * You can find out more about the various pieces: - * {https://tools.ietf.org/html/draft-ietf-httpbis-encryption-encoding} - * {https://en.wikipedia.org/wiki/Elliptic_curve_Diffie%E2%80%93Hellman} - * {https://tools.ietf.org/html/draft-ietf-webpush-encryption} * * @param message * Message to encrypt * @param sharedSecret * Shared secret computed using the server keys and the client * public key * @param salt * 16 random bytes * @param contentEncryptionKeyInfo * @param nonceInfo * @param clientAuth * Client's auth (generated on the browser) * @return * @throws InvalidKeyException * @throws NoSuchAlgorithmException * @throws IllegalBlockSizeException * @throws InvalidAlgorithmParameterException * @throws BadPaddingException * @throws NoSuchProviderException * @throws NoSuchPaddingException */ public static String encryptPayload(final String message, final byte[] sharedSecret, final byte[] salt, final byte[] contentEncryptionKeyInfo, final byte[] nonceInfo, final byte[] clientAuth) throws InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, InvalidAlgorithmParameterException, BadPaddingException, NoSuchProviderException, NoSuchPaddingException { // Derive a Pseudo-Random Key (prk) that can be used to further derive // our // other encryption parameters. These derivations are described in // https://tools.ietf.org/html/draft-ietf-httpbis-encryption-encoding-00 final byte[] prk = hkdfExtract(sharedSecret, clientAuth, "Content-Encoding: auth\0".getBytes(StandardCharsets.UTF_8), 32); // Derive the Content Encryption Key final byte[] contentEncryptionKey = hkdfExtract(prk, salt, contentEncryptionKeyInfo, 16); // Derive the Nonce / iv final byte[] nonce = hkdfExtract(prk, salt, nonceInfo, 12); // Not adding any padding for now. First two bytes are reserved for // number of padding bytes (0) final byte[] record = ("\0\0" + message).getBytes(StandardCharsets.UTF_8); // Set |ciphertext| to the result of encrypting |record| with // AEAD_AES_128_GCM, using // the |content_encryption_key| as the key, the |nonce| as the nonce/IV, // and an authentication tag of 16 bytes. final byte[] ciphertext = encryptWithAESGCM128(nonce, contentEncryptionKey, record); // Encode the |ciphertext| using the URL-safe base64 encoding, store it // in |encoded_ciphertext|. return Base64.getEncoder().encodeToString(ciphertext); } /** * Encrypt the plaintext message using AES128/GCM * * @param nonce * The iv * @param contentEncryptionKey * The private key to use * @param record * The message to be encrypted * @return the encrypted payload * @throws NoSuchPaddingException * @throws NoSuchAlgorithmException * @throws NoSuchProviderException * @throws InvalidAlgorithmParameterException * @throws InvalidKeyException * @throws BadPaddingException * @throws IllegalBlockSizeException */ public static byte[] encryptWithAESGCM128(final byte[] nonce, final byte[] contentEncryptionKey, final byte[] record) throws NoSuchPaddingException, NoSuchAlgorithmException, NoSuchProviderException, IllegalArgumentException, InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException { if (record.length > Constants.MAX_PAYLOAD_LENGTH) { throw new IllegalArgumentException("Record is too big, dropping notification"); } Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding", "SunJCE"); SecretKey key = new SecretKeySpec(contentEncryptionKey, "AES"); GCMParameterSpec spec = new GCMParameterSpec(Constants.GCM_TAG_LENGTH * 8, nonce); cipher.init(Cipher.ENCRYPT_MODE, key, spec); return cipher.doFinal(record); } /** * Generates 16 cryptographically secure random bytes * * @return 16 byte salt */ public static byte[] generateSalt() { byte[] salt = new byte[16]; RANDOM_BYTE_GENERATOR.nextBytes(salt); return salt; } }
- .然後我參考使用範例寫了一個EncryptWebPushHelper.java,可以輸出上述加密後的三個結果並傳送加密的推播訊息給GCM伺服器:
EncryptWebPushHelper.java :package webPush; import java.io.IOException; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.KeyPair; import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; import java.security.interfaces.ECPublicKey; import java.security.spec.InvalidKeySpecException; import java.util.Base64; import javax.crypto.BadPaddingException; import javax.crypto.IllegalBlockSizeException; import javax.crypto.NoSuchPaddingException; import org.apache.http.HttpResponse; import org.apache.http.client.methods.HttpPost; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClientBuilder; import org.bouncycastle.util.encoders.DecoderException; public class EncryptWebPushHelper { static String cipherText; static String encryptionHeader; static String cryptoKeyHeader; public static void main(String[] args) { String p256dh = "就是p256dh "; String auth = "就是auth "; String apiKey = "就是api key "; String regId = "User的registration id"; try { //加密後得到cipherText, encryptionHeader, cryptoKeyHeader JSONObject message_JSONObject = new JSONObject(); message_JSONObject.put("myText", "Hugo ,^o^ test"); generateEncryptedPayload(message_JSONObject.toString(), p256dh , auth ); System.out.println("cipherText: " + cipherText); System.out.println("encryptionHeader: " + encryptionHeader); System.out.println("cryptoKeyHeader: " + cryptoKeyHeader); //向GCM Server送推播資料 HttpClientBuilder httpClientBuilder = HttpClientBuilder.create(); CloseableHttpClient closeableHttpClient = httpClientBuilder.build(); HttpPost httpRequest = new HttpPost("https://gcm-http.googleapis.com/gcm/send"); httpRequest.addHeader("Authorization", "key=" + apiKey); httpRequest.addHeader("Content-Type", "application/json"); httpRequest.addHeader("Encryption", encryptionHeader); httpRequest.addHeader("Crypto-Key", cryptoKeyHeader); httpRequest.addHeader("Content-Encoding", "aesgcm"); httpRequest.addHeader("Cache-Control", "no-cache"); //要送的JSON內容(加密後) JSONObject sendInfo_jsonObject = new JSONObject(); sendInfo_jsonObject.put("registration_ids", new JSONArray().put(regId)); sendInfo_jsonObject.put("raw_data", cipherText); //要送的JSON內容(加密後) httpRequest.setEntity(new StringEntity(sendInfo_jsonObject.toString())); HttpResponse httpResponse = closeableHttpClient.execute(httpRequest); closeableHttpClient.close(); /* 也可用curl來送資料測試 curl --header "Authorization: key=就是API Key" --header "Content-Type:application/json" --header "Encryption: 就是encryptionHeader --header "Crypto-Key: 就是cryptoKeyHeader" --header "Content-Encoding:aesgcm" --header "Cache-Control: no-cache" https://gcm-http.googleapis.com/gcm/send -d "{ \"registration_ids\": [\"客戶端的registration id\"], \"raw_data\": \"就是cipherText" }" */ } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } } public static void generateEncryptedPayload(final String payload, String P256dh, String AuthKey) throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, NoSuchProviderException, InvalidKeySpecException, InvalidKeyException, IOException, NoSuchPaddingException, BadPaddingException, IllegalBlockSizeException, DecoderException, org.apache.commons.codec.DecoderException { EllipticCurveKeyUtil _ellipticCurveKeyUtil = new EllipticCurveKeyUtil(); KeyPair serverKeys = _ellipticCurveKeyUtil.generateServerKeyPair(); final ECPublicKey clientPublicKey = _ellipticCurveKeyUtil.loadP256Dh(P256dh); final byte[] clientAuth = Base64.getUrlDecoder().decode(AuthKey); final byte[] salt = PushApiUtil.generateSalt(); final byte[] sharedSecret = _ellipticCurveKeyUtil.generateSharedSecret(serverKeys, clientPublicKey); final byte[] serverPublicKeyBytes = _ellipticCurveKeyUtil.publicKeyToBytes((ECPublicKey) serverKeys.getPublic()); final byte[] clientPublicKeyBytes = _ellipticCurveKeyUtil.publicKeyToBytes(clientPublicKey); final byte[] nonceInfo = PushApiUtil.generateInfo(serverPublicKeyBytes, clientPublicKeyBytes, Constants.NONCE); final byte[] contentEncryptionKeyInfo = PushApiUtil.generateInfo(serverPublicKeyBytes, clientPublicKeyBytes, Constants.AESGCM128); cipherText = PushApiUtil.encryptPayload(payload, sharedSecret, salt, contentEncryptionKeyInfo, nonceInfo, clientAuth); encryptionHeader = PushApiUtil.createEncryptionHeader(salt); cryptoKeyHeader = PushApiUtil.createCryptoKeyHeader(serverPublicKeyBytes); } }
- 如果有裝curl的話也可以向上述程式碼裡的注解寫的那樣,用命令列視窗來送推播訊息給GCM伺服器做測試:
curl --header "Authorization: key=就是FCM的ServerKey或GCM的API_Key"
--header "Content-Type:application/json"
--header "Encryption: salt=XXX"
--header "Crypto-Key: dh=XXX"
--header "Content-Encoding: aesgcm"
--header "Cache-Control: no-cache"
https://gcm-http.googleapis.com/gcm/send
-d "{
\"registration_ids\": [\"User的Registration_id\"],
\"raw_data\": \"加密後的cipherText\"
}" - 如果成功的話,應該就會在User的視窗看到跳出的彈跳視窗了。
- 先到index.html
會先看到如下的畫面,
如果沒有問題(有問題請按F12開啟開發者視窗看錯誤訊息),等一下Subscribe按鈕會變成可按,按下後第一次會跳出詢問的視窗如下: - 把API Key, Registration Id、p256dh和auth,還有想要送的文字放到EncryptWebPushHelper.java中,並直接執行EncryptWebPushHelper.java,應該就可以看到電腦的右下角出現彈跳訊息視窗了,如下圖。
GoogleWebPushTest.7z
所需的jar檔:
作者已經移除這則留言。
回覆刪除Hi,你好!
回覆刪除我收到你的e-mail回覆,挺有幫助,
所以之後有問題請教以blog留言為主是嗎?
謝謝
很高與幫到你,
刪除Email也可以,只是我blog留言比較會注意到而已。
Hi,我有問題想請教,
回覆刪除我是參考這個網站用VB.NET去寫的:
http://stackoverflow.com/questions/11261718/gcm-push-notification-with-asp-net
其中 ContentType = "application/x-www-form-urlencoded",
可是你的Content-Type:application/json,
那會影響sw.js中event.data.json()接收的type嗎?
我推測連結裡的payload就是客製化訊息,
但我不知道怎麼在sw.js接收payload的訊息,
還是因為是沒加密的關係呢?謝謝!
抱歉我不太熟VB.NET。
刪除Content-Type:application/json是用在傳Webpush請求給Webpush Server時設置的Header,它告訴Webpush Server我們傳送的是JSON格式的資料,以利對方辨認解析,建議使用這個Content-Type。
sw.js的event.data是在客戶端接收到Webpush傳來的訊息,其中帶有的json()方法可以得到解密後的訊息,為一個JSON物件,建議可以把event.data或event.data.json()印出來看看有無接收成功。
要客製化訊息的話需要將加密的資料(HTTP請求內容、Header)傳給Webpush Server才行。
Hi,謝謝你的回覆,
刪除後來研究確實是加密的問題,
連結裡沒加密所以event.data都是null,
我後來用libs加密成功接收到值,
謝謝你!
不過我另外有個疑問,
不知道VapidHeader的功用是什麼?
你指的是Encryption和Crypto-Key Header嗎?
刪除就是官方說明加密訊息時要附上的Header,可參考
https://developers.google.com/web/updates/2016/07/web-push-interop-wins
似乎是用來讓Server端辨識的,可能就不用在manifest.json上打gcm_sender_id了吧?
不確定,可Google關鍵字
VAPID(Voluntary Application Server Identification)自主應用伺服器標識
您好,請問Subscribe之後會跳出一個alert,這個alert的格式可以改嗎?另外右下角的彈跳訊息的標題要在哪邊修改呢?謝謝。
回覆刪除1.“請問Subscribe之後會跳出一個alert,這個alert的格式可以改嗎”
刪除--> 指的是Chrome的“要求下列權限”alert嗎?是的話不行。
2.“另外右下角的彈跳訊息的標題要在哪邊修改呢?謝謝。”
--> 就是sw.js裡下面程式碼的“title"。
self.registration.showNotification(title, {
body: event.data.json().myText,
icon: 'images/icon.png',
tag: 'my-tag'
}));
感謝。
刪除