-
Notifications
You must be signed in to change notification settings - Fork 22
/
macaroon.js
1198 lines (1125 loc) · 36.6 KB
/
macaroon.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
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/**
A JavaScript implementation of
[macaroons](http://theory.stanford.edu/~ataly/Papers/macaroons.pdf)
compatible with the [Go](http://github.com/go-macaroon/macaroon),
[Python, and C ](https://github.com/rescrv/libmacaroons)
implementations. Including functionality to interact with
third party caveat dischargers implemented by the [Go macaroon
bakery](http://github.com/go-macaroon-bakery/macaroon-bakery).
It supports both version 1 and 2 macaroons in JSON and binary formats.
@module macaroon
*/
'use strict';
const sjcl = require('sjcl');
const nacl = require('tweetnacl');
const naclutil = require('tweetnacl-util');
let TextEncoder, TextDecoder;
if (typeof window !== 'undefined' && window && window.TextEncoder) {
TextEncoder = window.TextEncoder;
TextDecoder = window.TextDecoder;
} else {
// No window.TextEncoder if it's node.js.
const util = require('util');
TextEncoder = util.TextEncoder;
TextDecoder = util.TextDecoder;
}
const utf8Encoder = new TextEncoder();
const utf8Decoder = new TextDecoder('utf-8', {fatal : true});
const NONCELEN = 24;
const FIELD_EOS = 0;
const FIELD_LOCATION = 1;
const FIELD_IDENTIFIER = 2;
const FIELD_VID = 4;
const FIELD_SIGNATURE = 6;
const maxInt = Math.pow(2, 32)-1;
/**
* Return a form of x suitable for including in a error message.
* @param {any} x The object to be converted to string form.
* @returns {string} - The converted object.
*/
const toString = function(x) {
if (x instanceof Array) {
// Probably bitArray, try to convert it.
try {x = bitsToBytes(x);} catch (e) {}
}
if (x instanceof Uint8Array) {
if (isValidUTF8(x)) {
x = bytesToString(x);
} else {
return `b64"${bytesToBase64(x)}"`;
}
}
if (typeof x === 'string') {
// TODO quote embedded double-quotes?
return `"${x}"`;
}
return `type ${typeof x} (${JSON.stringify(x)})`;
};
const ByteBuffer = class ByteBuffer {
/**
* Create a new ByteBuffer. A ByteBuffer holds
* a Uint8Array that it grows when written to.
* @param {int} capacity The initial capacity of the buffer.
*/
constructor(capacity) {
this._buf = new Uint8Array(capacity);
this._length = 0;
}
/**
* Append several bytes to the buffer.
* @param {Uint8Array} bytes The bytes to append.
*/
appendBytes(bytes) {
this._grow(this._length + bytes.length);
this._buf.set(bytes, this._length);
this._length += bytes.length;
}
/**
* Append a single byte to the buffer.
* @param {int} byte The byte to append
*/
appendByte(byte) {
this._grow(this._length + 1);
this._buf[this._length] = byte;
this._length++;
}
/**
* Append a variable length integer to the buffer.
* @param {int} x The number to append.
*/
appendUvarint(x) {
if (x > maxInt || x < 0) {
throw new RangeError(`varint ${x} out of range`);
}
this._grow(this._length + maxVarintLen32);
let i = this._length;
while(x >= 0x80) {
this._buf[i++] = (x & 0xff) | 0x80;
x >>>= 7;
}
this._buf[i++] = x | 0;
this._length = i;
}
/**
* Return everything that has been appended to the buffer.
* Note that the returned array is shared with the internal buffer.
* @returns {Uint8Array} - The buffer.
*/
get bytes() {
return this._buf.subarray(0, this._length);
}
/**
* Grow the internal buffer so that it's at least as big as minCap.
* @param {int} minCap The minimum new capacity.
*/
_grow(minCap) {
const cap = this._buf.length;
if (minCap <= cap) {
return;
}
// Could use more intelligent logic to grow more slowly on large buffers
// but this should be fine for macaroon use.
const doubleCap = cap * 2;
const newCap = minCap > doubleCap ? minCap : doubleCap;
const newContent = new Uint8Array(newCap);
newContent.set(this._buf.subarray(0, this._length));
this._buf = newContent;
}
};
const maxVarintLen32 = 5;
const ByteReader = class ByteReader {
/**
* Create a new ByteReader that reads from the given buffer.
* @param {Uint8Array} bytes The buffer to read from.
*/
constructor(bytes) {
this._buf = bytes;
this._index = 0;
}
/**
* Read a byte from the buffer. If there are no bytes left in the
* buffer, throws a RangeError exception.
* @returns {int} - The read byte.
*/
readByte() {
if (this.length <= 0) {
throw new RangeError('Read past end of buffer');
}
return this._buf[this._index++];
}
/**
* Inspect the next byte without consuming it.
* If there are no bytes left in the
* buffer, throws a RangeError exception.
* @returns {int} - The peeked byte.
*/
peekByte() {
if (this.length <= 0) {
throw new RangeError('Read past end of buffer');
}
return this._buf[this._index];
}
/**
* Read a number of bytes from the buffer.
* If there are not enough bytes left in the buffer,
* throws a RangeError exception.
* @param {int} n The number of bytes to read.
*/
readN(n) {
if (this.length < n) {
throw new RangeError('Read past end of buffer');
}
const bytes = this._buf.subarray(this._index, this._index + n);
this._index += n;
return bytes;
}
/**
* Return the size of the buffer.
* @returns {int} - The number of bytes left to read in the buffer.
*/
get length() {
return this._buf.length - this._index;
}
/**
* Read a variable length integer from the buffer.
* If there are not enough bytes left in the buffer
* or the encoded integer is too big, throws a
* RangeError exception.
* @returns {int} - The number that's been read.
*/
readUvarint() {
const length = this._buf.length;
let x = 0;
let shift = 0;
for(let i = this._index; i < length; i++) {
const b = this._buf[i];
if (b < 0x80) {
const n = i - this._index;
this._index = i+1;
if (n > maxVarintLen32 || n === maxVarintLen32 && b > 1) {
throw new RangeError('Overflow error decoding varint');
}
return (x | (b << shift)) >>> 0;
}
x |= (b & 0x7f) << shift;
shift += 7;
}
this._index = length;
throw new RangeError('Buffer too small decoding varint');
}
};
const isValue = x => x !== undefined && x !== null;
/**
* Convert a string to a Uint8Array by utf-8
* encoding it.
* @param {string} s The string to convert.
* @returns {Uint8Array}
*/
const stringToBytes = s => isValue(s) ? utf8Encoder.encode(s) : s;
/**
* Convert a Uint8Array to a string by
* utf-8 decoding it. Throws an exception if
* the bytes do not represent well-formed utf-8.
* @param {Uint8Array} b The bytes to convert.
* @returns {string}
*/
const bytesToString = b => isValue(b) ? utf8Decoder.decode(b) : b;
/**
* Convert an sjcl bitArray to a string by
* utf-8 decoding it. Throws an exception if
* the bytes do not represent well-formed utf-8.
* @param {bitArray} s The bytes to convert.
* @returns {string}
*/
const bitsToString = s => sjcl.codec.utf8String.fromBits(s);
/**
* Convert a base64 string to a Uint8Array by decoding it.
* It copes with unpadded and URL-safe base64 encodings.
* @param {string} s The base64 string to decode.
* @returns {Uint8Array} - The decoded bytes.
* @alias module:macaroon
*/
const base64ToBytes = function(s) {
s = s.replace(/-/g, '+').replace(/_/g, '/');
if (s.length % 4 !== 0 && !s.match(/=$/)) {
// Add the padding that's required by base64-js.
s += '='.repeat(4 - s.length % 4);
}
return naclutil.decodeBase64(s);
};
/** Convert a Uint8Array to a base64-encoded string
* using URL-safe, unpadded encoding.
* @param {Uint8Array} bytes The bytes to encode.
* @returns {string} - The base64-encoded result.
* @alias module:macaroon
*/
const bytesToBase64 = function(bytes) {
return naclutil.encodeBase64(bytes)
.replace(/=+$/, '')
.replace(/\+/g, '-')
.replace(/\//g, '_');
};
/**
Converts a Uint8Array to a bitArray for use by nacl.
@param {Uint8Array} arr The array to convert.
@returns {bitArray} - The converted array.
*/
const bytesToBits = function(arr) {
// See https://github.com/bitwiseshiftleft/sjcl/issues/344 for why
// we cannot just use sjcl.codec.bytes.toBits.
return sjcl.codec.base64.toBits(naclutil.encodeBase64(arr));
};
/**
Converts a bitArray to a Uint8Array.
@param {bitArray} arr The array to convert.
@returns {Uint8Array} - The converted array.
*/
const bitsToBytes = function(arr) {
// See https://github.com/bitwiseshiftleft/sjcl/issues/344 for why
// we cannot just use sjcl.codec.bytes.toBits.
return naclutil.decodeBase64(sjcl.codec.base64.fromBits(arr));
};
/**
Converts a hex to Uint8Array
@param {String} hex The hex value to convert.
@returns {Uint8Array}
*/
const hexToBytes = function(hex) {
const arr = new Uint8Array(Math.ceil(hex.length / 2));
for (let i = 0; i < arr.length; i++) {
arr[i] = parseInt(hex.substr(i * 2, 2), 16);
}
return arr;
};
/**
* Report whether the argument encodes a valid utf-8 string.
* @param {Uint8Array} bytes The bytes to check.
* @returns {boolean} - True if the bytes are valid utf-8.
*/
const isValidUTF8 = function(bytes) {
try {
bytesToString(bytes);
} catch (e) {
// While https://encoding.spec.whatwg.org states that the
// exception should be a TypeError, we'll be defensive here
// and just treat any exception as signifying invalid utf-8.
return false;
}
return true;
};
/**
Check that supplied value is a string and return it. Throws an
error including the provided label if not.
@param {String} val The value to assert as a string
@param {String} label The value label.
@returns {String} - The supplied value.
*/
const requireString = function(val, label) {
if (typeof val !== 'string') {
throw new TypeError(`${label} has the wrong type; want string, got ${typeof val}.`);
}
return val;
};
/**
Check that supplied value is a string or undefined or null. Throws
an error including the provided label if not. Always returns a string
(the empty string if undefined or null).
@param {(String | null)} val The value to assert as a string
@param {String} label The value label.
@returns {String} - The supplied value or an empty string.
*/
const maybeString = (val, label) => isValue(val) ? requireString(val, label) : '';
/**
Check that supplied value is a Uint8Array or a string.
Throws an error
including the provided label if not.
@param {(Uint8Array | string)} val The value to assert as a Uint8Array
@param {string} label The value label.
@returns {Uint8Array} - The supplied value, utf-8-encoded if it was a string.
*/
const requireBytes = function(val, label) {
if (val instanceof Uint8Array) {
return val;
}
if (typeof(val) === 'string') {
return stringToBytes(val);
}
throw new TypeError(`${label} has the wrong type; want string or Uint8Array, got ${typeof val}.`);
};
const emptyBytes = new Uint8Array();
/**
* Read a macaroon V2 field from the buffer. If the
* field does not have the expected type, throws an exception.
* @param {ByteReader} buf The buffer to read from.
* @param {int} expectFieldType The required field type.
* @returns {Uint8Array} - The contents of the field.
*/
const readFieldV2 = function(buf, expectFieldType) {
const fieldType = buf.readByte();
if (fieldType !== expectFieldType) {
throw new Error(`Unexpected field type, got ${fieldType} want ${expectFieldType}`);
}
if (fieldType === FIELD_EOS) {
return emptyBytes;
}
return buf.readN(buf.readUvarint());
};
/**
* Append a macaroon V2 field to the buffer.
* @param {ByteBuffer} buf The buffer to append to.
* @param {int} fieldType The type of the field.
* @param {Uint8Array} data The content of the field.
*/
const appendFieldV2 = function(buf, fieldType, data) {
buf.appendByte(fieldType);
if (fieldType !== FIELD_EOS) {
buf.appendUvarint(data.length);
buf.appendBytes(data);
}
};
/**
* Read an optionally-present macaroon V2 field from the buffer.
* If the field is not present, returns null.
* @param {ByteReader} buf The buffer to read from.
* @param {int} maybeFieldType The expected field type.
* @returns {Uint8Array | null} - The contents of the field, or null if not present.
*/
const readFieldV2Optional = function(buf, maybeFieldType) {
if (buf.peekByte() !== maybeFieldType) {
return null;
}
return readFieldV2(buf, maybeFieldType);
};
/**
* Sets a field in a V2 encoded JSON object.
* @param {Object} obj The JSON object.
* @param {string} key The key to set.
* @param {Uint8Array} valBytes The key's value.
*/
const setJSONFieldV2 = function(obj, key, valBytes) {
if (isValidUTF8(valBytes)) {
obj[key] = bytesToString(valBytes);
} else {
obj[key + '64'] = bytesToBase64(valBytes);
}
};
/**
Generate a hash using the supplied data.
@param {bitArray} keyBits
@param {bitArray} dataBits
@returns {bitArray} - The keyed hash of the supplied data as a sjcl bitArray.
*/
const keyedHash = function(keyBits, dataBits) {
const hash = new sjcl.misc.hmac(keyBits, sjcl.hash.sha256);
hash.update(dataBits);
return hash.digest();
};
/**
Generate a hash keyed with key of both data objects.
@param {bitArray} keyBits
@param {bitArray} d1Bits
@param {bitArray} d2Bits
@returns {bitArray} - The keyed hash of d1 and d2 as a sjcl bitArray.
*/
const keyedHash2 = function(keyBits, d1Bits, d2Bits) {
const h1Bits = keyedHash(keyBits, d1Bits);
const h2Bits = keyedHash(keyBits, d2Bits);
return keyedHash(keyBits, sjcl.bitArray.concat(h1Bits, h2Bits));
};
const keyGeneratorBits = bytesToBits(stringToBytes('macaroons-key-generator'));
/**
Generate a fixed length key for use as a nacl secretbox key.
@param {bitArray} keyBits The key to convert.
@returns {bitArray}
*/
const makeKey = function(keyBits) {
return keyedHash(keyGeneratorBits, keyBits);
};
/**
Generate a random nonce as Uint8Array.
@returns {Uint8Array}
*/
const newNonce = function() {
return nacl.randomBytes(NONCELEN);
};
/**
Encrypt the given plaintext with the given key.
@param {bitArray} keyBits encryption key.
@param {bitArray} textBits plaintext.
@returns {bitArray} - encrypted text.
*/
const encrypt = function(keyBits, textBits) {
const keyBytes = bitsToBytes(keyBits);
const textBytes = bitsToBytes(textBits);
const nonceBytes = newNonce();
const dataBytes = nacl.secretbox(textBytes, nonceBytes, keyBytes);
const ciphertextBytes = new Uint8Array(nonceBytes.length + dataBytes.length);
ciphertextBytes.set(nonceBytes, 0);
ciphertextBytes.set(dataBytes, nonceBytes.length);
return bytesToBits(ciphertextBytes);
};
/**
Decrypts the given cyphertext.
@param {bitArray} keyBits decryption key.
@param {bitArray} ciphertextBits encrypted text.
@returns {bitArray} - decrypted text.
*/
const decrypt = function(keyBits, ciphertextBits) {
const keyBytes = bitsToBytes(keyBits);
const ciphertextBytes = bitsToBytes(ciphertextBits);
const nonceBytes = ciphertextBytes.slice(0, NONCELEN);
const dataBytes = ciphertextBytes.slice(NONCELEN);
let textBytes = nacl.secretbox.open(dataBytes, nonceBytes, keyBytes);
if (!textBytes) {
throw new Error('decryption failed');
}
return bytesToBits(textBytes);
};
const zeroKeyBits = bytesToBits(stringToBytes('\0'.repeat(32)));
/**
Bind a given macaroon to the given signature of its parent macaroon. If the
keys already match then it will return the rootSig.
@param {bitArray} rootSigBits
@param {bitArray} dischargeSigBits
@returns {bitArray} - The bound macaroon signature.
*/
const bindForRequest = function(rootSigBits, dischargeSigBits) {
if (sjcl.bitArray.equal(rootSigBits, dischargeSigBits)) {
return rootSigBits;
}
return keyedHash2(zeroKeyBits, rootSigBits, dischargeSigBits);
};
const Macaroon = class Macaroon {
/**
Create a new Macaroon with the given root key, identifier, location
and signature.
@param {Object} params The necessary values to generate a macaroon.
It contains the following fields:
identifierBytes: {Uint8Array}
locationStr: {string}
caveats: {Array of {locationStr: string, identifierBytes: Uint8Array, vidBytes: Uint8Array}}
signatureBytes: {Uint8Array}
version: {int} The version of macaroon to create.
*/
constructor(params) {
if (!params) {
// clone uses null parameters.
return;
}
let {version, identifierBytes, locationStr, caveats, signatureBytes} = params;
if (version !== 1 && version !== 2) {
throw new Error(`Unexpected version ${version}`);
}
this._version = version;
this._locationStr = locationStr;
identifierBytes = requireBytes(identifierBytes, 'Identifier');
if (version === 1 && !isValidUTF8(identifierBytes)) {
throw new Error('Version 1 macaroon identifier must be well-formed UTF-8');
}
this._identifierBits = identifierBytes && bytesToBits(identifierBytes);
this._signatureBits = signatureBytes && bytesToBits(requireBytes(signatureBytes, 'Signature'));
this._caveats = caveats ? caveats.map(cav => {
const identifierBytes = requireBytes(cav.identifierBytes, 'Caveat identifier');
if (version === 1 && !isValidUTF8(identifierBytes)) {
throw new Error('Version 1 caveat identifier must be well-formed UTF-8');
}
return {
_locationStr: maybeString(cav.locationStr),
_identifierBits: bytesToBits(identifierBytes),
_vidBits: cav.vidBytes && bytesToBits(requireBytes(cav.vidBytes, 'Verification ID')),
};
}) : [];
}
/**
* Return the caveats associated with the macaroon,
* as an array of caveats. A caveat is represented
* as an object with an identifier field (Uint8Array)
* and (for third party caveats) a location field (string),
* and verification id (Uint8Array).
* @returns {Array} - The macaroon's caveats.
* @alias module:macaroon
*/
get caveats() {
return this._caveats.map(cav => {
return isValue(cav._vidBits) ? {
identifier: bitsToBytes(cav._identifierBits),
location: cav._locationStr,
vid: bitsToBytes(cav._vidBits),
} : {
identifier: bitsToBytes(cav._identifierBits),
};
});
}
/**
* Return the location of the macaroon.
* @returns {string} - The macaroon's location.
* @alias module:macaroon
*/
get location() {
return this._locationStr;
}
/**
* Return the macaroon's identifier.
* @returns {Uint8Array} - The macaroon's identifier.
* @alias module:macaroon
*/
get identifier() {
return bitsToBytes(this._identifierBits);
}
/**
* Return the signature of the macaroon.
* @returns {Uint8Array} - The macaroon's signature.
* @alias module:macaroon
*/
get signature() {
return bitsToBytes(this._signatureBits);
}
/**
Adds a third party caveat to the macaroon. Using the given shared root key,
caveat id and location hint. The caveat id should encode the root key in
some way, either by encrypting it with a key known to the third party or by
holding a reference to it stored in the third party's storage.
@param {Uint8Array} rootKeyBytes
@param {(Uint8Array | string)} caveatIdBytes
@param {String} [locationStr]
@alias module:macaroon
*/
addThirdPartyCaveat(rootKeyBytes, caveatIdBytes, locationStr) {
const cav = {
_identifierBits: bytesToBits(requireBytes(caveatIdBytes, 'Caveat id')),
_vidBits: encrypt(
this._signatureBits,
makeKey(bytesToBits(requireBytes(rootKeyBytes, 'Caveat root key')))),
_locationStr: maybeString(locationStr),
};
this._signatureBits = keyedHash2(
this._signatureBits,
cav._vidBits,
cav._identifierBits
);
this._caveats.push(cav);
}
/**
Adds a caveat that will be verified by the target service.
@param {String | Uint8Array} caveatIdBytes
@alias module:macaroon
*/
addFirstPartyCaveat(caveatIdBytes) {
const identifierBits = bytesToBits(requireBytes(caveatIdBytes, 'Condition'));
this._caveats.push({
_identifierBits: identifierBits,
});
this._signatureBits = keyedHash(this._signatureBits, identifierBits);
}
/**
Binds the macaroon signature to the given root signature.
This must be called on discharge macaroons with the primary
macaroon's signature before sending the macaroons in a request.
@param {Uint8Array} rootSig
@alias module:macaroon
*/
bindToRoot(rootSig) {
const rootSigBits = bytesToBits(requireBytes(rootSig, 'Primary macaroon signature'));
this._signatureBits = bindForRequest(rootSigBits, this._signatureBits);
}
/**
Returns a copy of the macaroon. Any caveats added to the returned macaroon
will not effect the original.
@returns {Macaroon} - The cloned macaroon.
@alias module:macaroon
*/
clone() {
const m = new Macaroon(null);
m._version = this._version;
m._signatureBits = this._signatureBits;
m._identifierBits = this._identifierBits;
m._locationStr = this._locationStr;
m._caveats = this._caveats.slice();
return m;
}
/**
Verifies that the macaroon is valid. Throws exception if verification fails.
@param {Uint8Array} rootKeyBytes Must be the same that the macaroon was
originally created with.
@param {Function} check Called to verify each first-party caveat. It
is passed the condition to check (a string) and should return an error string if the condition
is not met, or null if satisfied.
@param {Array} discharges
@alias module:macaroon
*/
verify(rootKeyBytes, check, discharges = []) {
const rootKeyBits = makeKey(bytesToBits(requireBytes(rootKeyBytes, 'Root key')));
const used = discharges.map(d => 0);
this._verify(this._signatureBits, rootKeyBits, check, discharges, used);
discharges.forEach((dm, i) => {
if (used[i] === 0) {
throw new Error(
`discharge macaroon ${toString(dm.identifier)} was not used`);
}
if (used[i] !== 1) {
// Should be impossible because of check in verify, but be defensive.
throw new Error(
`discharge macaroon ${toString(dm.identifier)} was used more than once`);
}
});
}
_verify(rootSigBits, rootKeyBits, check, discharges, used) {
let caveatSigBits = keyedHash(rootKeyBits, this._identifierBits);
this._caveats.forEach(caveat => {
if (caveat._vidBits) {
const cavKeyBits = decrypt(caveatSigBits, caveat._vidBits);
let found = false;
let di, dm;
for (di = 0; di < discharges.length; di++) {
dm = discharges[di];
if (!sjcl.bitArray.equal(dm._identifierBits, caveat._identifierBits)) {
continue;
}
found = true;
// It's important that we do this before calling _verify,
// as it prevents potentially infinite recursion.
used[di]++;
if (used[di] > 1) {
throw new Error(
`discharge macaroon ${toString(dm.identifier)} was used more than once`);
}
dm._verify(rootSigBits, cavKeyBits, check, discharges, used);
break;
}
if (!found) {
throw new Error(
`cannot find discharge macaroon for caveat ${toString(caveat._identifierBits)}`);
}
caveatSigBits = keyedHash2(caveatSigBits, caveat._vidBits, caveat._identifierBits);
} else {
const cond = bitsToString(caveat._identifierBits);
const err = check(cond);
if (err) {
throw new Error(`caveat check failed (${cond}): ${err}`);
}
caveatSigBits = keyedHash(caveatSigBits, caveat._identifierBits);
}
});
const boundSigBits = bindForRequest(rootSigBits, caveatSigBits);
if (!sjcl.bitArray.equal(boundSigBits, this._signatureBits)) {
throw new Error('signature mismatch after caveat verification');
}
}
/**
Exports the macaroon to a JSON-serializable object.
The version used depends on what version the
macaroon was created with or imported from.
@returns {Object}
@alias module:macaroon
*/
exportJSON() {
switch (this._version) {
case 1:
return this._exportAsJSONObjectV1();
case 2:
return this._exportAsJSONObjectV2();
default:
throw new Error(`unexpected macaroon version ${this._version}`);
}
}
/**
Returns a JSON compatible object representation of this version 1 macaroon.
@returns {Object} - JSON compatible representation of this macaroon.
*/
_exportAsJSONObjectV1() {
const obj = {
identifier: bitsToString(this._identifierBits),
signature: sjcl.codec.hex.fromBits(this._signatureBits),
};
if (this._locationStr) {
obj.location = this._locationStr;
}
if (this._caveats.length > 0) {
obj.caveats = this._caveats.map(caveat => {
const caveatObj = {
cid: bitsToString(caveat._identifierBits),
};
if (caveat._vidBits) {
// Use URL encoding and do not append "=" characters.
caveatObj.vid = sjcl.codec.base64.fromBits(caveat._vidBits, true, true);
caveatObj.cl = caveat._locationStr;
}
return caveatObj;
});
}
return obj;
}
/**
Returns the V2 JSON serialization of this macaroon.
@returns {Object} - JSON compatible representation of this macaroon.
*/
_exportAsJSONObjectV2() {
const obj = {
v: 2, // version
};
setJSONFieldV2(obj, 's', bitsToBytes(this._signatureBits));
setJSONFieldV2(obj, 'i', bitsToBytes(this._identifierBits));
if (this._locationStr) {
obj.l = this._locationStr;
}
if (this._caveats && this._caveats.length > 0) {
obj.c = this._caveats.map(caveat => {
const caveatObj = {};
setJSONFieldV2(caveatObj, 'i', bitsToBytes(caveat._identifierBits));
if (caveat._vidBits) {
setJSONFieldV2(caveatObj, 'v', bitsToBytes(caveat._vidBits));
caveatObj.l = caveat._locationStr;
}
return caveatObj;
});
}
return obj;
}
/**
* Exports the macaroon using the v1 binary format.
* @returns {Uint8Array} - Serialized macaroon
*/
_exportBinaryV1() {
throw new Error('V1 binary export not supported');
};
/**
Exports the macaroon using the v2 binary format.
@returns {Uint8Array} - Serialized macaroon
*/
_exportBinaryV2() {
const buf = new ByteBuffer(200);
buf.appendByte(2);
if (this._locationStr) {
appendFieldV2(buf, FIELD_LOCATION, stringToBytes(this._locationStr));
}
appendFieldV2(buf, FIELD_IDENTIFIER, bitsToBytes(this._identifierBits));
appendFieldV2(buf, FIELD_EOS);
this._caveats.forEach(function(cav) {
if (cav._locationStr) {
appendFieldV2(buf, FIELD_LOCATION, stringToBytes(cav._locationStr));
}
appendFieldV2(buf, FIELD_IDENTIFIER, bitsToBytes(cav._identifierBits));
if (cav._vidBits) {
appendFieldV2(buf, FIELD_VID, bitsToBytes(cav._vidBits));
}
appendFieldV2(buf, FIELD_EOS);
});
appendFieldV2(buf, FIELD_EOS);
appendFieldV2(buf, FIELD_SIGNATURE, bitsToBytes(this._signatureBits));
return buf.bytes;
};
/**
Exports the macaroon using binary format.
The version will be the same as the version that the
macaroon was created with or imported from.
@returns {Uint8Array}
@alias module:macaroon
*/
exportBinary() {
switch (this._version) {
case 1:
return this._exportBinaryV1();
case 2:
return this._exportBinaryV2();
default:
throw new Error(`unexpected macaroon version ${this._version}`);
}
};
};
/**
Returns a macaroon instance based on the object passed in.
If obj is a string, it is assumed to be a base64-encoded
macaroon in binary or JSON format.
If obj is a Uint8Array, it is assumed to be a macaroon in
binary format, as produced by the exportBinary method.
Otherwise obj is assumed to be a object decoded from JSON,
and will be unmarshaled as such.
@param obj A deserialized JSON macaroon.
@returns {Macaroon | Macaroon[]}
@alias module:macaroon
*/
const importMacaroon = function(obj) {
if (typeof obj === 'string') {
obj = base64ToBytes(obj);
}
if (obj instanceof Uint8Array) {
const buf = new ByteReader(obj);
const m = importBinary(buf);
if (buf.length !== 0) {
throw new TypeError('extra data found at end of serialized macaroon');
}
return m;
}
if (Array.isArray(obj)) {
throw new TypeError('cannot import an array of macaroons as a single macaroon');
}
return importJSON(obj);
};
/**
Returns an array of macaroon instances based on the object passed in.
If obj is a string, it is assumed to be a set of base64-encoded
macaroons in binary or JSON format.
If obj is a Uint8Array, it is assumed to be set of macaroons in
binary format, as produced by the exportBinary method.
If obj is an array, it is assumed to be an array of macaroon
objects decoded from JSON.
Otherwise obj is assumed to be a macaroon object decoded from JSON.
This function accepts a strict superset of the formats accepted
by importMacaroons. When decoding a single macaroon,
it will return an array with one macaroon element.
@param obj A deserialized JSON macaroon or macaroons.
@returns {Macaroon[]}
@alias module:macaroon
*/
const importMacaroons = function(obj) {
if (typeof obj === 'string') {
obj = base64ToBytes(obj);
}
if (obj instanceof Uint8Array) {
if (obj.length === 0) {
throw new TypeError('empty macaroon data');
}
const buf = new ByteReader(obj);
const ms = [];
do {
ms.push(importBinary(buf));
} while (buf.length > 0);
return ms;
}
if (Array.isArray(obj)) {
return obj.map(val => importJSON(val));
}
return [importJSON(obj)];
};
/**
Returns a macaroon instance imported from a JSON-decoded object.
@param {object} obj The JSON to import from.
@returns {Macaroon}
*/
const importJSON = function(obj) {
if (isValue(obj.signature)) {
// Looks like a V1 macaroon.
return importJSONV1(obj);
}
return importJSONV2(obj);
};
const importJSONV1 = function(obj) {
const caveats = obj.caveats && obj.caveats.map(jsonCaveat => {
const caveat = {
identifierBytes: stringToBytes(requireString(jsonCaveat.cid, 'Caveat id')),
locationStr: maybeString(jsonCaveat.cl, 'Caveat location'),
};
if (jsonCaveat.vid) {
caveat.vidBytes = base64ToBytes(requireString(jsonCaveat.vid, 'Caveat verification id'));
}
return caveat;
});
return new Macaroon({
version: 1,
locationStr: maybeString(obj.location, 'Macaroon location'),
identifierBytes: stringToBytes(requireString(obj.identifier, 'Macaroon identifier')),
caveats: caveats,
signatureBytes: hexToBytes(obj.signature),
});
};
/**
* Imports V2 JSON macaroon encoding.
* @param {Object|Array} obj A serialized JSON macaroon
* @returns {Macaroon}
*/
const importJSONV2 = function(obj) {
// The Go macaroon library omits the version, so we'll assume that
// it is 2 in that case. See https://github.com/go-macaroon/macaroon/issues/35
if (obj.v !== 2 && obj.v !== undefined) {
throw new Error(`Unsupported macaroon version ${obj.v}`);