-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcryptokey.js
491 lines (439 loc) · 14.8 KB
/
cryptokey.js
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
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
"use strict";
import * as Coze from './coze.js';
import * as Alg from './alg.js';
import * as CZK from './key.js';
import {
isEmpty
} from './coze.js';
export {
CryptoKey,
SigToLowS,
IsSigLowS,
};
/**
@typedef {import('./typedef.js').B64} B64
@typedef {import('./typedef.js').Alg} Alg
@typedef {import('./typedef.js').Sig} Sig
@typedef {import('./typedef.js').Hsh} Hsh
@typedef {import('./typedef.js').Key} Key
@typedef {import('./typedef.js').Crv} Crv
@typedef {import('./typedef.js').Msg} Msg
*/
var CryptoKey = {
/**
New returns a ECDSA CryptoKeyPair.
https://developer.mozilla.org/en-US/docs/Web/API/CryptoKeyPair
@param {Alg} [alg=ES256] - Alg of the key to generate. (e.g. "ES256")
@return {CryptoKeyPair}
@throws {error} Error, SyntaxError, DOMException, TypeError
*/
New: async function(alg) {
if (isEmpty(alg)) {
alg = Alg.Algs.ES256;
}
// Javascript only supports ECDSA, and doesn't support ES192 or ES224. See
// https://developer.mozilla.org/en-US/docs/Web/API/EcdsaParams
switch (alg) {
case Alg.Algs.ES256:
case Alg.Algs.ES384:
case Alg.Algs.ES512:
return await window.crypto.subtle.generateKey({
name: Alg.GenAlgs.ECDSA,
namedCurve: Alg.Curve(alg)
},
true,
["sign", "verify"]
);
default:
throw new Error("CryptoKey.New: Unsupported key algorithm:" + alg);
}
},
/**
FromCozeKey returns a Javascript CryptoKey from a Coze Key. Only supports
ECDSA because of Crypto.subtle limitations. Throws error on invalid keys.
https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/importKey#JSON_Web_Key
@param {Key} cozeKey Coze key.
@param {boolean} [public=false] Return only a public key.
@returns {CryptoKey}
@throws {error} Error, SyntaxError, DOMException, TypeError
*/
FromCozeKey: async function(cozeKey, onlyPublic) {
if (Alg.Genus(cozeKey.alg) != Alg.GenAlgs.ECDSA) {
throw new Error("CryptoKey.FromCozeKey: unsupported CryptoKey algorithm: " + cozeKey.alg);
}
// Create a new JWK that can be used to create and "import" a CryptoKey
var jwk = {};
jwk.use = Alg.Uses.Sig;
jwk.crv = Alg.Curve(cozeKey.alg);
jwk.kty = Alg.FamAlgs.EC;
let half = Alg.XSize(cozeKey.alg) / 2;
let xyab = await Coze.B64ToUint8Array(cozeKey.x);
jwk.x = await Coze.ArrayBufferTo64ut(xyab.slice(0, half));
jwk.y = await Coze.ArrayBufferTo64ut(xyab.slice(half));
// Public CryptoKey "crypto.subtle.importKey" needs key use to be "verify"
// even though this doesn't exist in JWK RFC or IANA registry. (2021/05/12)
// Gawd help us. Private CryptoKey needs key `use` to be "sign".
if (isEmpty(cozeKey.d) || onlyPublic) {
var signOrVerify = "verify";
} else {
signOrVerify = "sign";
jwk.d = cozeKey.d;
}
return await crypto.subtle.importKey(
"jwk",
jwk, {
name: Alg.GenAlgs.ECDSA,
namedCurve: jwk.crv,
},
true,
[signOrVerify]
);
},
/**
ToPublic accepts a Javascript CryptoKey and modifies the key to remove
any private components.
@param {CryptoKey} cryptoKey
@returns {void}
*/
ToPublic: async function(cryptoKey) {
delete cryptoKey.d; // Remove private `d` from the key.
// Only ["verify"] is a valid `key_ops` value for a public CryptoKey.
// `key_ops` must be an array.
cryptoKey.key_ops = ["verify"];
},
/**
CryptoKeyToCozeKey returns a Coze Key from Javascript's "CryptoKey" type.
(https://developer.mozilla.org/en-US/docs/Web/API/CryptoKey) Coze keys are
similiar to JOSE JWK's but has a few significant differences.
See the Coze docs for more on these differences.
- Coze Byte-to-string values are always b64ut, "RFC 4648 base64 URI Safe
Truncated".
- Coze keys also use the field `alg` to denote everything about the key:
it's use, hashing algorithm, curve, family, signature size, private
component size, public component size, etc...
- A Coze key's Thumbprint's hashing algorithm must always be in alignment
with the alg. This is unlike JOSE which appears to use SHA-256 even for
keys that don't use that algorithm.
This function currently only supports ECDSA (ES256. ES384, ES512) as
crypto.subtle only supports these ECDSA algorithms. From Cryptokey,
`exported` key output should is in the following form:
{
"crv": "P-256",
"d": "GwJgQIcbB29IfWO46QZwansE5XVVOg_CfafcpGk3K9I",
"key_ops": [
"sign",
"verify"
],
"kty": "EC",
"x": "bMgUwXPLFR5WPERFIdUR8f6J9znFlM4fL-TaYr7YNSo",
"y": "vuU0bE-JafF1zEW_MbL-oaO0eGltDeMHIfc_bxkdCHU",
"use": "sig"
}
Some aspects of the Javascript exported key are in conflict with JOSE. The
`delete`s below are for reference of how out of alignment the Javascript
representation is from JOSE. If for some reason a JOSE representation is
required, the deletes are suggested.
`delete exported.key_ops;`
According to RFC 7517 Section 4.3, "use" is mutually exclusive with
key_ops.
`delete exported["ext"];`
`ext` is define by the Web Cryptography API and does not appear in the
core JOSE RFC's. It stands for "extractable". Since the key is already
"extracted" we don't care, and we're not going to burden downstream with
it. However, this may need to be added again later if the key is further
manipulated by SubtleCrypto.
Coze does not use "crv", "kty", or "use" and instead relies solely on
"alg". Since alg is not given, it's assumed from `crv` while `kty`is
ignored.
Why are we exporting to JWK?
1. There's no access to the key fields without exporting. (The
browser hides the information from Javascript.)
2. The exporting formats are limited.
3. Can't export to "raw" because "raw" appears to only work on public
keys. This may be a private key.
@param {CryptoKey} cryptoKey
@returns {Key}
@throws {error}
*/
ToCozeKey: async function(cryptoKey) {
let exported = await window.crypto.subtle.exportKey(
"jwk",
cryptoKey
);
var czk = {};
czk.alg = await CryptoKey.algFromCrv(exported.crv);
// Concatenate x and y, but concatenation is done at the byte level, so:
// unencode, concatenated, and encoded.
let xui8 = Coze.B64ToUint8Array(exported.x);
let yui8 = Coze.B64ToUint8Array(exported.y);
var xyui8 = new Uint8Array([
...xui8,
...yui8,
]);
czk.x = Coze.ArrayBufferTo64ut(xyui8.buffer);
// Only private ECDSA keys have `d`.
if (exported.hasOwnProperty('d')) {
czk.d = exported.d;
}
czk.tmb = await CZK.Thumbprint(czk);
// console.log("exported: " + JSON.stringify(exported), "Coze Key: " + JSON.stringify(czk)); // Debugging
return czk;
},
/**
Uses a Javascript `CryptoKey` to sign a array buffer. Returns array buffer
bytes of the signature. Returns empty buffer on error.The signing algorithm's
hashing algorithm is used for the digest of the payload. Coze uses UTF-8.
https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/importKey#JSON_Web_Key
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer
@param {CryptoKey} cryptoKey
@param {ArrayBuffer} payloadBuffer
@returns {ArrayBuffer}
@throws {error}
*/
SignBuffer: async function(cryptoKey, arrayBuffer) {
let alg = await CryptoKey.algFromCrv(cryptoKey.algorithm.namedCurve);
let sig = await window.crypto.subtle.sign({
name: Alg.GenAlgs.ECDSA,
hash: {
name: Alg.HashAlg(alg)
},
},
cryptoKey,
arrayBuffer
);
sig = sigToLowSArrayBuffer(alg, sig);
return sig;
},
/**
SignBufferB64 signs a buffer with a CryptoKey and returns the b64ut
signature. The input is hashed before it's signed.
Coze uses UTF-8.
@param {CryptoKey} cryptoKey Private CryptoKey
@param {ArrayBuffer} arrayBuffer ArrayBuffer to sign.
@returns {B64}
*/
SignBufferB64: async function(cryptoKey, arrayBuffer) {
return await Coze.ArrayBufferTo64ut(await CryptoKey.SignBuffer(cryptoKey, arrayBuffer));
},
/**
SignString signs a string and returns the b64ut signature.
Coze uses UTF-8.
@param {CryptoKey} cryptoKey Private key used for signing.
@param {string} utf8 String to sign.
@returns {B64}
*/
SignString: async function(cryptoKey, utf8) {
return await CryptoKey.SignBufferB64(cryptoKey, await Coze.SToArrayBuffer(utf8));
},
/**
VerifyArrayBuffer verifies an ArrayBuffer msg with an ArrayBuffer sig and
Javascript CryptoKey.
Returns whether or not message is verified by the given key and signature.
@param {Alg} alg
@param {CryptoKey} cryptoKey Javascript CryptoKey.
@param {ArrayBuffer} sig Signature.
@param {ArrayBuffer} msg Message.
@returns {boolean}
*/
VerifyArrayBuffer: async function(alg, cryptoKey, msg, sig) {
// Currently, CozeJS is only ECDSA. For ECDSA, only accept low-S
// signatures.
if (!(await IsSigLowS(alg, sig))) {
return false;
}
// Guarantee key is not private to appease Javascript 😔:
await CryptoKey.ToPublic(cryptoKey);
return await window.crypto.subtle.verify({
name: Alg.GenAlgs.ECDSA,
hash: {
name: await CryptoKey.GetSignHashAlgoFromCryptoKey(cryptoKey)
},
},
cryptoKey,
sig,
msg);
},
/**
VerifyMsg uses a public key to verify a string msg with a b64ut sig.
Returns whether or not the signature is valid.
@param {Alg} alg
@param {CryptoKey} cryptoKey Javascript CryptoKey.
@param {Msg} msg String that was signed.
@param {Sig} sig B64 signature.
@returns {boolean}
*/
VerifyMsg: async function(alg, cryptoKey, msg, sig) {
return CryptoKey.VerifyArrayBuffer(alg, cryptoKey, await Coze.SToArrayBuffer(msg), await Coze.B64uToArrayBuffer(sig));
},
/**
GetSignHashAlgoFromCryptoKey gets the signing hashing algorithm from the
CryptoKey.
Returns the name of the hashing algorithm. E.g. "SHA-256".
Javascript's CryptoKey explicitly requires a signing hashing algorithm, but
the CryptoKey itself may not explicitly contain that information. For
example, a ES256 key will have the curve (P-256) and the general key type
(ECDSA), but the hashing algo is not explicitly stated (SHA-256), nor is
the algorithm explicitly stated (ES256).
However, for some CryptoKeys, the hashing algorithm is explicitly stated.
For example, "RsaHashedKeyGenParams" has the field "hash" which explicitly
denotes what hashing algorithm was used. As of 2021/05/26,
"EcKeyGenParams" has no such field, so it must be assumed that certain
hashing algorithms are paired with certain curves.
The purpose of this function is to return the correct hashing digest for
all CryptoKeys regardless of their form.
@param {CryptoKey} CryptoKey CryptoKey Javascript object.
@returns {Hsh}
@throws {error} Fails if alg is not supported.
*/
GetSignHashAlgoFromCryptoKey: async function(cryptoKey) {
return Alg.HashAlg(await CryptoKey.algFromCrv(cryptoKey.algorithm.namedCurve));
},
/**
algFromCrv returns a SEAlg from the given curve.
Fails if curve is not supported.
@param {Crv} src Curve type. E.g. "P-256".
@returns {Alg}
@throws {error}
*/
algFromCrv: async function(crv) {
switch (crv) {
case Alg.Curves.P224:
var alg = Alg.Algs.ES224;
break;
case Alg.Curves.P256:
alg = Alg.Algs.ES256
break;
case Alg.Curves.P384:
alg = Alg.Algs.ES384;
break;
case Alg.Curves.P521: // P-521 is not ES512/SHA-512. The curve != the alg/hash.
alg = Alg.Algs.ES512;
break;
default:
throw new Error("CryptoKey.ToCozeKey: Unsupported key algorithm.");
}
return alg;
}
}; // End CryptoKey
/**
Checks if S is a "low-S". See the Coze docs on "Low-S"
@param {Alg} alg
@param {BigInt} s
@returns {BigInt}
@throws {error}
*/
function IsLowS(alg, s) {
if (typeof s !== "bigint") {
throw new Error("IsLowS: s is not of type bigint");
}
return Alg.CurveHalfOrder(alg) > s;
}
/**
Makes sure that s is a "low-S". See the Coze docs on "Low-S" and the Go
package's "ToLowS" function.
@param {Alg} alg
@param {BigInt} s
@returns {BigInt}
@throws {error}
*/
function toLowS(alg, s) {
if (typeof s !== "bigint") {
throw new Error("toLowS: s is not of type bigint");
}
if (!IsLowS(alg, s)) {
return Alg.CurveOrder(alg) - s;
}
return s
}
/**
Makes sure that S in sig is a "low-S" and converts if needed. See the Coze docs
on "low-S"
@param {Alg} alg
@param {Sig} sig
@returns {Sig}
@throws {error}
*/
async function SigToLowS(alg, sig) {
let ab = await Coze.B64uToArrayBuffer(sig);
let lowSSigAB = await sigToLowSArrayBuffer(alg, ab);
return Coze.ArrayBufferTo64ut(lowSSigAB);
}
/** SigIsLowS checks if S in sig is a "low-S". See the Coze docs on "low-S"
@param {Alg} alg
@param {Sig} sig
@returns {boolean}
@throws {error}
*/
async function IsSigLowS(alg, sig) {
let bigIntS = await sigToS(alg, sig);
return IsLowS(alg, bigIntS);
}
/**
Returns S from sig.
@param {Alg} alg Return only a public key.
@param {ArrayBuffer} sig Sig ArrayBuffer from subtle crypto
@returns {BigInt}
@throws {error} Error, SyntaxError, DOMException, TypeError
*/
function sigToS(alg, sig) {
let half = Alg.SigSize(alg) / 2;
let s = sig.slice(half);
return arrayBufferToBigInt(s);
}
/**
sigToLowSArrayBuffer
@param {Alg} alg Return only a public key.
@param {ArrayBuffer} sig Sig ArrayBuffer from subtle crypto
@returns {ArrayBuffer}
@throws {error} Error, SyntaxError, DOMException, TypeError
*/
async function sigToLowSArrayBuffer(alg, sig) {
let half = Alg.SigSize(alg) / 2;
let r = sig.slice(0, half);
let s = sig.slice(half);
let bigIntS = arrayBufferToBigInt(s);
let bigIntNormS = toLowS(alg, bigIntS);
// console.log("sig in:", sig);
// console.log("r:", r);
// console.log("s:", s);
// console.log("s hex:", bigIntS.toString(16).toUpperCase());
// console.log("IsLowS: ", IsLowS(alg, bigIntS));
// console.log("Before toLowS", bigIntS)
// console.log("After toLowS", bigIntNormS)
let normS = bigIntToArrayBuffer(Alg.SigSize(alg) / 2, bigIntNormS);
// Add two ArrayBuffers, but it's Javascript so it's hard. 😔 This is just
// doing `sig = r + normS`;
var tmp = new Uint8Array(r.byteLength + normS.byteLength);
tmp.set(new Uint8Array(r), 0);
tmp.set(new Uint8Array(normS), r.byteLength);
sig = tmp.buffer;
return sig
}
/**
Converts a Big Endian ArrayBuffer to BigInt.
@param {ArrayBuffer} buffer
@returns {BigInt}
*/
function arrayBufferToBigInt(buffer) {
let result = 0n;
let a = new Uint8Array(buffer)
for (let i = 0; i < a.length; i++) {
result = (result << 8n) + BigInt(a[i]);
}
return result;
}
/** Converts a BigInt to a Big Endian ArrayBuffer.
@param {size} int // Number of bytes to pad the ArrayBuffer
@param {Bigint} bigInt
@returns {ArrayBuffer} buffer
*/
function bigIntToArrayBuffer(size, bigInt) {
const buffer = new ArrayBuffer(size);
const view = new DataView(buffer);
do {
size--;
view.setUint8(size, Number(bigInt & BigInt(0xff)));
bigInt >>= 8n;
} while (size > 0);
return buffer;
}