Skip to content

Commit 42d8667

Browse files
committed
Added support for aes128gcm
1 parent e6bb825 commit 42d8667

File tree

4 files changed

+94
-63
lines changed

4 files changed

+94
-63
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "web-push-browser",
3-
"version": "1.0.1",
3+
"version": "1.1.0",
44
"description": "Minimal library for sending notifications via the browser Push API",
55
"main": "build/index.js",
66
"dependencies": {

src/crypto/payload.ts

Lines changed: 33 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,24 @@
11
import type { PushNotificationSubscription } from "../types.js";
22
import { fromBase64Url } from "../utils/base64url.js";
33

4+
type EncryptionOptions = {
5+
algorithm: "aesgcm" | "aes128gcm";
6+
};
7+
48
/**
59
* Encrypt a plaintext payload using the keys provided by the PushSubscription.
610
* @param payload - The plaintext payload to encrypt.
711
* @param keys - The keys from the PushSubscription.
12+
* @param options - Options for encryption. Defaults to AES128GCM if not specified.
813
*/
914
export async function encryptPayload(
1015
payload: string,
1116
keys: PushNotificationSubscription["keys"],
17+
options: EncryptionOptions = { algorithm: "aes128gcm" },
1218
) {
1319
const encoder = new TextEncoder();
1420
const salt = crypto.getRandomValues(new Uint8Array(16));
1521

16-
if (!keys.p256dh || !keys.auth) {
17-
throw new Error("Missing p256dh or auth key");
18-
}
19-
2022
// Get the p256dh and auth keys from the subscription
2123
const auth =
2224
typeof keys.auth === "string" ? fromBase64Url(keys.auth) : keys.auth;
@@ -62,7 +64,7 @@ export async function encryptPayload(
6264
);
6365

6466
// Derive the Content Encryption Key
65-
const cekInfo = encoder.encode("Content-Encoding: aes128gcm");
67+
const cekInfo = encoder.encode(`Content-Encoding: ${options.algorithm}`);
6668
const cek = await crypto.subtle.deriveBits(
6769
{
6870
name: "HKDF",
@@ -95,24 +97,30 @@ export async function encryptPayload(
9597
await crypto.subtle.exportKey("raw", localKeyPair.publicKey),
9698
);
9799

98-
// Construct the header
99-
const header = new Uint8Array([
100-
...salt, // 16 bytes
101-
...new Uint8Array(4), // 4 bytes for record size (we'll fill this later)
102-
...serverPublicKeyBytes, // 65 bytes for public key
103-
]);
104-
105-
// Construct the full message
106-
const message = new Uint8Array([
107-
...header,
108-
...new Uint8Array(encryptedPayload),
109-
]);
110-
111-
// Now fill in the record size
112-
const recordSize = new Uint32Array([encryptedPayload.byteLength]);
113-
new Uint8Array(message.buffer, 16, 4).set(new Uint8Array(recordSize.buffer));
114-
const encrypted = message.buffer;
115-
const serverPublicKey = localKeyPair.publicKey;
116-
117-
return { encrypted, salt, serverPublicKey };
100+
let encrypted: ArrayBuffer;
101+
102+
if (options.algorithm === "aes128gcm") {
103+
const header = new Uint8Array([
104+
...salt,
105+
...new Uint8Array(4),
106+
...serverPublicKeyBytes,
107+
]);
108+
109+
const message = new Uint8Array([
110+
...header,
111+
...new Uint8Array(encryptedPayload),
112+
]);
113+
114+
const recordSize = new Uint32Array([encryptedPayload.byteLength]);
115+
new Uint8Array(message.buffer, 16, 4).set(
116+
new Uint8Array(recordSize.buffer),
117+
);
118+
encrypted = message.buffer;
119+
} else {
120+
// For 'aesgcm', we don't include the header in the encrypted payload
121+
encrypted = encryptedPayload;
122+
}
123+
const localPublicKey = localKeyPair.publicKey;
124+
125+
return { encrypted, salt, localPublicKey };
118126
}

src/request/generate.ts

Lines changed: 47 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,70 @@
1-
import type { PushNotificationSubscription } from "../types.js";
21
import { toBase64Url } from "../utils/base64url.js";
32

3+
type BaseOptions = {
4+
ttl?: number;
5+
urgency?: "very-low" | "low" | "normal" | "high";
6+
};
7+
8+
type AESGCMOptions = BaseOptions & {
9+
algorithm: "aesgcm";
10+
salt: ArrayBuffer;
11+
localPublicKey: CryptoKey;
12+
};
13+
14+
type AES128GCMOptions = BaseOptions & {
15+
algorithm?: "aes128gcm";
16+
};
17+
18+
type EncryptionOptions = AESGCMOptions | AES128GCMOptions;
19+
420
/**
521
* Generate the headers for a Web Push request.
622
* @param publicVapidKey - The public VAPID key.
723
* @param jwt - The signed JWT token.
824
* @param encryptedPayload - The encrypted payload.
9-
* @param salt - The salt used to encrypt the payload.
10-
* @param localPublicKey - The public key used to encrypt the payload.
11-
* @param ttl - The time-to-live for the notification.
25+
* @param options - Options for encryption and additional headers. Defaults to AES128GCM if not specified.
1226
* @returns The generated headers.
1327
*/
1428
export async function generateHeaders(
1529
publicVapidKey: CryptoKey,
1630
jwt: string,
1731
encryptedPayload: ArrayBuffer,
18-
salt: ArrayBuffer,
19-
localPublicKey: CryptoKey,
20-
ttl = 86400,
21-
) {
32+
options: EncryptionOptions = { algorithm: "aes128gcm" },
33+
): Promise<Headers> {
2234
const exportedPubKey = await crypto.subtle.exportKey("raw", publicVapidKey);
2335
const encodedPubKey = toBase64Url(exportedPubKey);
2436

25-
const exportedLocalPubKey = await crypto.subtle.exportKey(
26-
"raw",
27-
localPublicKey,
28-
);
29-
const encodedLocalPubKey = toBase64Url(exportedLocalPubKey);
30-
3137
const headers = new Headers();
32-
headers.append("Authorization", `Bearer ${jwt}`);
33-
const cryptoKey = new URLSearchParams();
34-
cryptoKey.set("dh", encodedLocalPubKey);
38+
headers.append("Content-Type", "application/octet-stream");
39+
headers.append("Content-Length", encryptedPayload.byteLength.toString());
40+
headers.append("TTL", Math.floor(options.ttl ?? 86400).toString());
3541

36-
// On Microsoft Edge servers, this doesn't work, despite being documented in the spec
37-
// headers.append("Crypto-Key", `p256ecdsa=${encodedPubKey}`);
38-
// headers.append("Crypto-Key", `dh=${encodedLocalPubKey}`);
42+
if (options.urgency) {
43+
headers.append("Urgency", options.urgency);
44+
}
3945

40-
// Also on Microsoft, the order matters, despite the spec saying it doesn't
41-
headers.append(
42-
"Crypto-Key",
43-
`p256ecdsa=${encodedPubKey};dh=${encodedLocalPubKey}`,
44-
);
46+
if (options.algorithm === "aesgcm") {
47+
const exportedLocalPubKey = await crypto.subtle.exportKey(
48+
"raw",
49+
options.localPublicKey,
50+
);
51+
const encodedLocalPubKey = toBase64Url(exportedLocalPubKey);
4552

46-
headers.append("Content-Encoding", "aesgcm");
47-
headers.append("Content-Type", "application/octet-stream");
48-
headers.append("Content-Length", encryptedPayload.byteLength.toString());
49-
headers.append("Encryption", `salt=${toBase64Url(salt)}`);
50-
headers.append("TTL", Math.floor(ttl).toString());
53+
headers.append("Authorization", `Bearer ${jwt}`);
54+
headers.append("Content-Encoding", "aesgcm");
55+
56+
// On Microsoft Edge servers, this doesn't work, despite being documented in the spec
57+
// headers.append("Crypto-Key", `p256ecdsa=${encodedPubKey}`);
58+
// headers.append("Crypto-Key", `dh=${encodedLocalPubKey}`);
59+
headers.append(
60+
"Crypto-Key",
61+
`p256ecdsa=${encodedPubKey};dh=${encodedLocalPubKey}`,
62+
);
63+
headers.append("Encryption", `salt=${toBase64Url(options.salt)}`);
64+
} else {
65+
headers.append("Authorization", `vapid t=${jwt}, k=${encodedPubKey}`);
66+
headers.append("Content-Encoding", "aes128gcm");
67+
}
5168

5269
return headers;
5370
}

src/request/send.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,19 @@ import { encryptPayload } from "../crypto/payload.js";
33
import type { PushNotificationSubscription } from "../types.js";
44
import { generateHeaders } from "./generate.js";
55

6+
type EncryptionOptions = {
7+
algorithm: "aesgcm" | "aes128gcm";
8+
urgency?: "very-low" | "low" | "normal" | "high";
9+
ttl?: number;
10+
};
11+
612
/**
713
* Send a push notification to a user.
814
* @param vapidKeys - The VAPID keys to use for the request.
915
* @param subscription - The PushSubscription to send the notification to.
1016
* @param email - The email address to use as the `sub` claim in the JWT. For example, `[email protected]`.
1117
* @param payload - The payload to send in the notification.
18+
* @param options - Options for encryption and additional headers. Defaults to AES128GCM if not specified.
1219
* @returns The response from the push service.
1320
* @throws If any of the keys are unable to be parsed.
1421
*/
@@ -17,23 +24,22 @@ export async function sendPushNotification(
1724
subscription: PushNotificationSubscription,
1825
email: string,
1926
payload: string,
27+
options: EncryptionOptions = { algorithm: "aes128gcm" },
2028
) {
2129
const jwt = await createJWT(
2230
vapidKeys.privateKey,
2331
new URL(subscription.endpoint),
2432
email,
2533
);
26-
const { encrypted, salt, serverPublicKey } = await encryptPayload(
34+
const { encrypted, salt, localPublicKey } = await encryptPayload(
2735
payload,
2836
subscription.keys,
2937
);
30-
const headers = await generateHeaders(
31-
vapidKeys.publicKey,
32-
jwt,
33-
encrypted,
38+
const headers = await generateHeaders(vapidKeys.publicKey, jwt, encrypted, {
39+
...options,
40+
localPublicKey,
3441
salt,
35-
serverPublicKey,
36-
);
42+
});
3743
const request = new Request(subscription.endpoint, {
3844
method: "POST",
3945
headers,

0 commit comments

Comments
 (0)