Skip to content

Commit ffcd253

Browse files
committed
feat: review refactoring
* Moved utility functions to common.js * Moved DER functions to DER_lite.js * Improved comments * Improved descriptive text * made vapid & mzcc classes for multiple instantiation
1 parent 3a000e9 commit ffcd253

File tree

5 files changed

+480
-257
lines changed

5 files changed

+480
-257
lines changed

js/common.js

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
class MozCommon {
2+
3+
constructor() {
4+
}
5+
6+
ord(c){
7+
/* return an ordinal for a character
8+
*/
9+
return c.charCodeAt(0);
10+
}
11+
12+
chr(c){
13+
/* return a character for a given ordinal
14+
*/
15+
return String.fromCharCode(c);
16+
}
17+
18+
toUrlBase64(data) {
19+
/* Convert a binary array into a URL safe base64 string
20+
*/
21+
return btoa(data)
22+
.replace(/\+/g, "-")
23+
.replace(/\//g, "_")
24+
.replace(/=/g, "")
25+
}
26+
27+
fromUrlBase64(data) {
28+
/* return a binary array from a URL safe base64 string
29+
*/
30+
return atob((data + "====".substr(data.length % 4))
31+
.replace(/\-/g, "+")
32+
.replace(/\_/g, "/"));
33+
}
34+
35+
_strToArray(str) {
36+
/* convert a string into a ByteArray
37+
*
38+
* TextEncoders would be faster, but have a habit of altering
39+
* byte order
40+
*/
41+
let split = str.split("");
42+
let reply = new Uint8Array(split.length);
43+
for (let i in split) {
44+
reply[i] = this.ord(split[i]);
45+
}
46+
return reply;
47+
}
48+
49+
_arrayToStr(array) {
50+
/* convert a ByteArray into a string
51+
*/
52+
return String.fromCharCode.apply(null, new Uint8Array(array));
53+
}
54+
55+
rawToJWK(raw, ops) {
56+
/* convert a URL safe base64 raw key to jwk format
57+
*/
58+
if (typeof(raw) == "string") {
59+
raw = this._strToArray(this.fromUrlBase64(raw));
60+
}
61+
// Raw is supposed to start with a 0x04, but some libraries don't. sigh.
62+
if (raw.length == 65 && raw[0] != 4) {
63+
throw new Error('ERR_PUB_KEY');
64+
}
65+
66+
raw= raw.slice(-64);
67+
let x = this.toUrlBase64(String.fromCharCode.apply(null,
68+
raw.slice(0,32)));
69+
let y = this.toUrlBase64(String.fromCharCode.apply(null,
70+
raw.slice(32,64)));
71+
72+
// Convert to a JWK and import it.
73+
let jwk = {
74+
crv: "P-256",
75+
ext: true,
76+
key_ops: ops,
77+
kty: "EC",
78+
x: x,
79+
y, y
80+
};
81+
82+
return jwk
83+
}
84+
85+
JWKToRaw(jwk) {
86+
/* Convert a JWK object to a "raw" URL Safe base64 string
87+
*/
88+
xv = this.fromUrlBase64(jwk.x);
89+
yv = this.fromUrlBase64(jwk.y);
90+
return this.toUrlBase64("\x04" + xv + yv);
91+
}
92+
}
93+

js/der_lite.js

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
'use strict';
2+
3+
class DERLite{
4+
constructor() {}
5+
6+
/* Simplified DER export and import is provided because a large number of
7+
* libraries and languages understand DER as a key exchange and storage
8+
* format. DER is NOT required for VAPID, however, the key you may
9+
* generate here (or in a different library) may be in this format.
10+
*
11+
* A fully featured DER library is available at
12+
* https://github.com/indutny/asn1.js
13+
*/
14+
15+
export_private_der(key) {
16+
/* Generate a DER sequence.
17+
*
18+
* This can be read in via something like
19+
* python's
20+
* ecdsa.keys.SigningKey
21+
* .from_der(base64.urlsafe_b64decode("MHc..."))
22+
*
23+
* :param key: CryptoKey containing private key info
24+
*/
25+
return webCrypto.exportKey("jwk", key)
26+
.then(k => {
27+
// verifying key
28+
let xv = mzcc.fromUrlBase64(k.x);
29+
let yv = mzcc.fromUrlBase64(k.y);
30+
// private key
31+
let dv = mzcc.fromUrlBase64(k.d);
32+
33+
// verifying key (public)
34+
let vk = '\x00\x04' + xv + yv;
35+
// \x02 is integer
36+
let int1 = '\x02\x01\x01'; // integer 1
37+
// \x04 is octet string
38+
let dvstr = '\x04' + mzcc.chr(dv.length) + dv;
39+
let curve_oid = "\x06\x08" +
40+
"\x2a\x86\x48\xce\x3d\x03\x01\x07";
41+
// \xaX is a construct, low byte is order.
42+
let curve_oid_const = '\xa0' + mzcc.chr(curve_oid.length) +
43+
curve_oid;
44+
// \x03 is a bitstring
45+
let vk_enc = '\x03' + mzcc.chr(vk.length) + vk;
46+
let vk_const = '\xa1' + mzcc.chr(vk_enc.length) + vk_enc;
47+
// \x30 is a sequence start.
48+
let seq = int1 + dvstr + curve_oid_const + vk_const;
49+
let rder = "\x30" + mzcc.chr(seq.length) + seq;
50+
return mzcc.toUrlBase64(rder);
51+
})
52+
.catch(err => console.error(err))
53+
}
54+
55+
import_private_der(der_str) {
56+
/* Import a Private Key stored in DER format. This allows a key
57+
* to be generated outside of this script.
58+
*
59+
* :param der_str: URL safe base64 formatted DER string.
60+
* :returns: Promise containing the imported private key
61+
*/
62+
let der = mzcc._strToArray(mzcc.fromUrlBase64(der_str));
63+
// quick guantlet to see if this is a valid DER
64+
let cmp = new Uint8Array([2,1,1,4]);
65+
if (der[0] != 48 ||
66+
! der.slice(2, 6).every(function(v, i){return cmp[i] == v})){
67+
throw new Error("Invalid import key")
68+
}
69+
let dv = der.slice(7, 7+der[6]);
70+
// HUGE cheat to get the x y values
71+
let xv = der.slice(-64, -32);
72+
let yv = der.slice(-32);
73+
let key_ops = ['sign'];
74+
75+
let jwk = {
76+
crv: "P-256",
77+
ext: true,
78+
key_ops: key_ops,
79+
kty: "EC",
80+
x: mzcc.toUrlBase64(String.fromCharCode.apply(null, xv)),
81+
y: mzcc.toUrlBase64(String.fromCharCode.apply(null, yv)),
82+
d: mzcc.toUrlBase64(String.fromCharCode.apply(null, dv)),
83+
};
84+
85+
console.debug(JSON.stringify(jwk));
86+
return webCrypto.importKey('jwk', jwk, 'ECDSA', true, key_ops);
87+
}
88+
89+
export_public_der(key) {
90+
/* Generate a DER sequence containing just the public key info.
91+
*
92+
* :param key: CryptoKey containing public key information
93+
* :returns: a URL safe base64 encoded string containing the
94+
* public key
95+
*/
96+
return webCrypto.exportKey("jwk", key)
97+
.then(k => {
98+
// raw keys always begin with a 4
99+
let xv = mzcc._strToArray(mzcc.fromUrlBase64(k.x));
100+
let yv = mzcc._strToArray(mzcc.fromUrlBase64(k.y));
101+
102+
let point = "\x00\x04" +
103+
String.fromCharCode.apply(null, xv) +
104+
String.fromCharCode.apply(null, yv);
105+
window.Kpoint = point;
106+
// a combination of the oid_ecPublicKey + p256 encoded oid
107+
let prefix = "\x30\x13" + // sequence + length
108+
"\x06\x07" + "\x2a\x86\x48\xce\x3d\x02\x01" +
109+
"\x06\x08" + "\x2a\x86\x48\xce\x3d\x03\x01\x07"
110+
let encPoint = "\x03" + mzcc.chr(point.length) + point
111+
let rder = "\x30" + mzcc.chr(prefix.length + encPoint.length) +
112+
prefix + encPoint;
113+
let der = mzcc.toUrlBase64(rder);
114+
return der;
115+
});
116+
}
117+
118+
import_public_der(derArray) {
119+
/* Import a DER formatted public key string.
120+
*
121+
* The Crypto-Key p256ecdsa=... key is such a thing.
122+
* Returns a promise containing the public key.
123+
*
124+
* :param derArray: the DER array containing the public key.
125+
* NOTE: This may also be a URL safe base64 encoded version
126+
* of the DER array.
127+
* :returns: A promise containing the imported public key.
128+
*
129+
*/
130+
if (typeof(derArray) == "string") {
131+
derArray = mzcc._strToArray(mzcc.fromUrlBase64(derArray));
132+
}
133+
/* Super light weight public key import function */
134+
let err = new Error(this.lang.errs.ERR_PUB_D_KEY);
135+
// Does the record begin with "\x30"
136+
if (derArray[0] != 48) { throw err}
137+
// is this an ECDSA record? (looking for \x2a and \x86
138+
if (derArray[6] != 42 && derArray[7] != 134) { throw err}
139+
if (derArray[15] != 42 && derArray[16] != 134) { throw err}
140+
// Public Key Record usually beings @ offset 23.
141+
if (derArray[23] != 3 && derArray[24] != 40 &&
142+
derArray[25] != 0 && derArray[26] != 4) {
143+
throw err;
144+
}
145+
// pubkey offset starts at byte 25
146+
let x = mzcc.toUrlBase64(String.fromCharCode
147+
.apply(null, derArray.slice(27, 27+32)));
148+
let y = mzcc.toUrlBase64(String.fromCharCode
149+
.apply(null, derArray.slice(27+32, 27+64)));
150+
151+
// Convert to a JWK and import it.
152+
let jwk = {
153+
crv: "P-256",
154+
ext: true,
155+
key_ops: ["verify"],
156+
kty: "EC",
157+
x: x,
158+
y, y
159+
};
160+
161+
return webCrypto.importKey('jwk', jwk, 'ECDSA', true, ["verify"])
162+
163+
}
164+
}
165+

js/index.html

Lines changed: 46 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,35 +11,55 @@
1111
<div id="document-main">
1212
<h1>VAPID verification</h1>
1313
<div id="intro" class="section">
14-
<p>This page helps construct or validate <a href="https://datatracker.ietf.org/doc/draft-thomson-webpush-vapid/">VAPID</a> header data.</p>
14+
<p>This page helps validate or construct
15+
<a href="https://datatracker.ietf.org/doc/draft-thomson-webpush-vapid/">VAPID</a>
16+
header data. The <b><a href="#headers">Headers</a></b> section accepts existing header values
17+
(e.g. ones created by a library or included as part of a subscription
18+
update).</p>
19+
<p>The <b><a href="#claims">Claims</a></b> section will create a valid JSON claim, generate a
20+
VAPID key pair, and generate the proper header values.</p>
21+
<p>The <b><a href="#export">Exported Keys</a></b> section provides the keys in DER format which
22+
should be readable by many encryption libraries.</p>
1523
</div>
1624
<div id="inputs" class="section">
17-
<h2>Headers</h2>
18-
<p>The headers are sent with subscription updates. They provide the site information to associate with
19-
this feed.</p>
25+
<a name="headers"><h2>Headers</h2></a>
26+
<p>The headers are sent with subscription updates. They provide the
27+
site information to associate with this feed. PLEASE NOTE: Your private
28+
key should be generated on your machine and should NEVER leave your
29+
box or control. This page will generate a valid key that can be used,
30+
and all functions are local, but this is purely for educational purposes
31+
only.</p>
2032
<label for="auth">Authorization Header:</label>
33+
<div class="description">This is the content of the
34+
<code>Authorization</code> header included as part of the subscription
35+
POST update.</div>
2136
<textarea name="auth" placeholder="Bearer abCDef..."></textarea>
2237
<label for="crypt">Crypto-Key Header:</label>
23-
<p>The public key expressed after "p256ecdsa=" can associate this feed with the dashboard.</p>
38+
<div class="description">This is your VAPID public key. This is included
39+
as part of the <code>Crypto-Key</code> header, which is included
40+
as part of the subscription POST update. <code>Crypto-Key</code>
41+
may contain more than one part. Each part should be separated by a
42+
comma (",")</div>
2443
<textarea name="crypt" placeholder="p256ecdsa=abCDef.."></textarea>
2544
<div class="control">
2645
<label for="publicKey">Public Key:</label>
27-
<p>This is the public key you'd use for the Dashboard</p>
28-
<textarea name="publicKey" placeholder="abCDef..." readonly=true></textarea>
46+
<div class="description">This is the VAPID key you would use when adding
47+
applications to your Dashboard.</div>
48+
<div name="publicKey" class="value" ></div>
2949
<button id="check">Check headers</button>
3050
</div>
3151
</div>
3252
<div id="result" class="section">
33-
<h2>Claims</h2>
53+
<a name="claims"><h2>Claims</h2></a>
3454
<p>Claims are the information a site uses to identify itself.
3555
<div class="row">
3656
<label for="aud" title="The full URL to your site."><b>Aud</b>ience:</label>
37-
<p>The full URL to your site.</p>
57+
<p>The optional full URL to your site.</p>
3858
<input name="aud" placeholder="https://push.example.com">
3959
</div>
4060
<div class="row">
4161
<label for="sub" ><b>Sub</b>scriber:</label>
42-
<p>The administrative email address that can be contacted if there's an issue</p>
62+
<p>The required administrative email address that can be contacted if there's an issue</p>
4363
<input name="sub" placeholder="mailto:admin@push.example.com">
4464
</div>
4565
<div class="row">
@@ -61,17 +81,20 @@ <h3>Claims JSON object:</h3>
6181
</div>
6282
</div>
6383
<div id="keys" class="section">
64-
<h2>Exported Keys</h2>
84+
<a name="export"><h2>Exported Keys</h2></a>
6585
<b>Auto-generated keys:</b>
66-
<p>These are ASN.1 DER formatted version of the public and private keys used to generate
67-
the VAPID headers. These can be useful for languages that use DER or PEM for key import.</p>
86+
<p>These are ASN.1 DER formatted version of the public and private keys used
87+
to generate the above VAPID headers. These can be useful for languages that
88+
use DER or PEM for key import.</p>
6889
<label for="priv">DER Private Key:</label><textarea name="priv"></textarea>
6990
<label for="pub">DER Public Key:</label><textarea name="pub"></textarea>
7091
</div>
7192
<div id="err" class="hidden section"></div>
7293
</div>
7394
</div>
7495
</main>
96+
<script src="der_lite.js"></script>
97+
<script src="common.js"></script>
7598
<script src="vapid.js"></script>
7699
<script>
77100

@@ -207,12 +230,6 @@ <h2>Exported Keys</h2>
207230
rclaims.innerHTML = JSON.stringify(claims, null, " ");
208231
rclaims.classList.add("updated");
209232
vapid.generate_keys().then(x => {
210-
vapid.export_private_der()
211-
.then(k => document.getElementsByName("priv")[0].value = k)
212-
.catch(er => error(er, "Private Key export failed"));
213-
vapid.export_public_der()
214-
.then(k => document.getElementsByName("pub")[0].value = k)
215-
.catch(er => error(er, "Public key export failed" ));
216233
vapid.sign(claims)
217234
.then(k => {
218235
let auth = document.getElementsByName("auth")[0];
@@ -223,10 +240,17 @@ <h2>Exported Keys</h2>
223240
crypt.classList.add('updated');
224241
let pk = document.getElementsByName("publicKey")[0];
225242
// Public Key is the crypto key minus the 'p256ecdsa='
226-
pk.value = k.publicKey;
243+
pk.innerHTML = k.publicKey;
227244
pk.classList.add('updated');
228245
})
229246
.catch(err => error(err, err_strs.enus.CLAIMS_FAIL));
247+
exporter = new DERLite();
248+
exporter.export_private_der(x.privateKey)
249+
.then(k => document.getElementsByName("priv")[0].value = k)
250+
.catch(er => error(er, "Private Key export failed"));
251+
exporter.export_public_der(x.publicKey)
252+
.then(k => document.getElementsByName("pub")[0].value = k)
253+
.catch(er => error(er, "Public key export failed" ));
230254
});
231255
} catch (ex) {
232256
error(ex, err_strs.enus.HEADER_NOPE);
@@ -269,6 +293,8 @@ <h2>Exported Keys</h2>
269293
document.getElementById("gen").addEventListener("click", gen);
270294
document.getElementById('vapid_exp').value = parseInt(Date.now()*.001) + 86400;
271295

296+
var vapid = new VapidToken();
297+
var mzcc = new MozCommon();
272298

273299
</script>
274300
</body>

0 commit comments

Comments
 (0)