18 package org.turro.push.service;
20 import java.io.IOException;
21 import java.security.GeneralSecurityException;
22 import java.security.InvalidAlgorithmParameterException;
23 import java.security.KeyPair;
24 import java.security.KeyPairGenerator;
25 import java.security.NoSuchAlgorithmException;
26 import java.security.NoSuchProviderException;
27 import java.security.PrivateKey;
28 import java.security.PublicKey;
29 import java.security.SecureRandom;
30 import java.security.spec.InvalidKeySpecException;
31 import java.util.HashMap;
33 import org.apache.commons.codec.binary.Base64;
34 import org.bouncycastle.jce.ECNamedCurveTable;
35 import org.bouncycastle.jce.interfaces.ECPublicKey;
36 import org.bouncycastle.jce.spec.ECNamedCurveParameterSpec;
37 import org.jose4j.jws.AlgorithmIdentifiers;
38 import org.jose4j.jws.JsonWebSignature;
39 import org.jose4j.jwt.JwtClaims;
40 import org.jose4j.lang.JoseException;
41 import org.turro.push.security.ServerKeys;
47 public abstract class AbstractPushService<T
extends AbstractPushService<T>> {
49 private static final SecureRandom SECURE_RANDOM =
new SecureRandom();
50 public static final String SERVER_KEY_ID =
"server-key-id";
51 public static final String SERVER_KEY_CURVE =
"P-256";
56 private String gcmApiKey;
62 private String subject;
67 private PublicKey publicKey;
72 private PrivateKey privateKey;
74 public AbstractPushService() {
77 public AbstractPushService(String gcmApiKey) {
78 this.gcmApiKey = gcmApiKey;
81 public AbstractPushService(KeyPair keyPair) {
82 this.publicKey = keyPair.getPublic();
83 this.privateKey = keyPair.getPrivate();
86 public AbstractPushService(KeyPair keyPair, String subject) {
88 this.subject = subject;
91 public AbstractPushService(String publicKey, String privateKey)
throws GeneralSecurityException {
96 public AbstractPushService(String publicKey, String privateKey, String subject)
throws GeneralSecurityException {
97 this(publicKey, privateKey);
98 this.subject = subject;
115 public static Encrypted encrypt(
byte[] payload, ECPublicKey userPublicKey,
byte[] userAuth,
Encoding encoding)
throws GeneralSecurityException {
116 KeyPair localKeyPair = generateLocalKeyPair();
118 Map<String, KeyPair> keys =
new HashMap<>();
119 keys.put(SERVER_KEY_ID, localKeyPair);
121 Map<String, String> labels =
new HashMap<>();
122 labels.put(SERVER_KEY_ID, SERVER_KEY_CURVE);
124 byte[] salt =
new byte[16];
125 SECURE_RANDOM.nextBytes(salt);
128 byte[] ciphertext = httpEce.
encrypt(payload, salt,
null, SERVER_KEY_ID, userPublicKey, userAuth, encoding);
132 .withPublicKey(localKeyPair.getPublic())
133 .withCiphertext(ciphertext)
145 private static KeyPair generateLocalKeyPair()
throws NoSuchAlgorithmException, NoSuchProviderException, InvalidAlgorithmParameterException {
146 ECNamedCurveParameterSpec parameterSpec = ECNamedCurveTable.getParameterSpec(
"prime256v1");
147 KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(
"ECDH",
"BC");
148 keyPairGenerator.initialize(parameterSpec);
150 return keyPairGenerator.generateKeyPair();
154 if (getPrivateKey() !=
null && getPublicKey() !=
null) {
156 throw new IllegalStateException(
"Public key and private key do not match.");
168 byte[] salt = encrypted.
getSalt();
171 Map<String, String> headers =
new HashMap<>();
174 headers.put(
"TTL", String.valueOf(notification.
getTTL()));
181 headers.put(
"Topic", notification.
getTopic());
185 headers.put(
"Content-Type",
"application/octet-stream");
188 headers.put(
"Content-Encoding",
"aes128gcm");
190 headers.put(
"Content-Encoding",
"aesgcm");
191 headers.put(
"Encryption",
"salt=" + Base64.encodeBase64URLSafeString(salt));
192 headers.put(
"Crypto-Key",
"dh=" + Base64.encodeBase64URLSafeString(dh));
198 if (notification.
isGcm()) {
199 if (getGcmApiKey() ==
null) {
200 throw new IllegalStateException(
"An GCM API key is needed to send a push notification to a GCM endpoint.");
203 headers.put(
"Authorization",
"key=" + getGcmApiKey());
204 }
else if (vapidEnabled()) {
206 if (notification.
getEndpoint().startsWith(
"https://fcm.googleapis.com")) {
207 url = notification.
getEndpoint().replace(
"fcm/send",
"wp");
211 JwtClaims claims =
new JwtClaims();
212 claims.setAudience(notification.
getOrigin());
213 claims.setExpirationTimeMinutesInTheFuture(12 * 60);
214 if (getSubject() !=
null) {
215 claims.setSubject(getSubject());
218 JsonWebSignature jws =
new JsonWebSignature();
219 jws.setHeader(
"typ",
"JWT");
220 jws.setHeader(
"alg",
"ES256");
221 jws.setPayload(claims.toJson());
222 jws.setKey(getPrivateKey());
223 jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.ECDSA_USING_P256_CURVE_AND_SHA256);
228 headers.put(
"Authorization",
"vapid t=" + jws.getCompactSerialization() +
", k=" + Base64.encodeBase64URLSafeString(pk));
230 headers.put(
"Authorization",
"WebPush " + jws.getCompactSerialization());
233 if (headers.containsKey(
"Crypto-Key")) {
234 headers.put(
"Crypto-Key", headers.get(
"Crypto-Key") +
";p256ecdsa=" + Base64.encodeBase64URLSafeString(pk));
236 headers.put(
"Crypto-Key",
"p256ecdsa=" + Base64.encodeBase64URLSafeString(pk));
238 }
else if (notification.
isFcm() && getGcmApiKey() !=
null) {
239 headers.put(
"Authorization",
"key=" + getGcmApiKey());
251 public T setGcmApiKey(String gcmApiKey) {
252 this.gcmApiKey = gcmApiKey;
257 public String getGcmApiKey() {
261 public String getSubject() {
271 public T setSubject(String subject) {
272 this.subject = subject;
283 public T setKeyPair(KeyPair keyPair) {
284 setPublicKey(keyPair.getPublic());
285 setPrivateKey(keyPair.getPrivate());
290 public PublicKey getPublicKey() {
300 public T setPublicKey(String publicKey)
throws NoSuchAlgorithmException, NoSuchProviderException, InvalidKeySpecException {
306 public PrivateKey getPrivateKey() {
310 public KeyPair getKeyPair() {
311 return new KeyPair(publicKey, privateKey);
320 public T setPublicKey(PublicKey publicKey) {
321 this.publicKey = publicKey;
332 public T setPrivateKey(String privateKey)
throws NoSuchAlgorithmException, NoSuchProviderException, InvalidKeySpecException {
344 public T setPrivateKey(PrivateKey privateKey) {
345 this.privateKey = privateKey;
355 protected boolean vapidEnabled() {
356 return publicKey !=
null && privateKey !=
null;
static PrivateKey loadPrivateKey(String encodedPrivateKey)
static PublicKey loadPublicKey(String encodedPublicKey)
static byte[] encode(ECPublicKey publicKey)
static boolean verifyKeyPair(PrivateKey privateKey, PublicKey publicKey)
byte[] encrypt(byte[] plaintext, byte[] salt, byte[] privateKey, String keyid, ECPublicKey dh, byte[] authSecret, Encoding version)
ECPublicKey getUserPublicKey()