-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcoz.js
More file actions
395 lines (333 loc) · 11.5 KB
/
Copy pathcoz.js
File metadata and controls
395 lines (333 loc) · 11.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
"use strict";
import * as Can from './canon.js'; // import as "Can" since func "Canon" will conflict in `coz.join.js`.
import * as Enum from './alg.js';
import * as CZK from './key.js';
import * as CTK from './cryptokey.js';
export {
Sign,
SignPayRaw,
SignCozRaw,
Verify,
VerifyPay,
Meta,
// Base conversion
SToArrayBuffer,
B64uToArrayBuffer,
B64ToUint8Array,
ArrayBufferTo64ut,
// Helpers
isEmpty,
PayCanon,
MaxSafeTimestamp,
validateTimestamp,
RVK_MAX_SIZE,
}
/**
@typedef {import('./typedef.js').Alg} Alg
@typedef {import('./typedef.js').B64} B64
@typedef {import('./typedef.js').Coz} Coz
@typedef {import('./typedef.js').Pay} Pay
@typedef {import('./typedef.js').Sig} Sig
@typedef {import('./typedef.js').Key} Key
@typedef {import('./typedef.js').Can} Can
@typedef {import('./typedef.js').Meta} Meta
@typedef {import('./typedef.js').VerifiedArray} VerifiedArray
*/
// PayCanon is the standard coz.pay fields.
const PayCanon = ["alg", "now", "tmb", "typ"];
/**
Sign signs in place coz.pay. It populates/replaces alg, tmb, and now using
the given private Coz key. Returns the same, but updated, coz. The optional
canon is used to canonicalize pay before signing. If needing a coz without
alg, tmb, or now, use SignCozRaw.
Sign, SignCozRaw, and Verify assumes that object has no duplicate
fields since this is disallowed in Javascript.
@param {Coz} coz Object coz.
@param {Key} cozKey A private coz key.
@param {Can} [canon] Array for canonical keys.
@returns {Coz} Coz that may have been modified from given.
@throws {error} Fails on invalid key, parse error, mismatch fields.
*/
async function Sign(coz, cozKey, canon) {
if (CZK.IsRevoked(cozKey)) {
throw new Error("Sign: Cannot sign with revoked key.");
}
coz.pay.alg = cozKey.alg;
coz.pay.tmb = await CZK.Thumbprint(cozKey);
coz.pay.now = Math.round((Date.now() / 1000)); // Unix timestamp.
if (!isEmpty(canon)) {
coz.pay = await Can.Canonical(coz.pay, canon);
}
coz.sig = await SignPayRaw(JSON.stringify(coz.pay), cozKey);
return coz;
}
/**
SignPayRaw signs message with private Coz key and returns b64ut sig.
@param {Pay} pay ay. e.g. `{"alg"...}` May also be any message.
@param {Key} cozKey
@returns {Sig}
@throws {error} Error, SyntaxError, DOMException, TypeError
*/
async function SignPayRaw(pay, cozKey) {
return CTK.CryptoKey.SignBufferB64(
await CTK.CryptoKey.FromCozKey(cozKey),
await SToArrayBuffer(pay)
);
}
/**
SignCozRaw signs in place coz.pay with a private Coz key, but unlike
Sign, does not set `alg`, `tmb` or `now`. The optional canon is used to
canonicalize pay before signing.
@param {Coz} coz Object coz.
@param {Key} cozKey A private coz key.
@param {Can} [canon] Array for canonical keys.
@returns {Coz} Coz with new `sig` and canonicalized `pay`.
@throws {error} Fails on rvk or mismatch `alg` or `tmb`.
*/
async function SignCozRaw(coz, cozKey, canon) {
if (CZK.IsRevoked(cozKey)) {
throw new Error("SignCozRaw: Cannot sign with revoked key.");
}
if (!isEmpty(coz.pay.alg) && coz.pay.alg !== cozKey.alg) {
throw new Error("SignCozRaw: Coz key alg mismatch with coz.pay.alg.");
}
if (!isEmpty(coz.pay.tmb) && coz.pay.tmb !== cozKey.tmb) {
throw new Error("SignCozRaw: Coz key tmb mismatch with coz.pay.tmb.");
}
if (!isEmpty(canon)) {
coz.pay = await Can.Canonical(coz.pay, canon);
}
coz.sig = await SignPayRaw(JSON.stringify(coz.pay), cozKey);
return coz;
}
/**
Verify returns a whether or not the Coz is valid. coz.sig must be set.
If set, pay.alg and pay.tmb must match with cozKey.
@param {Coz} coz Coz with signed pay. e.g. `{"pay":..., "sig":...}`
@param {Key} [cozKey] Public Coz key for verification.
@param {Sig} [sig] Signature.
@return {boolean}
@throws {error}
*/
async function Verify(coz, cozKey) {
if (!isEmpty(coz.pay.alg) && coz.pay.alg !== cozKey.alg) {
throw new Error("Verify: Coz key alg mismatch with coz.pay.alg.");
}
if (!isEmpty(coz.pay.tmb) && coz.pay.tmb !== cozKey.tmb) {
throw new Error("Verify: Coz key tmb mismatch with coz.pay.tmb.");
}
// Spec: "rvk and now must be a positive integer less than 2^53 – 1".
if (!isEmpty(coz.pay.now)) {
validateTimestamp(coz.pay.now);
}
// Enforce revoke message max size to prevent DoS.
if (!isEmpty(coz.pay.rvk) && coz.pay.rvk > 0 && RVK_MAX_SIZE > 0) {
let paySize = JSON.stringify(coz.pay).length;
if (paySize > RVK_MAX_SIZE) {
throw new Error(`Verify: revoke message size ${paySize} exceeds RVK_MAX_SIZE ${RVK_MAX_SIZE}`);
}
}
return VerifyPay(JSON.stringify(coz.pay), cozKey, coz.sig);
}
/**
VerifyPay verifies a `pay` with `sig` and returns whether or not the message is
verified. Verify does no Coz checks. If checks are needed, use
Verify();
@param {Pay} pay pay. e.g. `{"alg"...}` May also be any message.
@param {Key} cozKey Coz key for validation.
@param {Sig} sig Signature.
@return {boolean}
@throws {error}
*/
async function VerifyPay(pay, cozKey, sig) {
return CTK.CryptoKey.VerifyMsg(
cozKey.alg,
await CTK.CryptoKey.FromCozKey(cozKey, true),
pay,
sig,
);
};
/**
Meta calculates a Meta object with the fields [alg,now,tmb,typ,can,cad,sig,czd]
derived from the given coz. Meta always calculates `can`, `cad`, if populated
from pay [alg,now,tmb,typ] are copied, and calculates `czd` if `sig` is set. Pay
must be set even if it is an empty object. Either Coz.Pay.Alg or parameter alg
must be set. If Coz.Sig is populated, `czd` is set. The empty coz (A coz with
an empty pay but sig is set) is legitimate input for Meta.
Errors when
1. Pay doesn't exist.
2. No alg is given (both coz.pay.alg and alg are empty).
3. Pay.Alg doesn't match parameter alg if both are set.
Meta does no cryptographic verification.
@param {Coz} coz coz.
@param {Alg} [alg] coz.pay.alg takes precedence.
@return {Meta} Meta object [alg,now,tmb,typ,can,cad,sig,czd].
@throws {error}
*/
async function Meta(coz, alg) {
if (isEmpty(coz.pay)) {
throw new Error("Meta: coz.pay must exist.")
}
let meta = {}
// Alg check section. Assumes later call to CanonicalHas64() errors on bad or empty alg.
if (isEmpty(alg)) {
if (isEmpty(coz.pay.alg)) {
throw new Error("Meta: either coz.pay.alg or parameter alg must be set.")
}
meta.alg = coz.pay.alg
} else {
meta.alg = alg
}
if (!isEmpty(coz.pay.alg) && meta.alg !== coz.pay.alg) {
throw new Error(`Meta: coz.pay.alg (${coz.pay.alg}) and parameter alg (${alg}) do not match. `)
}
if (!isEmpty(coz.pay.now)) {
meta.now = coz.pay.now
}
if (!isEmpty(coz.pay.tmb)) {
meta.tmb = coz.pay.tmb
}
if (!isEmpty(coz.pay.typ)) {
meta.typ = coz.pay.typ
}
meta.can = await Can.Canon(coz.pay)
meta.cad = await Can.CanonicalHash64(coz.pay, Enum.HashAlg(meta.alg));
if (!isEmpty(coz.sig)) {
meta.sig = coz.sig
meta.czd = await Can.CanonicalHash64({
cad: meta.cad,
sig: meta.sig
}, Enum.HashAlg(meta.alg));
}
return meta;
}
///////////////////////////////////
// Base Conversion
///////////////////////////////////
/**
Converts a string (UTF-8) to an ArrayBuffer.
@param {string} string
@return {ArrayBuffer}
*/
async function SToArrayBuffer(string) {
return new TextEncoder().encode(string).buffer; // Suppose to be always in UTF-8
}
/**
B64uToArrayBuffer takes a b64 (truncated or not truncated, padded or not
padded) UTF-8 string and decodes it to an ArrayBuffer.
@param {B64} string
@returns {ArrayBuffer}
*/
function B64uToArrayBuffer(string) {
return B64ToUint8Array(string).buffer;
};
/**
B64ToUint8Array takes a b64 string (truncated or not truncated, padded or not
padded) and decodes it back into a string.
@param {B64} string
@returns {Uint8Array}
*/
function B64ToUint8Array(string) {
// Make sure that the encoding is canonical. See issue "Enforce Canonical
// Base64 encoding" https://github.com/Cyphrme/Coz/issues/18. Alternatively
// to this method, we could write our own encoder as Mozilla suggests.
// https://developer.mozilla.org/en-US/docs/Glossary/Base64#solution_1_%E2%80%93_escaping_the_string_before_encoding_it
string = string.replace(/-/g, '+').replace(/_/g, '/')
let reencode = btoa(atob(string)).replace(/=/g, '')
if (reencode !== string) {
throw new Error('Non-canonical base64 string');
}
// atob doesn't care about the padding character '=', but does not like URI
// encoding.
return Uint8Array.from(atob(string), c => c.charCodeAt(0));
};
/**
ArrayBufferTo64ut returns a b64 string from an Array buffer.
@param {ArrayBuffer} buffer Arbitrary bytes. UTF-16 is Javascript native.
@returns {B64}
*/
function ArrayBufferTo64ut(buffer) {
return btoa(String.fromCharCode.apply(null, new Uint8Array(buffer))).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
}
///////////////////////////////////
// Helpers - Taken from Cyphr.me
///////////////////////////////////
/**
isEmpty is a helper function to determine if thing is empty.
Functions are considered always not empty.
Arrays: Only if an array has no elements it is empty. isEmpty does not check
element contents. (For item contents, do: `isEmpty(array[0])`)
Objects are empty if they have no keys. (Returns len === 0 of object keys.)
NaN returns true. (NaN === NaN is always false, as NaN is never equal to
anything. NaN is the only JavaScript value unequal to itself.)
Don't use on HTMl elements. For HTML elements, use the !== equality check
(element !== null). TODO fix this
Cannot use CryptoKey with this function since (len === 0) always.
@param {any} thing Thing you wish was empty.
@returns {boolean} Boolean.
*/
function isEmpty(thing) {
if (typeof thing === 'function') {
return false
}
if (Array.isArray(thing)) {
if (thing.length == 0) {
return true
}
}
if (thing === Object(thing)) {
if (Object.keys(thing).length === 0) {
return true
}
return false
}
if (!isBool(thing)) {
return true
}
return false
}
/**
isBool is a helper function to determine boolean.
Javascript, instead of considering everything false except a few key words,
decided everything is true instead of a few key words. Why? Because
Javascript. This function inverts that assumption, so that everything can be
considered false unless true.
@param {any} bool Thing that you wish was a boolean.
@returns {boolean} An actual boolean.
*/
function isBool(bool) {
if (
bool === false ||
bool === "false" ||
bool === undefined ||
bool === "undefined" ||
bool === "" ||
bool === 0 ||
bool === "0" ||
bool === null ||
bool === "null" ||
bool === "NaN" ||
Number.isNaN(bool) ||
bool === Object(bool) // isObject
) {
return false
}
return true
}
// MaxSafeTimestamp is the maximum valid timestamp value: 2^53 - 1.
const MaxSafeTimestamp = 9007199254740991;
// RVK_MAX_SIZE is the maximum allowed payload size in bytes for revoke
// messages. Prevents DoS via oversized revoke payloads. Set to 0 to
// disable. Default is 2048 bytes. Matches Go's coz.RVK_MAX_SIZE.
let RVK_MAX_SIZE = 2048;
/**
validateTimestamp checks whether a timestamp is within the allowed range
[0, 2^53 - 1]. Throws if invalid. Matches Go's Timestamp.Valid().
@param {number} t Timestamp value.
@throws {Error} If t is outside the valid range.
*/
function validateTimestamp(t) {
if (t < 0 || t > MaxSafeTimestamp) {
throw new Error(`validateTimestamp: value ${t} is invalid. Must be between 0 and ${MaxSafeTimestamp} inclusive`);
}
}