vapid.js (3110B)
1 function toBase64Url(array) { 2 return btoa([...array].map(c => String.fromCharCode(c)).join('')).replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "") 3 } 4 5 class VAPID { 6 #publicKey; 7 #privateKey; 8 9 constructor(publicKey, privateKey) { 10 this.#publicKey = publicKey; 11 this.#privateKey = privateKey; 12 } 13 14 get publicKey() { 15 return this.#publicKey; 16 } 17 18 async #jws(audience) { 19 // https://datatracker.ietf.org/doc/html/rfc7515#section-3.1 20 // BASE64URL(UTF8(JWS Protected Header)) || '.' || 21 // BASE64URL(JWS Payload) || '.' || 22 // BASE64URL(JWS Signature) 23 24 // https://datatracker.ietf.org/doc/html/rfc8292#section-2 25 // ECDSA on the NIST P-256 curve [FIPS186], which is identified as "ES256" [RFC7518]. 26 const rawHeader = { typ: "JWT", alg: "ES256" }; 27 const header = toBase64Url(new TextEncoder().encode(JSON.stringify(rawHeader))); 28 29 // https://datatracker.ietf.org/doc/html/rfc8292#section-2 30 const rawPayload = { 31 // An "aud" (Audience) claim in the token MUST include the Unicode 32 // serialization of the origin (Section 6.1 of [RFC6454]) of the push 33 // resource URL. 34 aud: audience, 35 // An "exp" (Expiry) claim MUST be included with the time after which 36 // the token expires. 37 exp: parseInt(new Date().getTime() / 1000) + 24 * 60 * 60, // seconds, 24hr 38 // The "sub" claim SHOULD include a contact URI for the application server as either a 39 // "mailto:" (email) [RFC6068] or an "https:" [RFC2818] URI. 40 sub: "mailto:webpush@example.com", 41 }; 42 const payload = toBase64Url(new TextEncoder().encode(JSON.stringify(rawPayload))); 43 44 const input = `${header}.${payload}`; 45 // https://datatracker.ietf.org/doc/html/rfc7518#section-3.1 46 // ES256 | ECDSA using P-256 and SHA-256 47 const rawSignature = await crypto.subtle.sign({ 48 name: "ECDSA", 49 namedCurve: "P-256", 50 hash: { name: "SHA-256" }, 51 }, this.#privateKey, new TextEncoder().encode(input)); 52 const signature = toBase64Url(new Uint8Array(rawSignature)); 53 return `${input}.${signature}`; 54 } 55 56 async generateAuthHeader(audience) { 57 // https://datatracker.ietf.org/doc/html/rfc8292#section-3.1 58 // The "t" parameter of the "vapid" authentication scheme carries a JWT 59 // as described in Section 2. 60 const t = await this.#jws(audience); 61 // https://datatracker.ietf.org/doc/html/rfc8292#section-3.2 62 // The "k" parameter includes an ECDSA public key [FIPS186] in 63 // uncompressed form [X9.62] that is encoded using base64url encoding 64 // [RFC7515]. 65 const k = toBase64Url(this.#publicKey) 66 return `vapid t=${t},k=${k}`; 67 } 68 }; 69 70 export async function createVapid() { 71 // https://datatracker.ietf.org/doc/html/rfc8292#section-2 72 // The signature MUST use ECDSA on the NIST P-256 curve [FIPS186] 73 const keys = await crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-256" }, true, ["sign"]); 74 const publicKey = new Uint8Array(await crypto.subtle.exportKey("raw", keys.publicKey)); 75 const privateKey = keys.privateKey; 76 return new VAPID(publicKey, privateKey); 77 };