77 lines
3 KiB
JavaScript
77 lines
3 KiB
JavaScript
function toBase64Url(array) {
|
|
return btoa([...array].map(c => String.fromCharCode(c)).join('')).replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "")
|
|
}
|
|
|
|
class VAPID {
|
|
#publicKey;
|
|
#privateKey;
|
|
|
|
constructor(publicKey, privateKey) {
|
|
this.#publicKey = publicKey;
|
|
this.#privateKey = privateKey;
|
|
}
|
|
|
|
get publicKey() {
|
|
return this.#publicKey;
|
|
}
|
|
|
|
async #jws(audience) {
|
|
// https://datatracker.ietf.org/doc/html/rfc7515#section-3.1
|
|
// BASE64URL(UTF8(JWS Protected Header)) || '.' ||
|
|
// BASE64URL(JWS Payload) || '.' ||
|
|
// BASE64URL(JWS Signature)
|
|
|
|
// https://datatracker.ietf.org/doc/html/rfc8292#section-2
|
|
// ECDSA on the NIST P-256 curve [FIPS186], which is identified as "ES256" [RFC7518].
|
|
const rawHeader = { typ: "JWT", alg: "ES256" };
|
|
const header = toBase64Url(new TextEncoder().encode(JSON.stringify(rawHeader)));
|
|
|
|
// https://datatracker.ietf.org/doc/html/rfc8292#section-2
|
|
const rawPayload = {
|
|
// An "aud" (Audience) claim in the token MUST include the Unicode
|
|
// serialization of the origin (Section 6.1 of [RFC6454]) of the push
|
|
// resource URL.
|
|
aud: audience,
|
|
// An "exp" (Expiry) claim MUST be included with the time after which
|
|
// the token expires.
|
|
exp: parseInt(new Date().getTime() / 1000) + 24 * 60 * 60, // seconds, 24hr
|
|
// The "sub" claim SHOULD include a contact URI for the application server as either a
|
|
// "mailto:" (email) [RFC6068] or an "https:" [RFC2818] URI.
|
|
sub: "mailto:webpush@example.com",
|
|
};
|
|
const payload = toBase64Url(new TextEncoder().encode(JSON.stringify(rawPayload)));
|
|
|
|
const input = `${header}.${payload}`;
|
|
// https://datatracker.ietf.org/doc/html/rfc7518#section-3.1
|
|
// ES256 | ECDSA using P-256 and SHA-256
|
|
const rawSignature = await crypto.subtle.sign({
|
|
name: "ECDSA",
|
|
namedCurve: "P-256",
|
|
hash: { name: "SHA-256" },
|
|
}, this.#privateKey, new TextEncoder().encode(input));
|
|
const signature = toBase64Url(new Uint8Array(rawSignature));
|
|
return `${input}.${signature}`;
|
|
}
|
|
|
|
async generateAuthHeader(audience) {
|
|
// https://datatracker.ietf.org/doc/html/rfc8292#section-3.1
|
|
// The "t" parameter of the "vapid" authentication scheme carries a JWT
|
|
// as described in Section 2.
|
|
const t = await this.#jws(audience);
|
|
// https://datatracker.ietf.org/doc/html/rfc8292#section-3.2
|
|
// The "k" parameter includes an ECDSA public key [FIPS186] in
|
|
// uncompressed form [X9.62] that is encoded using base64url encoding
|
|
// [RFC7515].
|
|
const k = toBase64Url(this.#publicKey)
|
|
return `vapid t=${t},k=${k}`;
|
|
}
|
|
};
|
|
|
|
export async function createVapid() {
|
|
// https://datatracker.ietf.org/doc/html/rfc8292#section-2
|
|
// The signature MUST use ECDSA on the NIST P-256 curve [FIPS186]
|
|
const keys = await crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-256" }, true, ["sign"]);
|
|
const publicKey = new Uint8Array(await crypto.subtle.exportKey("raw", keys.publicKey));
|
|
const privateKey = keys.privateKey;
|
|
return new VAPID(publicKey, privateKey);
|
|
};
|