BrightSide Workbench Full Report + Source Code
AbstractPushService.java
Go to the documentation of this file.
1 /*
2  * TurrĂ³ i Cutiller Foundation. License notice.
3  * Copyright (C) 2022 Lluis TurrĂ³ Cutiller <http://www.turro.org/>
4  *
5  * This program is free software: you can redistribute it and/or modify
6  * it under the terms of the GNU Affero General Public License as published by
7  * the Free Software Foundation, either version 3 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13  * GNU Affero General Public License for more details.
14  *
15  * You should have received a copy of the GNU Affero General Public License
16  * along with this program. If not, see <http://www.gnu.org/licenses/>.
17  */
18 package org.turro.push.service;
19 
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;
32 import java.util.Map;
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;
42 
47 public abstract class AbstractPushService<T extends AbstractPushService<T>> {
48 
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";
52 
56  private String gcmApiKey;
57 
62  private String subject;
63 
67  private PublicKey publicKey;
68 
72  private PrivateKey privateKey;
73 
74  public AbstractPushService() {
75  }
76 
77  public AbstractPushService(String gcmApiKey) {
78  this.gcmApiKey = gcmApiKey;
79  }
80 
81  public AbstractPushService(KeyPair keyPair) {
82  this.publicKey = keyPair.getPublic();
83  this.privateKey = keyPair.getPrivate();
84  }
85 
86  public AbstractPushService(KeyPair keyPair, String subject) {
87  this(keyPair);
88  this.subject = subject;
89  }
90 
91  public AbstractPushService(String publicKey, String privateKey) throws GeneralSecurityException {
92  this.publicKey = ServerKeys.loadPublicKey(publicKey);
93  this.privateKey = ServerKeys.loadPrivateKey(privateKey);
94  }
95 
96  public AbstractPushService(String publicKey, String privateKey, String subject) throws GeneralSecurityException {
97  this(publicKey, privateKey);
98  this.subject = subject;
99  }
100 
115  public static Encrypted encrypt(byte[] payload, ECPublicKey userPublicKey, byte[] userAuth, Encoding encoding) throws GeneralSecurityException {
116  KeyPair localKeyPair = generateLocalKeyPair();
117 
118  Map<String, KeyPair> keys = new HashMap<>();
119  keys.put(SERVER_KEY_ID, localKeyPair);
120 
121  Map<String, String> labels = new HashMap<>();
122  labels.put(SERVER_KEY_ID, SERVER_KEY_CURVE);
123 
124  byte[] salt = new byte[16];
125  SECURE_RANDOM.nextBytes(salt);
126 
127  HttpEce httpEce = new HttpEce(keys, labels);
128  byte[] ciphertext = httpEce.encrypt(payload, salt, null, SERVER_KEY_ID, userPublicKey, userAuth, encoding);
129 
130  return new Encrypted.Builder()
131  .withSalt(salt)
132  .withPublicKey(localKeyPair.getPublic())
133  .withCiphertext(ciphertext)
134  .build();
135  }
136 
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);
149 
150  return keyPairGenerator.generateKeyPair();
151  }
152 
153  protected final HttpRequest prepareRequest(Notification notification, Encoding encoding) throws GeneralSecurityException, IOException, JoseException {
154  if (getPrivateKey() != null && getPublicKey() != null) {
155  if (!ServerKeys.verifyKeyPair(getPrivateKey(), getPublicKey())) {
156  throw new IllegalStateException("Public key and private key do not match.");
157  }
158  }
159 
160  Encrypted encrypted = encrypt(
161  notification.getPayload(),
162  notification.getUserPublicKey(),
163  notification.getUserAuth(),
164  encoding
165  );
166 
167  byte[] dh = ServerKeys.encode((ECPublicKey) encrypted.getPublicKey());
168  byte[] salt = encrypted.getSalt();
169 
170  String url = notification.getEndpoint();
171  Map<String, String> headers = new HashMap<>();
172  byte[] body = null;
173 
174  headers.put("TTL", String.valueOf(notification.getTTL()));
175 
176  if (notification.hasUrgency()) {
177  headers.put("Urgency", notification.getUrgency().getHeaderValue());
178  }
179 
180  if (notification.hasTopic()) {
181  headers.put("Topic", notification.getTopic());
182  }
183 
184  if (notification.hasPayload()) {
185  headers.put("Content-Type", "application/octet-stream");
186 
187  if (encoding == Encoding.AES128GCM) {
188  headers.put("Content-Encoding", "aes128gcm");
189  } else if (encoding == Encoding.AESGCM) {
190  headers.put("Content-Encoding", "aesgcm");
191  headers.put("Encryption", "salt=" + Base64.encodeBase64URLSafeString(salt));
192  headers.put("Crypto-Key", "dh=" + Base64.encodeBase64URLSafeString(dh));
193  }
194 
195  body = encrypted.getCiphertext();
196  }
197 
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.");
201  }
202 
203  headers.put("Authorization", "key=" + getGcmApiKey());
204  } else if (vapidEnabled()) {
205  if (encoding == Encoding.AES128GCM) {
206  if (notification.getEndpoint().startsWith("https://fcm.googleapis.com")) {
207  url = notification.getEndpoint().replace("fcm/send", "wp");
208  }
209  }
210 
211  JwtClaims claims = new JwtClaims();
212  claims.setAudience(notification.getOrigin());
213  claims.setExpirationTimeMinutesInTheFuture(12 * 60);
214  if (getSubject() != null) {
215  claims.setSubject(getSubject());
216  }
217 
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);
224 
225  byte[] pk = ServerKeys.encode((ECPublicKey) getPublicKey());
226 
227  if (encoding == Encoding.AES128GCM) {
228  headers.put("Authorization", "vapid t=" + jws.getCompactSerialization() + ", k=" + Base64.encodeBase64URLSafeString(pk));
229  } else if (encoding == Encoding.AESGCM) {
230  headers.put("Authorization", "WebPush " + jws.getCompactSerialization());
231  }
232 
233  if (headers.containsKey("Crypto-Key")) {
234  headers.put("Crypto-Key", headers.get("Crypto-Key") + ";p256ecdsa=" + Base64.encodeBase64URLSafeString(pk));
235  } else {
236  headers.put("Crypto-Key", "p256ecdsa=" + Base64.encodeBase64URLSafeString(pk));
237  }
238  } else if (notification.isFcm() && getGcmApiKey() != null) {
239  headers.put("Authorization", "key=" + getGcmApiKey());
240  }
241 
242  return new HttpRequest(url, headers, body);
243  }
244 
251  public T setGcmApiKey(String gcmApiKey) {
252  this.gcmApiKey = gcmApiKey;
253 
254  return (T) this;
255  }
256 
257  public String getGcmApiKey() {
258  return gcmApiKey;
259  }
260 
261  public String getSubject() {
262  return subject;
263  }
264 
271  public T setSubject(String subject) {
272  this.subject = subject;
273 
274  return (T) this;
275  }
276 
283  public T setKeyPair(KeyPair keyPair) {
284  setPublicKey(keyPair.getPublic());
285  setPrivateKey(keyPair.getPrivate());
286 
287  return (T) this;
288  }
289 
290  public PublicKey getPublicKey() {
291  return publicKey;
292  }
293 
300  public T setPublicKey(String publicKey) throws NoSuchAlgorithmException, NoSuchProviderException, InvalidKeySpecException {
301  setPublicKey(ServerKeys.loadPublicKey(publicKey));
302 
303  return (T) this;
304  }
305 
306  public PrivateKey getPrivateKey() {
307  return privateKey;
308  }
309 
310  public KeyPair getKeyPair() {
311  return new KeyPair(publicKey, privateKey);
312  }
313 
320  public T setPublicKey(PublicKey publicKey) {
321  this.publicKey = publicKey;
322 
323  return (T) this;
324  }
325 
332  public T setPrivateKey(String privateKey) throws NoSuchAlgorithmException, NoSuchProviderException, InvalidKeySpecException {
333  setPrivateKey(ServerKeys.loadPrivateKey(privateKey));
334 
335  return (T) this;
336  }
337 
344  public T setPrivateKey(PrivateKey privateKey) {
345  this.privateKey = privateKey;
346 
347  return (T) this;
348  }
349 
355  protected boolean vapidEnabled() {
356  return publicKey != null && privateKey != null;
357  }
358 }
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)
Definition: HttpEce.java:88