tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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 };