├── .gitignore
├── test
├── test.js
├── varint.js
├── base64.js
├── bytebuffer.js
├── import-export.js
├── bytereader.js
├── field.js
├── discharge.js
├── test-utils.js
├── serialize-binary.js
├── macaroon.js
├── serialize-json.js
└── verify.js
├── .github
└── PULL_REQUEST_TEMPLATE.md
├── .eslintrc
├── package.json
├── LICENSE
├── README.md
└── macaroon.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 |
--------------------------------------------------------------------------------
/test/test.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | require('./bytebuffer');
4 | require('./bytereader');
5 | require('./base64');
6 | require('./field');
7 | require('./macaroon');
8 | require('./import-export');
9 | require('./verify');
10 | require('./discharge');
11 | require('./serialize-binary');
12 | require('./serialize-json');
13 |
--------------------------------------------------------------------------------
/test/varint.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const tests = [
4 | [2147483648, [128, 128, 128, 128, 8]],
5 | [2147483649, [129, 128, 128, 128, 8]],
6 | [4294967295, [255, 255, 255, 255, 15]],
7 | [0, [0]],
8 | [1, [1]],
9 | [2, [2]],
10 | [10, [10]],
11 | [20, [20]],
12 | [63, [63]],
13 | [64, [64]],
14 | [65, [65]],
15 | [127, [127]],
16 | [128, [128, 1]],
17 | [129, [129, 1]],
18 | [255, [255, 1]],
19 | [256, [128, 2]],
20 | [257, [129, 2]],
21 | [2147483647, [255, 255, 255, 255, 7]],
22 | ];
23 |
24 | module.exports = tests;
25 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | Before submitting a pull request, please make sure the following has been completed:
2 |
3 | - You've rebased or merged in the latest version of master.
4 | - You've added or updated tests around this change.
5 | - You've run `npm run check` locally and it's passed.
6 | - You've run `npm run doc` to update the README.md.
7 | - Explain in detail what this pull request accomplishes and why it's necessary.
8 | - If you're fixing an issue, include "Fixes #1234" in this text box.
9 | - Add QA notes so that reviewers can test this change.
10 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | env:
2 | browser: true
3 | node: true
4 | es6: true
5 | parserOptions:
6 | ecmaVersion: 6
7 | rules:
8 | eqeqeq: ["error"]
9 | indent: [2, 2, {SwitchCase: 1, VariableDeclarator: 2}]
10 | no-const-assign: 2
11 | no-debugger: 2
12 | no-mixed-spaces-and-tabs: ["error"]
13 | no-multi-spaces: 2
14 | no-shadow: 0
15 | no-sparse-arrays: 2
16 | no-trailing-spaces: ["error"]
17 | no-undef: 2
18 | no-underscore-dangle: 0
19 | no-unused-vars: [2, {args: "none"}]
20 | quotes: [2, "single"]
21 | semi: [2, "always"]
22 | space-before-blocks: 2
23 | strict: [2, "global"]
24 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "author": "Roger Peppe (rogpeppe@gmail.com)",
3 | "bugs": {
4 | "url": "http://github.com/go-macaroon/js-macaroon/issues"
5 | },
6 | "contributors": [
7 | "Jeff Pihach (https://fromanegg.com)"
8 | ],
9 | "dependencies": {
10 | "sjcl": "^1.0.6",
11 | "tweetnacl": "^1.0.0",
12 | "tweetnacl-util": "^0.15.0"
13 | },
14 | "description": "Macaroons: cookies with contextual caveats for decentralized authorization in the cloud.",
15 | "devDependencies": {
16 | "eslint": "^5.7.0",
17 | "jsdoc-to-markdown": "^4.0.1",
18 | "rewire": "^4.0.1",
19 | "tape": "^4.6.3"
20 | },
21 | "homepage": "http://github.com/go-macaroon/js-macaroon",
22 | "keywords": [
23 | "authorization",
24 | "cookie",
25 | "macaroon"
26 | ],
27 | "license": "BSD-3-Clause",
28 | "main": "macaroon.js",
29 | "name": "macaroon",
30 | "repository": {
31 | "type": "git",
32 | "url": "http://github.com/go-macaroon/js-macaroon.git"
33 | },
34 | "scripts": {
35 | "lint": "eslint .",
36 | "test": "node test/test.js",
37 | "check": "npm run lint && npm t",
38 | "clean": "rm -rf node_modules",
39 | "doc": "jsdoc2md macaroon.js > README.md"
40 | },
41 | "version": "3.1.0"
42 | }
43 |
--------------------------------------------------------------------------------
/test/base64.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const test = require('tape');
4 | const testUtils = require('./test-utils.js');
5 | const m = require('../macaroon');
6 |
7 | const bytesToBase64 = m.bytesToBase64;
8 | const base64ToBytes = m.base64ToBytes;
9 |
10 | const base64ToBytesTests = [{
11 | about: 'empty string',
12 | input: '',
13 | expect: '',
14 | }, {
15 | about: 'standard encoding, padded',
16 | input: 'Z29+IQ==',
17 | expect: 'go~!',
18 | }, {
19 | about: 'URL encoding, padded',
20 | input: 'Z29-IQ==',
21 | expect: 'go~!',
22 | }, {
23 | about: 'standard encoding, not padded',
24 | input: 'Z29+IQ',
25 | expect: 'go~!',
26 | }, {
27 | about: 'URL encoding, not padded',
28 | input: 'Z29-IQ',
29 | expect: 'go~!',
30 | }, {
31 | about: 'standard encoding, too much padding',
32 | input: 'Z29+IQ===',
33 | expectError: /TypeError: invalid encoding/,
34 | }];
35 |
36 | test('base64ToBytes', t => {
37 | base64ToBytesTests.forEach(test => {
38 | t.test('base64ToBytes: ' + test.about, t => {
39 | if (test.expectError) {
40 | t.throws(() => base64ToBytes(test.input), test.expectError);
41 | } else {
42 | t.deepEqual(base64ToBytes(test.input), testUtils.stringToBytes(test.expect));
43 | }
44 | t.end();
45 | });
46 | });
47 | t.end();
48 | });
49 |
50 |
51 | test('bytesToBase64', t => {
52 | t.equal(bytesToBase64(testUtils.stringToBytes('go~!')), 'Z29-IQ');
53 | t.end();
54 | });
55 |
56 |
--------------------------------------------------------------------------------
/test/bytebuffer.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const rewire = require('rewire');
4 | const test = require('tape');
5 |
6 | const m = rewire('../macaroon');
7 | const testUtils = require('./test-utils');
8 | const bytes = testUtils.bytes;
9 | const varintTests = require('./varint');
10 |
11 | const ByteBuffer = m.__get__('ByteBuffer');
12 |
13 | test('ByteBuffer append single byte', t => {
14 | const buf = new ByteBuffer(0);
15 | buf.appendByte(123);
16 | t.equal(buf.bytes.toString(), '123');
17 | t.end();
18 | });
19 |
20 | test('ByteBuffer append 10 bytes byte', t => {
21 | const buf = new ByteBuffer(0);
22 | for(var i = 0; i < 10; i++) {
23 | buf.appendByte(i);
24 | }
25 | t.equal(buf.bytes.toString(), '0,1,2,3,4,5,6,7,8,9');
26 | t.end();
27 | });
28 |
29 | test('ByteBuffer append bytes', t => {
30 | const buf = new ByteBuffer(0);
31 | buf.appendBytes(bytes([3,1,4,1,5,9,3]));
32 | t.equal(buf.bytes.toString(), '3,1,4,1,5,9,3');
33 | t.end();
34 | });
35 |
36 | test('ByteBuffer append 256 bytes, verify growth not exponential', t => {
37 | const buf = new ByteBuffer(0);
38 | for(var i = 0; i < 256; i++) {
39 | buf.appendByte(i);
40 | }
41 | t.equal(buf._buf.length, 256);
42 | t.end();
43 | });
44 |
45 | test('ByteBuffer appendUvarint', t => {
46 | varintTests.forEach(test => {
47 | const buf = new ByteBuffer(0);
48 | buf.appendUvarint(test[0]);
49 | t.deepEqual(buf.bytes, test[1], `test ${test[0]}`);
50 | });
51 | t.end();
52 | });
53 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright © 2015, Roger Peppe
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without modification,
5 | are permitted provided that the following conditions are met:
6 |
7 | * Redistributions of source code must retain the above copyright notice,
8 | this list of conditions and the following disclaimer.
9 | * Redistributions in binary form must reproduce the above copyright notice,
10 | this list of conditions and the following disclaimer in the documentation
11 | and/or other materials provided with the distribution.
12 | * Neither the name of this project nor the names of its contributors
13 | may be used to endorse or promote products derived from this software
14 | without specific prior written permission.
15 |
16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
22 | TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
23 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
24 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
25 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
26 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27 |
--------------------------------------------------------------------------------
/test/import-export.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const test = require('tape');
4 |
5 | const m = require('../macaroon');
6 | const testUtils = require('./test-utils');
7 |
8 | test('importMacaroon should import from a single object', t => {
9 | const obj = {
10 | location: 'a location',
11 | identifier: 'id 1',
12 | signature: 'e0831c334c600631bf7b860ca20c9930f584b077b8eac1f1e99c6a45d11a3d20',
13 | caveats: [
14 | {
15 | 'cid': 'a caveat'
16 | }, {
17 | 'cid': '3rd question',
18 | 'vid': 'MMVAwhLcKvsgJS-SCTuhi9fMNYT9SjSePUX2q4z8y4_TpYfB82UCirA0ZICOdUb7ND_2',
19 | 'cl': '3rd loc'
20 | },
21 | ],
22 | };
23 |
24 | const macaroon = m.importMacaroon(obj);
25 | t.equal(macaroon.location, 'a location');
26 | t.equal(testUtils.bytesToString(macaroon.identifier), 'id 1');
27 | t.equal(
28 | testUtils.bytesToHex(macaroon.signature),
29 | 'e0831c334c600631bf7b860ca20c9930f584b077b8eac1f1e99c6a45d11a3d20');
30 | // Test that it round trips.
31 | const obj1 = macaroon.exportJSON();
32 | t.deepEqual(obj1, obj);
33 | t.end();
34 | });
35 |
36 | test('importMacaroons should import from an array', t => {
37 | const objs = [{
38 | location: 'a location',
39 | identifier: 'id 0',
40 | signature: '4579ad730bf3f819a299aaf63f04f5e897d80690c4c5814a1ae026a45989de7d',
41 | }, {
42 | location: 'a location',
43 | identifier: 'id 1',
44 | signature: '99b1c2dede0ce1cba0b632e3996e9924bdaee6287151600468644b92caf3761b',
45 | }];
46 | const macaroon = m.importMacaroons(objs);
47 |
48 | t.equal(macaroon.length, 2);
49 | t.equal(testUtils.bytesToString(macaroon[0].identifier), 'id 0');
50 | t.equal(testUtils.bytesToString(macaroon[1].identifier), 'id 1');
51 |
52 | t.deepEqual([
53 | macaroon[0].exportJSON(),
54 | macaroon[1].exportJSON()], objs);
55 | t.end();
56 | });
57 |
--------------------------------------------------------------------------------
/test/bytereader.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const rewire = require('rewire');
4 | const test = require('tape');
5 |
6 | const m = rewire('../macaroon');
7 | const testUtils = require('./test-utils');
8 | const bytes = testUtils.bytes;
9 |
10 | const ByteReader = m.__get__('ByteReader');
11 |
12 | const varintTests = require('./varint');
13 |
14 | test('ByteReader read byte', t => {
15 | const r = new ByteReader(bytes([0, 1, 2, 3]));
16 | t.equal(r.length, 4);
17 | for(var i = 0; i < 4; i++) {
18 | t.equal(r.readByte(), i, `byte ${i}`);
19 | }
20 | t.throws(function() {
21 | r.readByte();
22 | }, RangeError);
23 | t.end();
24 | });
25 |
26 | test('ByteReader read bytes', t => {
27 | const r = new ByteReader(bytes([0, 1, 2, 3, 4, 5]));
28 | t.equal(r.readByte(), 0);
29 | t.deepEqual(r.readN(3), bytes([1,2,3]));
30 | t.throws(function() {
31 | r.readN(3);
32 | }, RangeError);
33 | t.end();
34 | });
35 |
36 | test('ByteReader readUvarint', t => {
37 | varintTests.forEach(test => {
38 | const r = new ByteReader(bytes([99].concat(test[1])));
39 | // Read one byte at the start so we are dealing with a non-zero
40 | // index.
41 | r.readByte();
42 | const len0 = r.length;
43 | const x = r.readUvarint();
44 | t.equal(x, test[0], `test ${test[0]}`);
45 | // Check that we've read the expected number of bytes.
46 | t.equal(len0 - r.length, test[1].length);
47 | });
48 | t.end();
49 | });
50 |
51 | test('ByteReader readUvarint out of bounds', t => {
52 | const r = new ByteReader(bytes([]));
53 | t.throws(function() {
54 | r.readUvarint();
55 | }, RangeError);
56 | // Try all the tests with one less byte than there should be.
57 | varintTests.forEach(test => {
58 | const r = new ByteReader(test[1].slice(0, test[1].length-1));
59 | t.throws(function() {
60 | r.readUvarint();
61 | }, RangeError);
62 | });
63 | t.end();
64 | });
65 |
--------------------------------------------------------------------------------
/test/field.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const rewire = require('rewire');
4 | const test = require('tape');
5 |
6 | const m = rewire('../macaroon');
7 |
8 | const ByteReader = m.__get__('ByteReader');
9 | const readFieldV2 = m.__get__('readFieldV2');
10 |
11 | test('parse v2 field', t => {
12 | const tests = [{
13 | about: 'EOS packet',
14 | data: [0x00],
15 | field: 0,
16 | expectPacket: [],
17 | }, {
18 | about: 'simple field',
19 | data: [0x02, 0x03, 0x78, 0x79, 0x80],
20 | field: 2,
21 | expectPacket: [0x78, 0x79, 0x80],
22 | }, {
23 | about: 'unexpected field type',
24 | data: [0x02, 0x03, 0x78, 0x79, 0x80],
25 | field: 1,
26 | expectError: /Unexpected field type, got 2 want 1/,
27 | }, {
28 | about: 'empty buffer',
29 | data: [],
30 | field: 2,
31 | expectError: /Read past end of buffer/,
32 | }, {
33 | about: 'varint out of range',
34 | data: [0x02, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f],
35 | field: 2,
36 | expectError: /RangeError: Overflow error decoding varint/,
37 | }, {
38 | about: 'varint way out of range',
39 | data: [0x02, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f],
40 | field: 2,
41 | expectError: /RangeError: Overflow error decoding varint/,
42 | }, {
43 | about: 'unterminated varint',
44 | data: [0x02, 0x80],
45 | field: 2,
46 | expectError: /RangeError: Buffer too small decoding varint/,
47 | }, {
48 | about: 'field data too long',
49 | data: [0x02, 0x02, 0x48],
50 | field: 2,
51 | expectError: /RangeError: Read past end of buffer/,
52 | }];
53 | tests.forEach(test => {
54 | t.test('parse v2 field: ' + test.about, t => {
55 | const r = new ByteReader(new Uint8Array(test.data));
56 | if (test.expectError) {
57 | t.throws(() => readFieldV2(r, test.field), test.expectError);
58 | } else {
59 | t.deepEqual(readFieldV2(r, test.field), test.expectPacket, test.about);
60 | }
61 | t.end();
62 | });
63 | });
64 | t.end();
65 | });
66 |
--------------------------------------------------------------------------------
/test/discharge.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const test = require('tape');
4 |
5 | const m = require('../macaroon');
6 | const testUtils = require('./test-utils');
7 |
8 | test('should discharge a macaroon with no caveats without calling getDischarge', t => {
9 | const macaroon = m.newMacaroon({
10 | rootKey: 'key',
11 | identifier: 'some id',
12 | location: 'a location'
13 | });
14 | macaroon.addFirstPartyCaveat('a caveat');
15 | const getDischarge = () => {
16 | throw 'getDischarge called unexpectedly';
17 | };
18 | let result;
19 | const onOk = ms => {
20 | result = ms;
21 | };
22 | const onErr = err => {
23 | throw 'onErr called unexpectedly';
24 | };
25 | m.dischargeMacaroon(macaroon, getDischarge, onOk, onErr);
26 | t.deepEqual(result, [macaroon]);
27 | t.end();
28 | });
29 |
30 | test('should discharge many discharges correctly', t => {
31 | const rootKey = 'secret';
32 | const queued = [];
33 | const m0 = m.newMacaroon({
34 | rootKey,
35 | identifier: 'id0',
36 | location: 'location0'
37 | });
38 | let totalRequired = 40;
39 | let id = 1;
40 | const addCaveats = m => {
41 | let i;
42 | for (i = 0; i < 2; i++) {
43 | if (totalRequired === 0) {
44 | break;
45 | }
46 | const cid = 'id' + id;
47 | m.addThirdPartyCaveat(
48 | 'root key ' + cid, cid, 'somewhere');
49 | id++;
50 | totalRequired--;
51 | }
52 | };
53 | addCaveats(m0);
54 | const getDischarge = function (loc, thirdPartyLoc, cond, onOK, onErr) {
55 | t.equal(loc, 'location0');
56 | const macaroon = m.newMacaroon({
57 | rootKey: 'root key ' + testUtils.bytesToString(cond),
58 | identifier: cond});
59 | addCaveats(macaroon);
60 | queued.push(() => {
61 | onOK(macaroon);
62 | });
63 | };
64 | let discharges;
65 | m.dischargeMacaroon(m0, getDischarge, ms => {
66 | discharges = ms;
67 | }, err => {
68 | throw new Error('error callback called unexpectedly: ' + err);
69 | });
70 | while (queued.length > 0) {
71 | const f = queued.shift();
72 | f();
73 | }
74 | t.notEqual(discharges, null);
75 | t.equal(discharges.length, 41);
76 | discharges[0].verify(rootKey, ()=>{}, discharges.slice(1));
77 | t.end();
78 | });
79 |
--------------------------------------------------------------------------------
/test/test-utils.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const m = require('../macaroon');
4 |
5 | const util = require('util');
6 |
7 | const utf8Encoder = new util.TextEncoder();
8 | const utf8Decoder = new util.TextDecoder('utf-8', {fatal : true});
9 |
10 | const bytesToString = b => utf8Decoder.decode(b);
11 | const stringToBytes = s => utf8Encoder.encode(s);
12 |
13 | const bytesToHex = ua => {
14 | if (!(ua instanceof Uint8Array)) {
15 | throw new Error('invalid Uint8Array:' + ua);
16 | }
17 | let hex = '';
18 | for (var i = 0; i < ua.length; i++) {
19 | hex += (ua[i] < 16 ? '0' : '') + ua[i].toString(16);
20 | }
21 | return hex;
22 | };
23 |
24 | const never = () => 'condition is never true';
25 |
26 | const base64ToBytes = m.base64ToBytes;
27 |
28 | const bytesToBase64 = m.bytesToBase64;
29 |
30 | const bytes = a => {
31 | return new Uint8Array(a);
32 | };
33 |
34 | /**
35 | Make a set of macaroons from the given macaroon spec.
36 | Each macaroon specification is an object holding:
37 | - rootKey: the root key (string)
38 | - id: the macaroon id (string)
39 | - caveats: an array of caveats to add to the macaroon, (see below)
40 | - location: the location of the macaroon (string)
41 | Each caveat is specified with an object holding:
42 | - rootKey: the caveat root key (string, optional)
43 | - location: the caveat location (string, optional)
44 | - condition: the caveat condition (string)
45 | */
46 | const makeMacaroons = mspecs => {
47 | const macaroons = [];
48 | let i;
49 | for (i in mspecs) {
50 | let j;
51 | const mspec = mspecs[i];
52 | if (mspec.location === undefined) {
53 | mspec.location = '';
54 | }
55 | const macaroon = m.newMacaroon({
56 | rootKey: mspec.rootKey,
57 | identifier: mspec.id,
58 | location: mspec.location
59 | });
60 | for (j in mspec.caveats) {
61 | const caveat = mspec.caveats[j];
62 | if (caveat.location !== undefined) {
63 | macaroon.addThirdPartyCaveat(
64 | caveat.rootKey, caveat.condition, caveat.location);
65 | } else {
66 | macaroon.addFirstPartyCaveat(caveat.condition);
67 | }
68 | }
69 | macaroons.push(macaroon);
70 | }
71 | const primary = macaroons[0];
72 | const discharges = macaroons.slice(1);
73 | for (i in discharges) {
74 | discharges[i].bindToRoot(primary.signature);
75 | }
76 | return [mspecs[0].rootKey, primary, discharges];
77 | };
78 |
79 | module.exports = {
80 | bytesToString,
81 | stringToBytes,
82 | bytesToHex,
83 | base64ToBytes,
84 | bytesToBase64,
85 | never,
86 | bytes,
87 | makeMacaroons,
88 | };
89 |
--------------------------------------------------------------------------------
/test/serialize-binary.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const test = require('tape');
4 | const testUtils = require('./test-utils');
5 |
6 | const m = require('../macaroon');
7 |
8 | const base64ToBytes = testUtils.base64ToBytes;
9 | const bytesToBase64 = testUtils.bytesToBase64;
10 |
11 | test('should serialize binary format without caveats', t => {
12 | const macaroon = m.newMacaroon({
13 | rootKey: 'this is the key',
14 | identifier: 'keyid',
15 | location: 'http://example.org/'
16 | });
17 |
18 | t.equal(bytesToBase64(macaroon.exportBinary()), 'AgETaHR0cDovL2V4YW1wbGUub3JnLwIFa2V5aWQAAAYgfN7nklEcW8b1KEhYBd_psk54XijiqZMB-dcRxgnjjvc');
19 | t.end();
20 | });
21 |
22 | test('should serialize binary format with one caveat', t => {
23 | const macaroon = m.newMacaroon({
24 | rootKey: 'this is the key',
25 | identifier: 'keyid',
26 | location: 'http://example.org/'
27 | });
28 | macaroon.addFirstPartyCaveat('account = 3735928559');
29 | t.equal(bytesToBase64(macaroon.exportBinary()), 'AgETaHR0cDovL2V4YW1wbGUub3JnLwIFa2V5aWQAAhRhY2NvdW50ID0gMzczNTkyODU1OQAABiD1SAf23G7fiL8PcwazgiVio2JTPb9zObphdl2kvSWdhw');
30 | t.end();
31 | });
32 |
33 | test('should serialize binary format with two caveats', t => {
34 | const macaroon = m.newMacaroon({
35 | rootKey: 'this is the key',
36 | identifier: 'keyid',
37 | location: 'http://example.org/'
38 | });
39 | macaroon.addFirstPartyCaveat('account = 3735928559');
40 | macaroon.addFirstPartyCaveat('user = alice');
41 |
42 | t.equal(bytesToBase64(macaroon.exportBinary()), 'AgETaHR0cDovL2V4YW1wbGUub3JnLwIFa2V5aWQAAhRhY2NvdW50ID0gMzczNTkyODU1OQACDHVzZXIgPSBhbGljZQAABiBL6WfNHqDGsmuvakqU7psFsViG2guoXoxCqTyNDhJe_A');
43 | t.end();
44 | });
45 |
46 | test('should deserialize binary format without caveats', t => {
47 | const macaroon = m.importMacaroon(base64ToBytes('AgETaHR0cDovL2V4YW1wbGUub3JnLwIFa2V5aWQAAAYgfN7nklEcW8b1KEhYBd/psk54XijiqZMB+dcRxgnjjvc='));
48 | t.equal(macaroon.location, 'http://example.org/');
49 | t.equal(testUtils.bytesToString(macaroon.identifier), 'keyid');
50 | t.equal(macaroon.caveats.length, 0);
51 | t.equals(bytesToBase64(macaroon.signature), 'fN7nklEcW8b1KEhYBd_psk54XijiqZMB-dcRxgnjjvc');
52 | t.end();
53 | });
54 |
55 | test('should deserialize binary format with one caveat', t => {
56 | const macaroon = m.importMacaroon(base64ToBytes('AgETaHR0cDovL2V4YW1wbGUub3JnLwIFa2V5aWQAAhRhY2NvdW50ID0gMzczNTkyODU1OQAABiD1SAf23G7fiL8PcwazgiVio2JTPb9zObphdl2kvSWdhw=='));
57 | t.equal(macaroon.location, 'http://example.org/');
58 | t.equal(testUtils.bytesToString(macaroon.identifier), 'keyid');
59 | t.equal(macaroon.caveats.length, 1);
60 | t.equal(testUtils.bytesToString(macaroon.caveats[0].identifier), 'account = 3735928559');
61 | t.equal(bytesToBase64(macaroon.signature), '9UgH9txu34i_D3MGs4IlYqNiUz2_czm6YXZdpL0lnYc');
62 | t.end();
63 | });
64 |
65 | test('should import from base64 string', t => {
66 | const macaroon = m.importMacaroon('AgETaHR0cDovL2V4YW1wbGUub3JnLwIFa2V5aWQAAhRhY2NvdW50ID0gMzczNTkyODU1OQAABiD1SAf23G7fiL8PcwazgiVio2JTPb9zObphdl2kvSWdhw==');
67 | t.equal(macaroon.location, 'http://example.org/');
68 | t.equal(testUtils.bytesToString(macaroon.identifier), 'keyid');
69 | t.equal(macaroon.caveats.length, 1);
70 | t.equal(testUtils.bytesToString(macaroon.caveats[0].identifier), 'account = 3735928559');
71 | t.equal(bytesToBase64(macaroon.signature), '9UgH9txu34i_D3MGs4IlYqNiUz2_czm6YXZdpL0lnYc');
72 | t.end();
73 | });
74 |
75 | test('should deserialize binary format with two caveats', t => {
76 | const macaroon = m.importMacaroon(base64ToBytes('AgETaHR0cDovL2V4YW1wbGUub3JnLwIFa2V5aWQAAhRhY2NvdW50ID0gMzczNTkyODU1OQACDHVzZXIgPSBhbGljZQAABiBL6WfNHqDGsmuvakqU7psFsViG2guoXoxCqTyNDhJe/A=='));
77 | t.equal(macaroon.location, 'http://example.org/');
78 | t.equal(testUtils.bytesToString(macaroon.identifier), 'keyid');
79 | const caveats = macaroon.caveats;
80 | t.equal(caveats.length, 2);
81 | t.equal(testUtils.bytesToString(caveats[0].identifier), 'account = 3735928559');
82 | t.equal(testUtils.bytesToString(caveats[1].identifier), 'user = alice');
83 | t.equal(testUtils.bytesToBase64(macaroon.signature), 'S-lnzR6gxrJrr2pKlO6bBbFYhtoLqF6MQqk8jQ4SXvw');
84 | t.end();
85 | });
86 |
--------------------------------------------------------------------------------
/test/macaroon.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const test = require('tape');
4 |
5 | const m = require('../macaroon');
6 | const testUtils = require('./test-utils');
7 |
8 | test('should be created with the expected signature', t => {
9 | const macaroon = m.newMacaroon({
10 | version: 1,
11 | rootKey: 'secret',
12 | identifier: 'some id',
13 | location: 'a location'
14 | });
15 | t.equal(macaroon.location, 'a location');
16 | t.equal(testUtils.bytesToString(macaroon.identifier), 'some id');
17 | t.equal(
18 | testUtils.bytesToHex(macaroon.signature),
19 | 'd916ce6f9b62dc4a080ce5d4a660956471f19b860da4242b0852727331c1033d');
20 |
21 | const obj = macaroon.exportJSON();
22 | t.deepEqual(obj, {
23 | location: 'a location',
24 | identifier: 'some id',
25 | signature: 'd916ce6f9b62dc4a080ce5d4a660956471f19b860da4242b0852727331c1033d',
26 | });
27 |
28 | macaroon.verify('secret', testUtils.never);
29 | t.end();
30 | });
31 |
32 | test('should fail when newMacaroon called with bad args', t => {
33 | t.throws(() => {
34 | m.newMacaroon({
35 | rootKey: null,
36 | identifier: 'some id',
37 | location: 'a location',
38 | });
39 | }, /TypeError: Macaroon root key has the wrong type; want string or Uint8Array, got object./);
40 | t.throws(() => {
41 | m.newMacaroon({
42 | rootKey: 5,
43 | identifier: 'some id',
44 | location: 'a location',
45 | });
46 | }, /TypeError: Macaroon root key has the wrong type; want string or Uint8Array, got number./);
47 |
48 | var key = testUtils.stringToBytes('key');
49 | t.throws(() => {
50 | m.newMacaroon({
51 | rootKey: key,
52 | identifier: null,
53 | location: 'a location',
54 | });
55 | }, /TypeError: Macaroon identifier has the wrong type; want string or Uint8Array, got object./);
56 | t.throws(() => {
57 | m.newMacaroon({
58 | rootKey: key,
59 | identifier: 5,
60 | location: 'a location',
61 | });
62 | }, /TypeError: Macaroon identifier has the wrong type; want string or Uint8Array, got number./);
63 | t.throws(() => {
64 | m.newMacaroon({
65 | rootKey: key,
66 | identifier: 'id',
67 | location: 5,
68 | });
69 | }, /TypeError: Macaroon location has the wrong type; want string, got number./);
70 | t.throws(() => {
71 | m.newMacaroon({
72 | rootKey: key,
73 | identifier: 'id',
74 | location: key,
75 | });
76 | }, /TypeError: Macaroon location has the wrong type; want string, got object./);
77 | t.end();
78 | });
79 |
80 | test('should allow adding first party caveats', t => {
81 | const rootKey = 'secret';
82 | const macaroon = m.newMacaroon({
83 | version: 1,
84 | rootKey,
85 | identifier: 'some id',
86 | location: 'a location'
87 | });
88 | const caveats = ['a caveat', 'another caveat'];
89 | const trueCaveats = {};
90 | const tested = {};
91 | for (let i = 0; i < caveats.length; i++) {
92 | macaroon.addFirstPartyCaveat(caveats[i]);
93 | trueCaveats[caveats[i]] = true;
94 | }
95 | t.equal(
96 | testUtils.bytesToHex(macaroon.signature),
97 | 'c934e6af642ee55a4e4cfc56e07706cf1c6c94dc2192e5582943cddd88dc99d8');
98 | const obj = macaroon.exportJSON();
99 | t.deepEqual(obj, {
100 | location: 'a location',
101 | identifier: 'some id',
102 | signature: 'c934e6af642ee55a4e4cfc56e07706cf1c6c94dc2192e5582943cddd88dc99d8',
103 | caveats: [{
104 | cid: 'a caveat',
105 | }, {
106 | cid: 'another caveat',
107 | }],
108 | });
109 | const check = caveat => {
110 | tested[caveat] = true;
111 | if (!trueCaveats[caveat]) {
112 | return 'condition not met';
113 | }
114 | };
115 | macaroon.verify(rootKey, check);
116 | t.deepEqual(tested, trueCaveats);
117 |
118 | macaroon.addFirstPartyCaveat('not met');
119 | t.throws(() => {
120 | macaroon.verify(rootKey, check);
121 | }, /caveat check failed \(not met\): condition not met/);
122 |
123 | t.equal(tested['not met'], true);
124 | t.end();
125 | });
126 |
127 | test('should allow adding a third party caveat', t => {
128 | const rootKey = 'secret';
129 | const macaroon = m.newMacaroon({
130 | rootKey,
131 | identifier: 'some id',
132 | location: 'a location',
133 | });
134 | const dischargeRootKey = 'shared root key';
135 | const thirdPartyCaveatId = '3rd party caveat';
136 | macaroon.addThirdPartyCaveat(
137 | dischargeRootKey, thirdPartyCaveatId, 'remote.com');
138 |
139 | const dm = m.newMacaroon({
140 | rootKey: dischargeRootKey,
141 | identifier: thirdPartyCaveatId,
142 | location: 'remote location',
143 | });
144 |
145 | dm.bindToRoot(macaroon.signature);
146 | macaroon.verify(rootKey, testUtils.never, [dm]);
147 | t.end();
148 | });
149 |
150 | test('should allow binding to another macaroon', t => {
151 | const macaroon = m.newMacaroon({
152 | rootKey: 'secret',
153 | identifier: 'some id',
154 | });
155 | macaroon.bindToRoot(testUtils.stringToBytes('another sig'));
156 | t.equal(
157 | testUtils.bytesToHex(macaroon.signature),
158 | 'bba29be9ed9485a594f678adad69b7071c2f353308933355fc81cfad601b8277');
159 | t.end();
160 | });
161 |
--------------------------------------------------------------------------------
/test/serialize-json.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const test = require('tape');
4 | const testUtils = require('./test-utils');
5 |
6 | const m = require('../macaroon');
7 |
8 | test('should serialize json format without caveats', t => {
9 | const macaroon = m.newMacaroon({
10 | rootKey: 'this is the key',
11 | identifier: 'keyid',
12 | location: 'http://example.org/'
13 | });
14 |
15 | t.deepEqual(macaroon.exportJSON(), {'v':2,'l':'http://example.org/','i':'keyid','s64':'fN7nklEcW8b1KEhYBd_psk54XijiqZMB-dcRxgnjjvc'});
16 | t.end();
17 | });
18 |
19 | test('should serialize json format with one caveat', t => {
20 | const macaroon = m.newMacaroon({
21 | rootKey: 'this is the key',
22 | identifier: 'keyid',
23 | location: 'http://example.org/'
24 | });
25 | macaroon.addFirstPartyCaveat('account = 3735928559');
26 |
27 | t.deepEqual(macaroon.exportJSON(), {'v':2,'l':'http://example.org/','i':'keyid','c':[{'i':'account = 3735928559'}],'s64':'9UgH9txu34i_D3MGs4IlYqNiUz2_czm6YXZdpL0lnYc'});
28 | t.end();
29 | });
30 |
31 | test('should serialize json format with two caveats', t => {
32 | const macaroon = m.newMacaroon({
33 | rootKey: 'this is the key',
34 | identifier: 'keyid',
35 | location: 'http://example.org/'
36 | });
37 | macaroon.addFirstPartyCaveat('account = 3735928559');
38 | macaroon.addFirstPartyCaveat('user = alice');
39 |
40 | t.deepEqual(macaroon.exportJSON(), {'v':2,'l':'http://example.org/','i':'keyid','c':[{'i':'account = 3735928559'},{'i':'user = alice'}],'s64':'S-lnzR6gxrJrr2pKlO6bBbFYhtoLqF6MQqk8jQ4SXvw'});
41 | t.end();
42 | });
43 |
44 | test('should deserialize json format without caveats', t => {
45 | const macaroon = m.importMacaroon({'v':2,'l':'http://example.org/','i':'keyid','c':[],'s64':'fN7nklEcW8b1KEhYBd_psk54XijiqZMB-dcRxgnjjvc'});
46 | t.equal(macaroon.location, 'http://example.org/');
47 | t.equal(testUtils.bytesToString(macaroon.identifier), 'keyid');
48 | t.equal(macaroon.caveats.length, 0);
49 | t.equal(testUtils.bytesToBase64(macaroon.signature), 'fN7nklEcW8b1KEhYBd_psk54XijiqZMB-dcRxgnjjvc');
50 | t.end();
51 | });
52 |
53 | test('should deserialize json format with one caveat', t => {
54 | const macaroon = m.importMacaroon({'v':2,'l':'http://example.org/','i':'keyid','c':[{'i':'account = 3735928559'}],'s64':'9UgH9txu34i_D3MGs4IlYqNiUz2_czm6YXZdpL0lnYc'});
55 | t.equal(macaroon.location, 'http://example.org/');
56 | t.equal(testUtils.bytesToString(macaroon.identifier), 'keyid');
57 | const caveats = macaroon.caveats;
58 | t.equal(caveats.length, 1);
59 | t.equal(testUtils.bytesToString(caveats[0].identifier), 'account = 3735928559');
60 | t.equal(testUtils.bytesToBase64(macaroon.signature), '9UgH9txu34i_D3MGs4IlYqNiUz2_czm6YXZdpL0lnYc');
61 | t.end();
62 | });
63 |
64 | const macaroonVersionTests = [
65 | {
66 | about: 'Version 0',
67 | macaroon: {
68 | 'v': 0,
69 | 'l':'http://example.org/',
70 | 'i':'keyid',
71 | 'c':[{'i':'account = 3735928559'}],
72 | 's64':'9UgH9txu34i_D3MGs4IlYqNiUz2_czm6YXZdpL0lnYc'
73 | },
74 | conditions: {
75 | expectErr: /Unsupported macaroon version 0/
76 | }
77 | },
78 | {
79 | about: 'Version 1 Test',
80 | macaroon: {
81 | 'v': 1,
82 | 'l':'http://example.org/',
83 | 'i':'keyid',
84 | 'c':[{'i':'account = 3735928559'}],
85 | 's64':'9UgH9txu34i_D3MGs4IlYqNiUz2_czm6YXZdpL0lnYc'
86 | },
87 | conditions: {
88 | expectErr: /Unsupported macaroon version 1/
89 | }
90 | },
91 | {
92 | about: 'Version 2 Test',
93 | macaroon: {
94 | 'v': 2,
95 | 'l':'http://example.org/',
96 | 'i':'keyid',
97 | 'c':[{'i':'account = 3735928559'}],
98 | 's64':'9UgH9txu34i_D3MGs4IlYqNiUz2_czm6YXZdpL0lnYc'
99 | },
100 | conditions: {}
101 | },
102 | {
103 | about: 'Version 3 Test',
104 | macaroon: {
105 | 'v': 3,
106 | 'l':'http://example.org/',
107 | 'i':'keyid',
108 | 'c':[{'i':'account = 3735928559'}],
109 | 's64':'9UgH9txu34i_D3MGs4IlYqNiUz2_czm6YXZdpL0lnYc'
110 | },
111 | conditions: {
112 | expectErr: /Unsupported macaroon version 3/
113 | }
114 | },
115 | {
116 | about: 'Empty Test',
117 | macaroon: {
118 | 'v': '',
119 | 'l':'http://example.org/',
120 | 'i':'keyid',
121 | 'c':[{'i':'account = 3735928559'}],
122 | 's64':'9UgH9txu34i_D3MGs4IlYqNiUz2_czm6YXZdpL0lnYc'
123 | },
124 | conditions: {
125 | expectErr: /Unsupported macaroon version /
126 | }
127 | },
128 | {
129 | about: 'Null Version Test',
130 | macaroon: {
131 | 'v': null,
132 | 'l':'http://example.org/',
133 | 'i':'keyid',
134 | 'c':[{'i':'account = 3735928559'}],
135 | 's64':'9UgH9txu34i_D3MGs4IlYqNiUz2_czm6YXZdpL0lnYc'
136 | },
137 | conditions: {
138 | expectErr: /Unsupported macaroon version /
139 | }
140 | },
141 | {
142 | about: 'String Version Test',
143 | macaroon: {
144 | 'v': 'V2',
145 | 'l':'http://example.org/',
146 | 'i':'keyid',
147 | 'c':[{'i':'account = 3735928559'}],
148 | 's64':'9UgH9txu34i_D3MGs4IlYqNiUz2_czm6YXZdpL0lnYc'
149 | },
150 | conditions: {
151 | expectErr: /Unsupported macaroon version V2/
152 | }
153 | }
154 | ];
155 |
156 | test('should not deserialize json format with invalid version', t => {
157 | for (const i in macaroonVersionTests) {
158 | const testData = macaroonVersionTests[i];
159 |
160 | t.test(testData.about, t => {
161 | const mac = testData.macaroon;
162 | if (testData.conditions.expectErr) {
163 | t.throws(() => {
164 | m.importMacaroon(mac);
165 | }, testData.conditions.expectErr, 'Expected error: ' + testData.conditions.expectErr);
166 | } else {
167 | m.importMacaroon(mac);
168 | }
169 | t.end();
170 | });
171 | }
172 | });
173 |
174 |
175 | test('should deserialize json format with two caveats', t => {
176 | const macaroon = m.importMacaroon({'v': 2, 'l':'http://example.org/','i':'keyid','c':[{'i':'account = 3735928559'},{'i':'user = alice'}],'s64':'S-lnzR6gxrJrr2pKlO6bBbFYhtoLqF6MQqk8jQ4SXvw'});
177 | t.equal(macaroon.location, 'http://example.org/');
178 | t.equal(testUtils.bytesToString(macaroon.identifier), 'keyid');
179 | const caveats = macaroon.caveats;
180 | t.equal(caveats.length, 2);
181 | t.equal(testUtils.bytesToString(caveats[0].identifier), 'account = 3735928559');
182 | t.equal(testUtils.bytesToString(caveats[1].identifier), 'user = alice');
183 | t.equal(testUtils.bytesToBase64(macaroon.signature), 'S-lnzR6gxrJrr2pKlO6bBbFYhtoLqF6MQqk8jQ4SXvw');
184 | t.end();
185 | });
186 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | ## macaroon
4 | A JavaScript implementation of
5 | [macaroons](http://theory.stanford.edu/~ataly/Papers/macaroons.pdf)
6 | compatible with the [Go](http://github.com/go-macaroon/macaroon),
7 | [Python, and C ](https://github.com/rescrv/libmacaroons)
8 | implementations. Including functionality to interact with
9 | third party caveat dischargers implemented by the [Go macaroon
10 | bakery](http://github.com/go-macaroon-bakery/macaroon-bakery).
11 | It supports both version 1 and 2 macaroons in JSON and binary formats.
12 |
13 |
14 | * [macaroon](#module_macaroon)
15 | * [Macaroon#caveats](#exp_module_macaroon--Macaroon+caveats) ⇒ Array ⏏
16 | * [Macaroon#location](#exp_module_macaroon--Macaroon+location) ⇒ string ⏏
17 | * [Macaroon#identifier](#exp_module_macaroon--Macaroon+identifier) ⇒ Uint8Array ⏏
18 | * [Macaroon#signature](#exp_module_macaroon--Macaroon+signature) ⇒ Uint8Array ⏏
19 | * [base64ToBytes(s)](#exp_module_macaroon--base64ToBytes) ⇒ Uint8Array ⏏
20 | * [bytesToBase64(bytes)](#exp_module_macaroon--bytesToBase64) ⇒ string ⏏
21 | * [Macaroon#addThirdPartyCaveat(rootKeyBytes, caveatIdBytes, [locationStr])](#exp_module_macaroon--Macaroon+addThirdPartyCaveat) ⏏
22 | * [Macaroon#addFirstPartyCaveat(caveatIdBytes)](#exp_module_macaroon--Macaroon+addFirstPartyCaveat) ⏏
23 | * [Macaroon#bindToRoot(rootSig)](#exp_module_macaroon--Macaroon+bindToRoot) ⏏
24 | * [Macaroon#clone()](#exp_module_macaroon--Macaroon+clone) ⇒ Macaroon ⏏
25 | * [Macaroon#verify(rootKeyBytes, check, discharges)](#exp_module_macaroon--Macaroon+verify) ⏏
26 | * [Macaroon#exportJSON()](#exp_module_macaroon--Macaroon+exportJSON) ⇒ Object ⏏
27 | * [Macaroon#exportBinary()](#exp_module_macaroon--Macaroon+exportBinary) ⇒ Uint8Array ⏏
28 | * [importMacaroon(obj)](#exp_module_macaroon--importMacaroon) ⇒ Macaroon \| Array.<Macaroon> ⏏
29 | * [importMacaroons(obj)](#exp_module_macaroon--importMacaroons) ⇒ Array.<Macaroon> ⏏
30 | * [newMacaroon()](#exp_module_macaroon--newMacaroon) ⇒ Macaroon ⏏
31 | * [dischargeMacaroon(macaroon, getDischarge, onOk, onError)](#exp_module_macaroon--dischargeMacaroon) ⏏
32 |
33 |
34 |
35 | ### Macaroon#caveats ⇒ Array ⏏
36 | Return the caveats associated with the macaroon,
37 | as an array of caveats. A caveat is represented
38 | as an object with an identifier field (Uint8Array)
39 | and (for third party caveats) a location field (string),
40 | and verification id (Uint8Array).
41 |
42 | **Kind**: Exported member
43 | **Returns**: Array - - The macaroon's caveats.
44 |
45 |
46 | ### Macaroon#location ⇒ string ⏏
47 | Return the location of the macaroon.
48 |
49 | **Kind**: Exported member
50 | **Returns**: string - - The macaroon's location.
51 |
52 |
53 | ### Macaroon#identifier ⇒ Uint8Array ⏏
54 | Return the macaroon's identifier.
55 |
56 | **Kind**: Exported member
57 | **Returns**: Uint8Array - - The macaroon's identifier.
58 |
59 |
60 | ### Macaroon#signature ⇒ Uint8Array ⏏
61 | Return the signature of the macaroon.
62 |
63 | **Kind**: Exported member
64 | **Returns**: Uint8Array - - The macaroon's signature.
65 |
66 |
67 | ### base64ToBytes(s) ⇒ Uint8Array ⏏
68 | Convert a base64 string to a Uint8Array by decoding it.
69 | It copes with unpadded and URL-safe base64 encodings.
70 |
71 | **Kind**: Exported function
72 | **Returns**: Uint8Array - - The decoded bytes.
73 |
74 | | Param | Type | Description |
75 | | --- | --- | --- |
76 | | s | string | The base64 string to decode. |
77 |
78 |
79 |
80 | ### bytesToBase64(bytes) ⇒ string ⏏
81 | Convert a Uint8Array to a base64-encoded string
82 | using URL-safe, unpadded encoding.
83 |
84 | **Kind**: Exported function
85 | **Returns**: string - - The base64-encoded result.
86 |
87 | | Param | Type | Description |
88 | | --- | --- | --- |
89 | | bytes | Uint8Array | The bytes to encode. |
90 |
91 |
92 |
93 | ### Macaroon#addThirdPartyCaveat(rootKeyBytes, caveatIdBytes, [locationStr]) ⏏
94 | Adds a third party caveat to the macaroon. Using the given shared root key,
95 | caveat id and location hint. The caveat id should encode the root key in
96 | some way, either by encrypting it with a key known to the third party or by
97 | holding a reference to it stored in the third party's storage.
98 |
99 | **Kind**: Exported function
100 |
101 | | Param | Type |
102 | | --- | --- |
103 | | rootKeyBytes | Uint8Array |
104 | | caveatIdBytes | Uint8Array \| string |
105 | | [locationStr] | String |
106 |
107 |
108 |
109 | ### Macaroon#addFirstPartyCaveat(caveatIdBytes) ⏏
110 | Adds a caveat that will be verified by the target service.
111 |
112 | **Kind**: Exported function
113 |
114 | | Param | Type |
115 | | --- | --- |
116 | | caveatIdBytes | String \| Uint8Array |
117 |
118 |
119 |
120 | ### Macaroon#bindToRoot(rootSig) ⏏
121 | Binds the macaroon signature to the given root signature.
122 | This must be called on discharge macaroons with the primary
123 | macaroon's signature before sending the macaroons in a request.
124 |
125 | **Kind**: Exported function
126 |
127 | | Param | Type |
128 | | --- | --- |
129 | | rootSig | Uint8Array |
130 |
131 |
132 |
133 | ### Macaroon#clone() ⇒ Macaroon ⏏
134 | Returns a copy of the macaroon. Any caveats added to the returned macaroon
135 | will not effect the original.
136 |
137 | **Kind**: Exported function
138 | **Returns**: Macaroon - - The cloned macaroon.
139 |
140 |
141 | ### Macaroon#verify(rootKeyBytes, check, discharges) ⏏
142 | Verifies that the macaroon is valid. Throws exception if verification fails.
143 |
144 | **Kind**: Exported function
145 |
146 | | Param | Type | Description |
147 | | --- | --- | --- |
148 | | rootKeyBytes | Uint8Array | Must be the same that the macaroon was originally created with. |
149 | | check | function | 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. |
150 | | discharges | Array | |
151 |
152 |
153 |
154 | ### Macaroon#exportJSON() ⇒ Object ⏏
155 | Exports the macaroon to a JSON-serializable object.
156 | The version used depends on what version the
157 | macaroon was created with or imported from.
158 |
159 | **Kind**: Exported function
160 |
161 |
162 | ### Macaroon#exportBinary() ⇒ Uint8Array ⏏
163 | Exports the macaroon using binary format.
164 | The version will be the same as the version that the
165 | macaroon was created with or imported from.
166 |
167 | **Kind**: Exported function
168 |
169 |
170 | ### importMacaroon(obj) ⇒ Macaroon \| Array.<Macaroon> ⏏
171 | Returns a macaroon instance based on the object passed in.
172 | If obj is a string, it is assumed to be a base64-encoded
173 | macaroon in binary or JSON format.
174 | If obj is a Uint8Array, it is assumed to be a macaroon in
175 | binary format, as produced by the exportBinary method.
176 | Otherwise obj is assumed to be a object decoded from JSON,
177 | and will be unmarshaled as such.
178 |
179 | **Kind**: Exported function
180 |
181 | | Param | Description |
182 | | --- | --- |
183 | | obj | A deserialized JSON macaroon. |
184 |
185 |
186 |
187 | ### importMacaroons(obj) ⇒ Array.<Macaroon> ⏏
188 | Returns an array of macaroon instances based on the object passed in.
189 | If obj is a string, it is assumed to be a set of base64-encoded
190 | macaroons in binary or JSON format.
191 | If obj is a Uint8Array, it is assumed to be set of macaroons in
192 | binary format, as produced by the exportBinary method.
193 | If obj is an array, it is assumed to be an array of macaroon
194 | objects decoded from JSON.
195 | Otherwise obj is assumed to be a macaroon object decoded from JSON.
196 |
197 | This function accepts a strict superset of the formats accepted
198 | by importMacaroons. When decoding a single macaroon,
199 | it will return an array with one macaroon element.
200 |
201 | **Kind**: Exported function
202 |
203 | | Param | Description |
204 | | --- | --- |
205 | | obj | A deserialized JSON macaroon or macaroons. |
206 |
207 |
208 |
209 | ### newMacaroon() ⇒ Macaroon ⏏
210 | Create a new Macaroon with the given root key, identifier, location
211 | and signature and return it.
212 |
213 | **Kind**: Exported function
214 |
215 | | Type | Description |
216 | | --- | --- |
217 | | Object | The necessary values to generate a macaroon. It contains the following fields: identifier: {String | Uint8Array} location: {String} (optional) rootKey: {String | Uint8Array} version: {int} (optional; defaults to 2). |
218 |
219 |
220 |
221 | ### dischargeMacaroon(macaroon, getDischarge, onOk, onError) ⏏
222 | Gathers discharge macaroons for all third party caveats in the supplied
223 | macaroon (and any subsequent caveats required by those) calling getDischarge
224 | to acquire each discharge macaroon.
225 |
226 | **Kind**: Exported function
227 |
228 | | Param | Type | Description |
229 | | --- | --- | --- |
230 | | macaroon | Macaroon | The macaroon to discharge. |
231 | | getDischarge | function | Called with 5 arguments. macaroon.location {String} caveat.location {String} caveat.id {String} success {Function} failure {Function} |
232 | | onOk | function | Called with an array argument holding the macaroon as the first element followed by all the discharge macaroons. All the discharge macaroons will be bound to the primary macaroon. |
233 | | onError | function | Called if an error occurs during discharge. |
234 |
235 |
--------------------------------------------------------------------------------
/test/verify.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const test = require('tape');
4 |
5 | const m = require('../macaroon');
6 | const testUtils = require('./test-utils');
7 |
8 | const recursiveThirdPartyCaveatMacaroons = [{
9 | rootKey: 'root-key',
10 | id: 'root-id',
11 | caveats: [{
12 | condition: 'wonderful',
13 | }, {
14 | condition: 'bob-is-great',
15 | location: 'bob',
16 | rootKey: 'bob-caveat-root-key',
17 | }, {
18 | condition: 'charlie-is-great',
19 | location: 'charlie',
20 | rootKey: 'charlie-caveat-root-key',
21 | }],
22 | }, {
23 | location: 'bob',
24 | rootKey: 'bob-caveat-root-key',
25 | id: 'bob-is-great',
26 | caveats: [{
27 | condition: 'splendid',
28 | }, {
29 | condition: 'barbara-is-great',
30 | location: 'barbara',
31 | rootKey: 'barbara-caveat-root-key',
32 | }],
33 | }, {
34 | location: 'charlie',
35 | rootKey: 'charlie-caveat-root-key',
36 | id: 'charlie-is-great',
37 | caveats: [{
38 | condition: 'splendid',
39 | }, {
40 | condition: 'celine-is-great',
41 | location: 'celine',
42 | rootKey: 'celine-caveat-root-key',
43 | }],
44 | }, {
45 | location: 'barbara',
46 | rootKey: 'barbara-caveat-root-key',
47 | id: 'barbara-is-great',
48 | caveats: [{
49 | condition: 'spiffing',
50 | }, {
51 | condition: 'ben-is-great',
52 | location: 'ben',
53 | rootKey: 'ben-caveat-root-key',
54 | }],
55 | }, {
56 | location: 'ben',
57 | rootKey: 'ben-caveat-root-key',
58 | id: 'ben-is-great',
59 | }, {
60 | location: 'celine',
61 | rootKey: 'celine-caveat-root-key',
62 | id: 'celine-is-great',
63 | caveats: [{
64 | condition: 'high-fiving',
65 | }],
66 | }];
67 |
68 | const verifyTests = [{
69 | about: 'single third party caveat without discharge',
70 | macaroons: [{
71 | rootKey: 'root-key',
72 | id: 'root-id',
73 | caveats: [{
74 | condition: 'wonderful',
75 | }, {
76 | condition: 'bob-is-great',
77 | location: 'bob',
78 | rootKey: 'bob-caveat-root-key',
79 | }],
80 | }],
81 | conditions: [{
82 | conditions: {
83 | 'wonderful': true,
84 | },
85 | expectErr: /cannot find discharge macaroon for caveat "bob-is-great"/,
86 | }],
87 | }, {
88 | about: 'single third party caveat with discharge',
89 | macaroons: [{
90 | rootKey: 'root-key',
91 | id: 'root-id',
92 | caveats: [{
93 | condition: 'wonderful',
94 | }, {
95 | condition: 'bob-is-great',
96 | location: 'bob',
97 | rootKey: 'bob-caveat-root-key',
98 | }],
99 | }, {
100 | location: 'bob',
101 | rootKey: 'bob-caveat-root-key',
102 | id: 'bob-is-great',
103 | }],
104 | conditions: [{
105 | conditions: {
106 | 'wonderful': true,
107 | },
108 | }, {
109 | conditions: {
110 | 'wonderful': false,
111 | },
112 | expectErr: /condition "wonderful" not met/,
113 | }],
114 | }, {
115 | about: 'single third party caveat with discharge with mismatching root key',
116 | macaroons: [{
117 | rootKey: 'root-key',
118 | id: 'root-id',
119 | caveats: [{
120 | condition: 'wonderful',
121 | }, {
122 | condition: 'bob-is-great',
123 | location: 'bob',
124 | rootKey: 'bob-caveat-root-key',
125 | }],
126 | }, {
127 | location: 'bob',
128 | rootKey: 'bob-caveat-root-key-wrong',
129 | id: 'bob-is-great',
130 | }],
131 | conditions: [{
132 | conditions: {
133 | 'wonderful': true,
134 | },
135 | expectErr: /signature mismatch after caveat verification/,
136 | }],
137 | }, {
138 | about: 'single third party caveat with two discharges',
139 | macaroons: [{
140 | rootKey: 'root-key',
141 | id: 'root-id',
142 | caveats: [{
143 | condition: 'wonderful',
144 | }, {
145 | condition: 'bob-is-great',
146 | location: 'bob',
147 | rootKey: 'bob-caveat-root-key',
148 | }],
149 | }, {
150 | location: 'bob',
151 | rootKey: 'bob-caveat-root-key',
152 | id: 'bob-is-great',
153 | caveats: [{
154 | condition: 'splendid',
155 | }],
156 | }, {
157 | location: 'bob',
158 | rootKey: 'bob-caveat-root-key',
159 | id: 'bob-is-great',
160 | caveats: [{
161 | condition: 'top of the world',
162 | }],
163 | }],
164 | conditions: [{
165 | conditions: {
166 | 'wonderful': true,
167 | },
168 | expectErr: /condition "splendid" not met/,
169 | }, {
170 | conditions: {
171 | 'wonderful': true,
172 | 'splendid': true,
173 | 'top of the world': true,
174 | },
175 | expectErr: /discharge macaroon "bob-is-great" was not used/,
176 | }, {
177 | conditions: {
178 | 'wonderful': true,
179 | 'splendid': false,
180 | 'top of the world': true,
181 | },
182 | expectErr: /condition "splendid" not met/,
183 | }, {
184 | conditions: {
185 | 'wonderful': true,
186 | 'splendid': true,
187 | 'top of the world': false,
188 | },
189 | expectErr: /discharge macaroon "bob-is-great" was not used/,
190 | }],
191 | }, {
192 | about: 'one discharge used for two macaroons',
193 | macaroons: [{
194 | rootKey: 'root-key',
195 | id: 'root-id',
196 | caveats: [{
197 | condition: 'somewhere else',
198 | location: 'bob',
199 | rootKey: 'bob-caveat-root-key',
200 | }, {
201 | condition: 'bob-is-great',
202 | location: 'charlie',
203 | rootKey: 'bob-caveat-root-key',
204 | }],
205 | }, {
206 | location: 'bob',
207 | rootKey: 'bob-caveat-root-key',
208 | id: 'somewhere else',
209 | caveats: [{
210 | condition: 'bob-is-great',
211 | location: 'charlie',
212 | rootKey: 'bob-caveat-root-key',
213 | }],
214 | }, {
215 | location: 'bob',
216 | rootKey: 'bob-caveat-root-key',
217 | id: 'bob-is-great',
218 | }],
219 | conditions: [{
220 | expectErr: /discharge macaroon "bob-is-great" was used more than once/,
221 | }],
222 | }, {
223 | about: 'recursive third party caveat',
224 | macaroons: [{
225 | rootKey: 'root-key',
226 | id: 'root-id',
227 | caveats: [{
228 | condition: 'bob-is-great',
229 | location: 'bob',
230 | rootKey: 'bob-caveat-root-key',
231 | }],
232 | }, {
233 | location: 'bob',
234 | rootKey: 'bob-caveat-root-key',
235 | id: 'bob-is-great',
236 | caveats: [{
237 | condition: 'bob-is-great',
238 | location: 'charlie',
239 | rootKey: 'bob-caveat-root-key',
240 | }],
241 | }],
242 | conditions: [{
243 | expectErr: /discharge macaroon "bob-is-great" was used more than once/,
244 | }],
245 | }, {
246 | about: 'two third party caveats',
247 | macaroons: [{
248 | rootKey: 'root-key',
249 | id: 'root-id',
250 | caveats: [{
251 | condition: 'wonderful',
252 | }, {
253 | condition: 'bob-is-great',
254 | location: 'bob',
255 | rootKey: 'bob-caveat-root-key',
256 | }, {
257 | condition: 'charlie-is-great',
258 | location: 'charlie',
259 | rootKey: 'charlie-caveat-root-key',
260 | }],
261 | }, {
262 | location: 'bob',
263 | rootKey: 'bob-caveat-root-key',
264 | id: 'bob-is-great',
265 | caveats: [{
266 | condition: 'splendid',
267 | }],
268 | }, {
269 | location: 'charlie',
270 | rootKey: 'charlie-caveat-root-key',
271 | id: 'charlie-is-great',
272 | caveats: [{
273 | condition: 'top of the world',
274 | }],
275 | }],
276 | conditions: [{
277 | conditions: {
278 | 'wonderful': true,
279 | 'splendid': true,
280 | 'top of the world': true,
281 | },
282 | }, {
283 | conditions: {
284 | 'wonderful': true,
285 | 'splendid': false,
286 | 'top of the world': true,
287 | },
288 | expectErr: /condition "splendid" not met/,
289 | }, {
290 | conditions: {
291 | 'wonderful': true,
292 | 'splendid': true,
293 | 'top of the world': false,
294 | },
295 | expectErr: /condition "top of the world" not met/,
296 | }],
297 | }, {
298 | about: 'third party caveat with undischarged third party caveat',
299 | macaroons: [{
300 | rootKey: 'root-key',
301 | id: 'root-id',
302 | caveats: [{
303 | condition: 'wonderful',
304 | }, {
305 | condition: 'bob-is-great',
306 | location: 'bob',
307 | rootKey: 'bob-caveat-root-key',
308 | }],
309 | }, {
310 | location: 'bob',
311 | rootKey: 'bob-caveat-root-key',
312 | id: 'bob-is-great',
313 | caveats: [{
314 | condition: 'splendid',
315 | }, {
316 | condition: 'barbara-is-great',
317 | location: 'barbara',
318 | rootKey: 'barbara-caveat-root-key',
319 | }],
320 | }],
321 | conditions: [{
322 | conditions: {
323 | 'wonderful': true,
324 | 'splendid': true,
325 | },
326 | expectErr: /cannot find discharge macaroon for caveat "barbara-is-great"/,
327 | }],
328 | }, {
329 | about: 'recursive third party caveats',
330 | macaroons: recursiveThirdPartyCaveatMacaroons,
331 | conditions: [{
332 | conditions: {
333 | 'wonderful': true,
334 | 'splendid': true,
335 | 'high-fiving': true,
336 | 'spiffing': true,
337 | },
338 | }, {
339 | conditions: {
340 | 'wonderful': true,
341 | 'splendid': true,
342 | 'high-fiving': false,
343 | 'spiffing': true,
344 | },
345 | expectErr: /condition "high-fiving" not met/,
346 | }],
347 | }, {
348 | about: 'unused discharge',
349 | macaroons: [{
350 | rootKey: 'root-key',
351 | id: 'root-id',
352 | }, {
353 | rootKey: 'other-key',
354 | id: 'unused',
355 | }],
356 | conditions: [{
357 | expectErr: /discharge macaroon "unused" was not used/,
358 | }],
359 | }];
360 |
361 | test('verify', t => {
362 | let i;
363 | for (i in verifyTests) {
364 | const testData = verifyTests[i];
365 | t.test(`should work with ${testData.about}`, t => {
366 | let j;
367 | const keyMac = testUtils.makeMacaroons(testData.macaroons);
368 | const rootKey = keyMac[0];
369 | const primary = keyMac[1];
370 | const discharges = keyMac[2];
371 | for (j in testData.conditions) {
372 | const cond = testData.conditions[j];
373 | const check = cav => {
374 | if (cond.conditions[cav]) {
375 | return null;
376 | }
377 | return 'condition "' + cav + '" not met';
378 | };
379 | if (cond.expectErr) {
380 | t.throws(() => {
381 | primary.verify(rootKey, check, discharges);
382 | }, cond.expectErr, 'expected error ' + cond.expectErr);
383 | } else {
384 | primary.verify(rootKey, check, discharges);
385 | }
386 | // Cloned macaroon should have the same verify result.
387 | const clonedPrimary = primary.clone();
388 | if (cond.expectErr) {
389 | t.throws(() => {
390 | clonedPrimary.verify(rootKey, check, discharges);
391 | }, cond.expectErr, 'expected error ' + cond.expectErr);
392 | } else {
393 | clonedPrimary.verify(rootKey, check, discharges);
394 | }
395 | }
396 | t.end();
397 | });
398 | }
399 | });
400 |
401 |
402 | const externalRootKey = 'root-key';
403 |
404 | // Produced by running this code: http://play.golang.org/p/Cn7q91tuql
405 | const externalMacaroons = [
406 | {
407 | 'caveats': [
408 | {
409 | 'cid': 'wonderful'
410 | },
411 | {
412 | 'cid': 'bob-is-great',
413 | 'vid': 'YnpoATFtXlPux-ASP0iXsud5KqOAPy2zLxSjnGt0OY0L1XooSQagZeupd001spBjNh2IqG6i99OB9O2ERyNKMxpY5oMInKaC',
414 | 'cl': 'bob'
415 | },
416 | {
417 | 'cid': 'charlie-is-great',
418 | 'vid': 'pvwga-URCMCaYElz3pdB984Hy9efe7xyVeY0vdlil1-nVVsS4KVvOrG1eQvZdpN1oEEDydSuiLzHE3dJMpqZ-qXZ9RV4NJ7C',
419 | 'cl': 'charlie'
420 | }
421 | ],
422 | 'location': '',
423 | 'identifier': 'root-id',
424 | 'signature': '79240bb490c6940658106811ad7033de5047ea0ef295d2d882da53b2e43bf3a1'
425 | },
426 | {
427 | 'caveats': [
428 | {
429 | 'cid': 'splendid'
430 | }
431 | ],
432 | 'location': 'bob',
433 | 'identifier': 'bob-is-great',
434 | 'signature': '0389d56449d5e66289f7bfc8771757204051e9eb3ee99e522cf23484bdaf1629'
435 | },
436 | {
437 | 'caveats': [
438 | {
439 | 'cid': 'top of the world'
440 | }
441 | ],
442 | 'location': 'charlie',
443 | 'identifier': 'charlie-is-great',
444 | 'signature': 'c48affa09c0fd0560e2a3176b639c09b4bdf957a379660f86f7bb35e14c8865e'
445 | }
446 | ];
447 |
448 | test('should verify external third party macaroons correctly', t => {
449 | const ms = m.importMacaroons(externalMacaroons);
450 | ms[0].verify(externalRootKey, () => {}, ms.slice(1));
451 | t.end();
452 | });
453 |
454 | test('should handle incorrect root key correctly', t => {
455 | const ms = m.importMacaroons(externalMacaroons);
456 | t.throws(() => {
457 | ms[0].verify('wrong-key', () => {}, ms.slice(1));
458 | }, /decryption failed/, 'Should fail with decryption error');
459 | t.end();
460 | });
461 |
462 |
--------------------------------------------------------------------------------
/macaroon.js:
--------------------------------------------------------------------------------
1 | /**
2 | A JavaScript implementation of
3 | [macaroons](http://theory.stanford.edu/~ataly/Papers/macaroons.pdf)
4 | compatible with the [Go](http://github.com/go-macaroon/macaroon),
5 | [Python, and C ](https://github.com/rescrv/libmacaroons)
6 | implementations. Including functionality to interact with
7 | third party caveat dischargers implemented by the [Go macaroon
8 | bakery](http://github.com/go-macaroon-bakery/macaroon-bakery).
9 | It supports both version 1 and 2 macaroons in JSON and binary formats.
10 | @module macaroon
11 | */
12 |
13 | 'use strict';
14 |
15 | const sjcl = require('sjcl');
16 | const nacl = require('tweetnacl');
17 | const naclutil = require('tweetnacl-util');
18 |
19 | let TextEncoder, TextDecoder;
20 | if (typeof window !== 'undefined' && window && window.TextEncoder) {
21 | TextEncoder = window.TextEncoder;
22 | TextDecoder = window.TextDecoder;
23 | } else {
24 | // No window.TextEncoder if it's node.js.
25 | const util = require('util');
26 | TextEncoder = util.TextEncoder;
27 | TextDecoder = util.TextDecoder;
28 | }
29 |
30 | const utf8Encoder = new TextEncoder();
31 | const utf8Decoder = new TextDecoder('utf-8', {fatal : true});
32 |
33 | const NONCELEN = 24;
34 |
35 | const FIELD_EOS = 0;
36 | const FIELD_LOCATION = 1;
37 | const FIELD_IDENTIFIER = 2;
38 | const FIELD_VID = 4;
39 | const FIELD_SIGNATURE = 6;
40 |
41 | const maxInt = Math.pow(2, 32)-1;
42 |
43 | /**
44 | * Return a form of x suitable for including in a error message.
45 | * @param {any} x The object to be converted to string form.
46 | * @returns {string} - The converted object.
47 | */
48 | const toString = function(x) {
49 | if (x instanceof Array) {
50 | // Probably bitArray, try to convert it.
51 | try {x = bitsToBytes(x);} catch (e) {}
52 | }
53 | if (x instanceof Uint8Array) {
54 | if (isValidUTF8(x)) {
55 | x = bytesToString(x);
56 | } else {
57 | return `b64"${bytesToBase64(x)}"`;
58 | }
59 | }
60 | if (typeof x === 'string') {
61 | // TODO quote embedded double-quotes?
62 | return `"${x}"`;
63 | }
64 | return `type ${typeof x} (${JSON.stringify(x)})`;
65 | };
66 |
67 | const ByteBuffer = class ByteBuffer {
68 | /**
69 | * Create a new ByteBuffer. A ByteBuffer holds
70 | * a Uint8Array that it grows when written to.
71 | * @param {int} capacity The initial capacity of the buffer.
72 | */
73 | constructor(capacity) {
74 | this._buf = new Uint8Array(capacity);
75 | this._length = 0;
76 | }
77 | /**
78 | * Append several bytes to the buffer.
79 | * @param {Uint8Array} bytes The bytes to append.
80 | */
81 | appendBytes(bytes) {
82 | this._grow(this._length + bytes.length);
83 | this._buf.set(bytes, this._length);
84 | this._length += bytes.length;
85 | }
86 | /**
87 | * Append a single byte to the buffer.
88 | * @param {int} byte The byte to append
89 | */
90 | appendByte(byte) {
91 | this._grow(this._length + 1);
92 | this._buf[this._length] = byte;
93 | this._length++;
94 | }
95 | /**
96 | * Append a variable length integer to the buffer.
97 | * @param {int} x The number to append.
98 | */
99 | appendUvarint(x) {
100 | if (x > maxInt || x < 0) {
101 | throw new RangeError(`varint ${x} out of range`);
102 | }
103 | this._grow(this._length + maxVarintLen32);
104 | let i = this._length;
105 | while(x >= 0x80) {
106 | this._buf[i++] = (x & 0xff) | 0x80;
107 | x >>>= 7;
108 | }
109 | this._buf[i++] = x | 0;
110 | this._length = i;
111 | }
112 | /**
113 | * Return everything that has been appended to the buffer.
114 | * Note that the returned array is shared with the internal buffer.
115 | * @returns {Uint8Array} - The buffer.
116 | */
117 | get bytes() {
118 | return this._buf.subarray(0, this._length);
119 | }
120 | /**
121 | * Grow the internal buffer so that it's at least as big as minCap.
122 | * @param {int} minCap The minimum new capacity.
123 | */
124 | _grow(minCap) {
125 | const cap = this._buf.length;
126 | if (minCap <= cap) {
127 | return;
128 | }
129 | // Could use more intelligent logic to grow more slowly on large buffers
130 | // but this should be fine for macaroon use.
131 | const doubleCap = cap * 2;
132 | const newCap = minCap > doubleCap ? minCap : doubleCap;
133 | const newContent = new Uint8Array(newCap);
134 | newContent.set(this._buf.subarray(0, this._length));
135 | this._buf = newContent;
136 | }
137 | };
138 |
139 | const maxVarintLen32 = 5;
140 |
141 | const ByteReader = class ByteReader {
142 | /**
143 | * Create a new ByteReader that reads from the given buffer.
144 | * @param {Uint8Array} bytes The buffer to read from.
145 | */
146 | constructor(bytes) {
147 | this._buf = bytes;
148 | this._index = 0;
149 | }
150 | /**
151 | * Read a byte from the buffer. If there are no bytes left in the
152 | * buffer, throws a RangeError exception.
153 | * @returns {int} - The read byte.
154 | */
155 | readByte() {
156 | if (this.length <= 0) {
157 | throw new RangeError('Read past end of buffer');
158 | }
159 | return this._buf[this._index++];
160 | }
161 | /**
162 | * Inspect the next byte without consuming it.
163 | * If there are no bytes left in the
164 | * buffer, throws a RangeError exception.
165 | * @returns {int} - The peeked byte.
166 | */
167 | peekByte() {
168 | if (this.length <= 0) {
169 | throw new RangeError('Read past end of buffer');
170 | }
171 | return this._buf[this._index];
172 | }
173 | /**
174 | * Read a number of bytes from the buffer.
175 | * If there are not enough bytes left in the buffer,
176 | * throws a RangeError exception.
177 | * @param {int} n The number of bytes to read.
178 | */
179 | readN(n) {
180 | if (this.length < n) {
181 | throw new RangeError('Read past end of buffer');
182 | }
183 | const bytes = this._buf.subarray(this._index, this._index + n);
184 | this._index += n;
185 | return bytes;
186 | }
187 | /**
188 | * Return the size of the buffer.
189 | * @returns {int} - The number of bytes left to read in the buffer.
190 | */
191 | get length() {
192 | return this._buf.length - this._index;
193 | }
194 | /**
195 | * Read a variable length integer from the buffer.
196 | * If there are not enough bytes left in the buffer
197 | * or the encoded integer is too big, throws a
198 | * RangeError exception.
199 | * @returns {int} - The number that's been read.
200 | */
201 | readUvarint() {
202 | const length = this._buf.length;
203 | let x = 0;
204 | let shift = 0;
205 | for(let i = this._index; i < length; i++) {
206 | const b = this._buf[i];
207 | if (b < 0x80) {
208 | const n = i - this._index;
209 | this._index = i+1;
210 | if (n > maxVarintLen32 || n === maxVarintLen32 && b > 1) {
211 | throw new RangeError('Overflow error decoding varint');
212 | }
213 | return (x | (b << shift)) >>> 0;
214 | }
215 | x |= (b & 0x7f) << shift;
216 | shift += 7;
217 | }
218 | this._index = length;
219 | throw new RangeError('Buffer too small decoding varint');
220 | }
221 | };
222 |
223 | const isValue = x => x !== undefined && x !== null;
224 |
225 | /**
226 | * Convert a string to a Uint8Array by utf-8
227 | * encoding it.
228 | * @param {string} s The string to convert.
229 | * @returns {Uint8Array}
230 | */
231 | const stringToBytes = s => isValue(s) ? utf8Encoder.encode(s) : s;
232 |
233 | /**
234 | * Convert a Uint8Array to a string by
235 | * utf-8 decoding it. Throws an exception if
236 | * the bytes do not represent well-formed utf-8.
237 | * @param {Uint8Array} b The bytes to convert.
238 | * @returns {string}
239 | */
240 | const bytesToString = b => isValue(b) ? utf8Decoder.decode(b) : b;
241 |
242 | /**
243 | * Convert an sjcl bitArray to a string by
244 | * utf-8 decoding it. Throws an exception if
245 | * the bytes do not represent well-formed utf-8.
246 | * @param {bitArray} s The bytes to convert.
247 | * @returns {string}
248 | */
249 | const bitsToString = s => sjcl.codec.utf8String.fromBits(s);
250 |
251 | /**
252 | * Convert a base64 string to a Uint8Array by decoding it.
253 | * It copes with unpadded and URL-safe base64 encodings.
254 | * @param {string} s The base64 string to decode.
255 | * @returns {Uint8Array} - The decoded bytes.
256 | * @alias module:macaroon
257 | */
258 | const base64ToBytes = function(s) {
259 | s = s.replace(/-/g, '+').replace(/_/g, '/');
260 | if (s.length % 4 !== 0 && !s.match(/=$/)) {
261 | // Add the padding that's required by base64-js.
262 | s += '='.repeat(4 - s.length % 4);
263 | }
264 | return naclutil.decodeBase64(s);
265 | };
266 |
267 | /** Convert a Uint8Array to a base64-encoded string
268 | * using URL-safe, unpadded encoding.
269 | * @param {Uint8Array} bytes The bytes to encode.
270 | * @returns {string} - The base64-encoded result.
271 | * @alias module:macaroon
272 | */
273 | const bytesToBase64 = function(bytes) {
274 | return naclutil.encodeBase64(bytes)
275 | .replace(/=+$/, '')
276 | .replace(/\+/g, '-')
277 | .replace(/\//g, '_');
278 | };
279 |
280 | /**
281 | Converts a Uint8Array to a bitArray for use by nacl.
282 | @param {Uint8Array} arr The array to convert.
283 | @returns {bitArray} - The converted array.
284 | */
285 | const bytesToBits = function(arr) {
286 | // See https://github.com/bitwiseshiftleft/sjcl/issues/344 for why
287 | // we cannot just use sjcl.codec.bytes.toBits.
288 | return sjcl.codec.base64.toBits(naclutil.encodeBase64(arr));
289 | };
290 |
291 | /**
292 | Converts a bitArray to a Uint8Array.
293 | @param {bitArray} arr The array to convert.
294 | @returns {Uint8Array} - The converted array.
295 | */
296 | const bitsToBytes = function(arr) {
297 | // See https://github.com/bitwiseshiftleft/sjcl/issues/344 for why
298 | // we cannot just use sjcl.codec.bytes.toBits.
299 | return naclutil.decodeBase64(sjcl.codec.base64.fromBits(arr));
300 | };
301 |
302 | /**
303 | Converts a hex to Uint8Array
304 | @param {String} hex The hex value to convert.
305 | @returns {Uint8Array}
306 | */
307 | const hexToBytes = function(hex) {
308 | const arr = new Uint8Array(Math.ceil(hex.length / 2));
309 | for (let i = 0; i < arr.length; i++) {
310 | arr[i] = parseInt(hex.substr(i * 2, 2), 16);
311 | }
312 | return arr;
313 | };
314 |
315 | /**
316 | * Report whether the argument encodes a valid utf-8 string.
317 | * @param {Uint8Array} bytes The bytes to check.
318 | * @returns {boolean} - True if the bytes are valid utf-8.
319 | */
320 | const isValidUTF8 = function(bytes) {
321 | try {
322 | bytesToString(bytes);
323 | } catch (e) {
324 | // While https://encoding.spec.whatwg.org states that the
325 | // exception should be a TypeError, we'll be defensive here
326 | // and just treat any exception as signifying invalid utf-8.
327 | return false;
328 | }
329 | return true;
330 | };
331 |
332 | /**
333 | Check that supplied value is a string and return it. Throws an
334 | error including the provided label if not.
335 | @param {String} val The value to assert as a string
336 | @param {String} label The value label.
337 | @returns {String} - The supplied value.
338 | */
339 | const requireString = function(val, label) {
340 | if (typeof val !== 'string') {
341 | throw new TypeError(`${label} has the wrong type; want string, got ${typeof val}.`);
342 | }
343 | return val;
344 | };
345 |
346 | /**
347 | Check that supplied value is a string or undefined or null. Throws
348 | an error including the provided label if not. Always returns a string
349 | (the empty string if undefined or null).
350 |
351 | @param {(String | null)} val The value to assert as a string
352 | @param {String} label The value label.
353 | @returns {String} - The supplied value or an empty string.
354 | */
355 | const maybeString = (val, label) => isValue(val) ? requireString(val, label) : '';
356 |
357 | /**
358 | Check that supplied value is a Uint8Array or a string.
359 | Throws an error
360 | including the provided label if not.
361 | @param {(Uint8Array | string)} val The value to assert as a Uint8Array
362 | @param {string} label The value label.
363 | @returns {Uint8Array} - The supplied value, utf-8-encoded if it was a string.
364 | */
365 | const requireBytes = function(val, label) {
366 | if (val instanceof Uint8Array) {
367 | return val;
368 | }
369 | if (typeof(val) === 'string') {
370 | return stringToBytes(val);
371 | }
372 | throw new TypeError(`${label} has the wrong type; want string or Uint8Array, got ${typeof val}.`);
373 | };
374 |
375 | const emptyBytes = new Uint8Array();
376 |
377 | /**
378 | * Read a macaroon V2 field from the buffer. If the
379 | * field does not have the expected type, throws an exception.
380 | * @param {ByteReader} buf The buffer to read from.
381 | * @param {int} expectFieldType The required field type.
382 | * @returns {Uint8Array} - The contents of the field.
383 | */
384 | const readFieldV2 = function(buf, expectFieldType) {
385 | const fieldType = buf.readByte();
386 | if (fieldType !== expectFieldType) {
387 | throw new Error(`Unexpected field type, got ${fieldType} want ${expectFieldType}`);
388 | }
389 | if (fieldType === FIELD_EOS) {
390 | return emptyBytes;
391 | }
392 | return buf.readN(buf.readUvarint());
393 | };
394 |
395 | /**
396 | * Append a macaroon V2 field to the buffer.
397 | * @param {ByteBuffer} buf The buffer to append to.
398 | * @param {int} fieldType The type of the field.
399 | * @param {Uint8Array} data The content of the field.
400 | */
401 | const appendFieldV2 = function(buf, fieldType, data) {
402 | buf.appendByte(fieldType);
403 | if (fieldType !== FIELD_EOS) {
404 | buf.appendUvarint(data.length);
405 | buf.appendBytes(data);
406 | }
407 | };
408 |
409 | /**
410 | * Read an optionally-present macaroon V2 field from the buffer.
411 | * If the field is not present, returns null.
412 | * @param {ByteReader} buf The buffer to read from.
413 | * @param {int} maybeFieldType The expected field type.
414 | * @returns {Uint8Array | null} - The contents of the field, or null if not present.
415 | */
416 | const readFieldV2Optional = function(buf, maybeFieldType) {
417 | if (buf.peekByte() !== maybeFieldType) {
418 | return null;
419 | }
420 | return readFieldV2(buf, maybeFieldType);
421 | };
422 |
423 | /**
424 | * Sets a field in a V2 encoded JSON object.
425 | * @param {Object} obj The JSON object.
426 | * @param {string} key The key to set.
427 | * @param {Uint8Array} valBytes The key's value.
428 | */
429 | const setJSONFieldV2 = function(obj, key, valBytes) {
430 | if (isValidUTF8(valBytes)) {
431 | obj[key] = bytesToString(valBytes);
432 | } else {
433 | obj[key + '64'] = bytesToBase64(valBytes);
434 | }
435 | };
436 |
437 | /**
438 | Generate a hash using the supplied data.
439 | @param {bitArray} keyBits
440 | @param {bitArray} dataBits
441 | @returns {bitArray} - The keyed hash of the supplied data as a sjcl bitArray.
442 | */
443 | const keyedHash = function(keyBits, dataBits) {
444 | const hash = new sjcl.misc.hmac(keyBits, sjcl.hash.sha256);
445 | hash.update(dataBits);
446 | return hash.digest();
447 | };
448 |
449 | /**
450 | Generate a hash keyed with key of both data objects.
451 | @param {bitArray} keyBits
452 | @param {bitArray} d1Bits
453 | @param {bitArray} d2Bits
454 | @returns {bitArray} - The keyed hash of d1 and d2 as a sjcl bitArray.
455 | */
456 | const keyedHash2 = function(keyBits, d1Bits, d2Bits) {
457 | const h1Bits = keyedHash(keyBits, d1Bits);
458 | const h2Bits = keyedHash(keyBits, d2Bits);
459 | return keyedHash(keyBits, sjcl.bitArray.concat(h1Bits, h2Bits));
460 | };
461 |
462 | const keyGeneratorBits = bytesToBits(stringToBytes('macaroons-key-generator'));
463 |
464 | /**
465 | Generate a fixed length key for use as a nacl secretbox key.
466 | @param {bitArray} keyBits The key to convert.
467 | @returns {bitArray}
468 | */
469 | const makeKey = function(keyBits) {
470 | return keyedHash(keyGeneratorBits, keyBits);
471 | };
472 |
473 | /**
474 | Generate a random nonce as Uint8Array.
475 | @returns {Uint8Array}
476 | */
477 | const newNonce = function() {
478 | return nacl.randomBytes(NONCELEN);
479 | };
480 |
481 | /**
482 | Encrypt the given plaintext with the given key.
483 | @param {bitArray} keyBits encryption key.
484 | @param {bitArray} textBits plaintext.
485 | @returns {bitArray} - encrypted text.
486 | */
487 | const encrypt = function(keyBits, textBits) {
488 | const keyBytes = bitsToBytes(keyBits);
489 | const textBytes = bitsToBytes(textBits);
490 | const nonceBytes = newNonce();
491 | const dataBytes = nacl.secretbox(textBytes, nonceBytes, keyBytes);
492 | const ciphertextBytes = new Uint8Array(nonceBytes.length + dataBytes.length);
493 | ciphertextBytes.set(nonceBytes, 0);
494 | ciphertextBytes.set(dataBytes, nonceBytes.length);
495 | return bytesToBits(ciphertextBytes);
496 | };
497 |
498 | /**
499 | Decrypts the given cyphertext.
500 | @param {bitArray} keyBits decryption key.
501 | @param {bitArray} ciphertextBits encrypted text.
502 | @returns {bitArray} - decrypted text.
503 | */
504 | const decrypt = function(keyBits, ciphertextBits) {
505 | const keyBytes = bitsToBytes(keyBits);
506 | const ciphertextBytes = bitsToBytes(ciphertextBits);
507 | const nonceBytes = ciphertextBytes.slice(0, NONCELEN);
508 | const dataBytes = ciphertextBytes.slice(NONCELEN);
509 | let textBytes = nacl.secretbox.open(dataBytes, nonceBytes, keyBytes);
510 | if (!textBytes) {
511 | throw new Error('decryption failed');
512 | }
513 | return bytesToBits(textBytes);
514 | };
515 |
516 | const zeroKeyBits = bytesToBits(stringToBytes('\0'.repeat(32)));
517 |
518 | /**
519 | Bind a given macaroon to the given signature of its parent macaroon. If the
520 | keys already match then it will return the rootSig.
521 | @param {bitArray} rootSigBits
522 | @param {bitArray} dischargeSigBits
523 | @returns {bitArray} - The bound macaroon signature.
524 | */
525 | const bindForRequest = function(rootSigBits, dischargeSigBits) {
526 | if (sjcl.bitArray.equal(rootSigBits, dischargeSigBits)) {
527 | return rootSigBits;
528 | }
529 | return keyedHash2(zeroKeyBits, rootSigBits, dischargeSigBits);
530 | };
531 |
532 | const Macaroon = class Macaroon {
533 | /**
534 | Create a new Macaroon with the given root key, identifier, location
535 | and signature.
536 | @param {Object} params The necessary values to generate a macaroon.
537 | It contains the following fields:
538 | identifierBytes: {Uint8Array}
539 | locationStr: {string}
540 | caveats: {Array of {locationStr: string, identifierBytes: Uint8Array, vidBytes: Uint8Array}}
541 | signatureBytes: {Uint8Array}
542 | version: {int} The version of macaroon to create.
543 | */
544 | constructor(params) {
545 | if (!params) {
546 | // clone uses null parameters.
547 | return;
548 | }
549 | let {version, identifierBytes, locationStr, caveats, signatureBytes} = params;
550 | if (version !== 1 && version !== 2) {
551 | throw new Error(`Unexpected version ${version}`);
552 | }
553 | this._version = version;
554 | this._locationStr = locationStr;
555 | identifierBytes = requireBytes(identifierBytes, 'Identifier');
556 | if (version === 1 && !isValidUTF8(identifierBytes)) {
557 | throw new Error('Version 1 macaroon identifier must be well-formed UTF-8');
558 | }
559 | this._identifierBits = identifierBytes && bytesToBits(identifierBytes);
560 | this._signatureBits = signatureBytes && bytesToBits(requireBytes(signatureBytes, 'Signature'));
561 | this._caveats = caveats ? caveats.map(cav => {
562 | const identifierBytes = requireBytes(cav.identifierBytes, 'Caveat identifier');
563 | if (version === 1 && !isValidUTF8(identifierBytes)) {
564 | throw new Error('Version 1 caveat identifier must be well-formed UTF-8');
565 | }
566 | return {
567 | _locationStr: maybeString(cav.locationStr),
568 | _identifierBits: bytesToBits(identifierBytes),
569 | _vidBits: cav.vidBytes && bytesToBits(requireBytes(cav.vidBytes, 'Verification ID')),
570 | };
571 | }) : [];
572 | }
573 |
574 | /**
575 | * Return the caveats associated with the macaroon,
576 | * as an array of caveats. A caveat is represented
577 | * as an object with an identifier field (Uint8Array)
578 | * and (for third party caveats) a location field (string),
579 | * and verification id (Uint8Array).
580 | * @returns {Array} - The macaroon's caveats.
581 | * @alias module:macaroon
582 | */
583 | get caveats() {
584 | return this._caveats.map(cav => {
585 | return isValue(cav._vidBits) ? {
586 | identifier: bitsToBytes(cav._identifierBits),
587 | location: cav._locationStr,
588 | vid: bitsToBytes(cav._vidBits),
589 | } : {
590 | identifier: bitsToBytes(cav._identifierBits),
591 | };
592 | });
593 | }
594 |
595 | /**
596 | * Return the location of the macaroon.
597 | * @returns {string} - The macaroon's location.
598 | * @alias module:macaroon
599 | */
600 | get location() {
601 | return this._locationStr;
602 | }
603 |
604 | /**
605 | * Return the macaroon's identifier.
606 | * @returns {Uint8Array} - The macaroon's identifier.
607 | * @alias module:macaroon
608 | */
609 | get identifier() {
610 | return bitsToBytes(this._identifierBits);
611 | }
612 |
613 | /**
614 | * Return the signature of the macaroon.
615 | * @returns {Uint8Array} - The macaroon's signature.
616 | * @alias module:macaroon
617 | */
618 | get signature() {
619 | return bitsToBytes(this._signatureBits);
620 | }
621 |
622 | /**
623 | Adds a third party caveat to the macaroon. Using the given shared root key,
624 | caveat id and location hint. The caveat id should encode the root key in
625 | some way, either by encrypting it with a key known to the third party or by
626 | holding a reference to it stored in the third party's storage.
627 | @param {Uint8Array} rootKeyBytes
628 | @param {(Uint8Array | string)} caveatIdBytes
629 | @param {String} [locationStr]
630 | @alias module:macaroon
631 | */
632 | addThirdPartyCaveat(rootKeyBytes, caveatIdBytes, locationStr) {
633 | const cav = {
634 | _identifierBits: bytesToBits(requireBytes(caveatIdBytes, 'Caveat id')),
635 | _vidBits: encrypt(
636 | this._signatureBits,
637 | makeKey(bytesToBits(requireBytes(rootKeyBytes, 'Caveat root key')))),
638 | _locationStr: maybeString(locationStr),
639 | };
640 | this._signatureBits = keyedHash2(
641 | this._signatureBits,
642 | cav._vidBits,
643 | cav._identifierBits
644 | );
645 | this._caveats.push(cav);
646 | }
647 |
648 | /**
649 | Adds a caveat that will be verified by the target service.
650 | @param {String | Uint8Array} caveatIdBytes
651 | @alias module:macaroon
652 | */
653 | addFirstPartyCaveat(caveatIdBytes) {
654 | const identifierBits = bytesToBits(requireBytes(caveatIdBytes, 'Condition'));
655 | this._caveats.push({
656 | _identifierBits: identifierBits,
657 | });
658 | this._signatureBits = keyedHash(this._signatureBits, identifierBits);
659 | }
660 |
661 | /**
662 | Binds the macaroon signature to the given root signature.
663 | This must be called on discharge macaroons with the primary
664 | macaroon's signature before sending the macaroons in a request.
665 | @param {Uint8Array} rootSig
666 | @alias module:macaroon
667 | */
668 | bindToRoot(rootSig) {
669 | const rootSigBits = bytesToBits(requireBytes(rootSig, 'Primary macaroon signature'));
670 | this._signatureBits = bindForRequest(rootSigBits, this._signatureBits);
671 | }
672 |
673 | /**
674 | Returns a copy of the macaroon. Any caveats added to the returned macaroon
675 | will not effect the original.
676 | @returns {Macaroon} - The cloned macaroon.
677 | @alias module:macaroon
678 | */
679 | clone() {
680 | const m = new Macaroon(null);
681 | m._version = this._version;
682 | m._signatureBits = this._signatureBits;
683 | m._identifierBits = this._identifierBits;
684 | m._locationStr = this._locationStr;
685 | m._caveats = this._caveats.slice();
686 | return m;
687 | }
688 |
689 | /**
690 | Verifies that the macaroon is valid. Throws exception if verification fails.
691 | @param {Uint8Array} rootKeyBytes Must be the same that the macaroon was
692 | originally created with.
693 | @param {Function} check Called to verify each first-party caveat. It
694 | is passed the condition to check (a string) and should return an error string if the condition
695 | is not met, or null if satisfied.
696 | @param {Array} discharges
697 | @alias module:macaroon
698 | */
699 | verify(rootKeyBytes, check, discharges = []) {
700 | const rootKeyBits = makeKey(bytesToBits(requireBytes(rootKeyBytes, 'Root key')));
701 | const used = discharges.map(d => 0);
702 |
703 | this._verify(this._signatureBits, rootKeyBits, check, discharges, used);
704 |
705 | discharges.forEach((dm, i) => {
706 | if (used[i] === 0) {
707 | throw new Error(
708 | `discharge macaroon ${toString(dm.identifier)} was not used`);
709 | }
710 | if (used[i] !== 1) {
711 | // Should be impossible because of check in verify, but be defensive.
712 | throw new Error(
713 | `discharge macaroon ${toString(dm.identifier)} was used more than once`);
714 | }
715 | });
716 | }
717 |
718 | _verify(rootSigBits, rootKeyBits, check, discharges, used) {
719 | let caveatSigBits = keyedHash(rootKeyBits, this._identifierBits);
720 | this._caveats.forEach(caveat => {
721 | if (caveat._vidBits) {
722 | const cavKeyBits = decrypt(caveatSigBits, caveat._vidBits);
723 | let found = false;
724 | let di, dm;
725 | for (di = 0; di < discharges.length; di++) {
726 | dm = discharges[di];
727 | if (!sjcl.bitArray.equal(dm._identifierBits, caveat._identifierBits)) {
728 | continue;
729 | }
730 | found = true;
731 | // It's important that we do this before calling _verify,
732 | // as it prevents potentially infinite recursion.
733 | used[di]++;
734 | if (used[di] > 1) {
735 | throw new Error(
736 | `discharge macaroon ${toString(dm.identifier)} was used more than once`);
737 | }
738 | dm._verify(rootSigBits, cavKeyBits, check, discharges, used);
739 | break;
740 | }
741 | if (!found) {
742 | throw new Error(
743 | `cannot find discharge macaroon for caveat ${toString(caveat._identifierBits)}`);
744 | }
745 | caveatSigBits = keyedHash2(caveatSigBits, caveat._vidBits, caveat._identifierBits);
746 | } else {
747 | const cond = bitsToString(caveat._identifierBits);
748 | const err = check(cond);
749 | if (err) {
750 | throw new Error(`caveat check failed (${cond}): ${err}`);
751 | }
752 | caveatSigBits = keyedHash(caveatSigBits, caveat._identifierBits);
753 | }
754 | });
755 | const boundSigBits = bindForRequest(rootSigBits, caveatSigBits);
756 | if (!sjcl.bitArray.equal(boundSigBits, this._signatureBits)) {
757 | throw new Error('signature mismatch after caveat verification');
758 | }
759 | }
760 |
761 |
762 | /**
763 | Exports the macaroon to a JSON-serializable object.
764 | The version used depends on what version the
765 | macaroon was created with or imported from.
766 | @returns {Object}
767 | @alias module:macaroon
768 | */
769 | exportJSON() {
770 | switch (this._version) {
771 | case 1:
772 | return this._exportAsJSONObjectV1();
773 | case 2:
774 | return this._exportAsJSONObjectV2();
775 | default:
776 | throw new Error(`unexpected macaroon version ${this._version}`);
777 | }
778 | }
779 |
780 | /**
781 | Returns a JSON compatible object representation of this version 1 macaroon.
782 | @returns {Object} - JSON compatible representation of this macaroon.
783 | */
784 | _exportAsJSONObjectV1() {
785 | const obj = {
786 | identifier: bitsToString(this._identifierBits),
787 | signature: sjcl.codec.hex.fromBits(this._signatureBits),
788 | };
789 | if (this._locationStr) {
790 | obj.location = this._locationStr;
791 | }
792 | if (this._caveats.length > 0) {
793 | obj.caveats = this._caveats.map(caveat => {
794 | const caveatObj = {
795 | cid: bitsToString(caveat._identifierBits),
796 | };
797 | if (caveat._vidBits) {
798 | // Use URL encoding and do not append "=" characters.
799 | caveatObj.vid = sjcl.codec.base64.fromBits(caveat._vidBits, true, true);
800 | caveatObj.cl = caveat._locationStr;
801 | }
802 | return caveatObj;
803 | });
804 | }
805 | return obj;
806 | }
807 |
808 | /**
809 | Returns the V2 JSON serialization of this macaroon.
810 | @returns {Object} - JSON compatible representation of this macaroon.
811 | */
812 | _exportAsJSONObjectV2() {
813 | const obj = {
814 | v: 2, // version
815 | };
816 | setJSONFieldV2(obj, 's', bitsToBytes(this._signatureBits));
817 | setJSONFieldV2(obj, 'i', bitsToBytes(this._identifierBits));
818 | if (this._locationStr) {
819 | obj.l = this._locationStr;
820 | }
821 | if (this._caveats && this._caveats.length > 0) {
822 | obj.c = this._caveats.map(caveat => {
823 | const caveatObj = {};
824 | setJSONFieldV2(caveatObj, 'i', bitsToBytes(caveat._identifierBits));
825 | if (caveat._vidBits) {
826 | setJSONFieldV2(caveatObj, 'v', bitsToBytes(caveat._vidBits));
827 | caveatObj.l = caveat._locationStr;
828 | }
829 | return caveatObj;
830 | });
831 | }
832 | return obj;
833 | }
834 |
835 | /**
836 | * Exports the macaroon using the v1 binary format.
837 | * @returns {Uint8Array} - Serialized macaroon
838 | */
839 | _exportBinaryV1() {
840 | throw new Error('V1 binary export not supported');
841 | };
842 |
843 | /**
844 | Exports the macaroon using the v2 binary format.
845 | @returns {Uint8Array} - Serialized macaroon
846 | */
847 | _exportBinaryV2() {
848 | const buf = new ByteBuffer(200);
849 | buf.appendByte(2);
850 | if (this._locationStr) {
851 | appendFieldV2(buf, FIELD_LOCATION, stringToBytes(this._locationStr));
852 | }
853 | appendFieldV2(buf, FIELD_IDENTIFIER, bitsToBytes(this._identifierBits));
854 | appendFieldV2(buf, FIELD_EOS);
855 | this._caveats.forEach(function(cav) {
856 | if (cav._locationStr) {
857 | appendFieldV2(buf, FIELD_LOCATION, stringToBytes(cav._locationStr));
858 | }
859 | appendFieldV2(buf, FIELD_IDENTIFIER, bitsToBytes(cav._identifierBits));
860 | if (cav._vidBits) {
861 | appendFieldV2(buf, FIELD_VID, bitsToBytes(cav._vidBits));
862 | }
863 | appendFieldV2(buf, FIELD_EOS);
864 | });
865 | appendFieldV2(buf, FIELD_EOS);
866 | appendFieldV2(buf, FIELD_SIGNATURE, bitsToBytes(this._signatureBits));
867 | return buf.bytes;
868 | };
869 |
870 | /**
871 | Exports the macaroon using binary format.
872 | The version will be the same as the version that the
873 | macaroon was created with or imported from.
874 | @returns {Uint8Array}
875 | @alias module:macaroon
876 | */
877 | exportBinary() {
878 | switch (this._version) {
879 | case 1:
880 | return this._exportBinaryV1();
881 | case 2:
882 | return this._exportBinaryV2();
883 | default:
884 | throw new Error(`unexpected macaroon version ${this._version}`);
885 | }
886 | };
887 | };
888 |
889 | /**
890 | Returns a macaroon instance based on the object passed in.
891 | If obj is a string, it is assumed to be a base64-encoded
892 | macaroon in binary or JSON format.
893 | If obj is a Uint8Array, it is assumed to be a macaroon in
894 | binary format, as produced by the exportBinary method.
895 | Otherwise obj is assumed to be a object decoded from JSON,
896 | and will be unmarshaled as such.
897 | @param obj A deserialized JSON macaroon.
898 | @returns {Macaroon | Macaroon[]}
899 | @alias module:macaroon
900 | */
901 | const importMacaroon = function(obj) {
902 | if (typeof obj === 'string') {
903 | obj = base64ToBytes(obj);
904 | }
905 | if (obj instanceof Uint8Array) {
906 | const buf = new ByteReader(obj);
907 | const m = importBinary(buf);
908 | if (buf.length !== 0) {
909 | throw new TypeError('extra data found at end of serialized macaroon');
910 | }
911 | return m;
912 | }
913 | if (Array.isArray(obj)) {
914 | throw new TypeError('cannot import an array of macaroons as a single macaroon');
915 | }
916 | return importJSON(obj);
917 | };
918 |
919 | /**
920 | Returns an array of macaroon instances based on the object passed in.
921 | If obj is a string, it is assumed to be a set of base64-encoded
922 | macaroons in binary or JSON format.
923 | If obj is a Uint8Array, it is assumed to be set of macaroons in
924 | binary format, as produced by the exportBinary method.
925 | If obj is an array, it is assumed to be an array of macaroon
926 | objects decoded from JSON.
927 | Otherwise obj is assumed to be a macaroon object decoded from JSON.
928 |
929 | This function accepts a strict superset of the formats accepted
930 | by importMacaroons. When decoding a single macaroon,
931 | it will return an array with one macaroon element.
932 |
933 | @param obj A deserialized JSON macaroon or macaroons.
934 | @returns {Macaroon[]}
935 | @alias module:macaroon
936 | */
937 | const importMacaroons = function(obj) {
938 | if (typeof obj === 'string') {
939 | obj = base64ToBytes(obj);
940 | }
941 | if (obj instanceof Uint8Array) {
942 | if (obj.length === 0) {
943 | throw new TypeError('empty macaroon data');
944 | }
945 | const buf = new ByteReader(obj);
946 | const ms = [];
947 | do {
948 | ms.push(importBinary(buf));
949 | } while (buf.length > 0);
950 | return ms;
951 | }
952 | if (Array.isArray(obj)) {
953 | return obj.map(val => importJSON(val));
954 | }
955 | return [importJSON(obj)];
956 | };
957 |
958 | /**
959 | Returns a macaroon instance imported from a JSON-decoded object.
960 | @param {object} obj The JSON to import from.
961 | @returns {Macaroon}
962 | */
963 | const importJSON = function(obj) {
964 | if (isValue(obj.signature)) {
965 | // Looks like a V1 macaroon.
966 | return importJSONV1(obj);
967 | }
968 | return importJSONV2(obj);
969 | };
970 |
971 | const importJSONV1 = function(obj) {
972 | const caveats = obj.caveats && obj.caveats.map(jsonCaveat => {
973 | const caveat = {
974 | identifierBytes: stringToBytes(requireString(jsonCaveat.cid, 'Caveat id')),
975 | locationStr: maybeString(jsonCaveat.cl, 'Caveat location'),
976 | };
977 | if (jsonCaveat.vid) {
978 | caveat.vidBytes = base64ToBytes(requireString(jsonCaveat.vid, 'Caveat verification id'));
979 | }
980 | return caveat;
981 | });
982 | return new Macaroon({
983 | version: 1,
984 | locationStr: maybeString(obj.location, 'Macaroon location'),
985 | identifierBytes: stringToBytes(requireString(obj.identifier, 'Macaroon identifier')),
986 | caveats: caveats,
987 | signatureBytes: hexToBytes(obj.signature),
988 | });
989 | };
990 |
991 | /**
992 | * Imports V2 JSON macaroon encoding.
993 | * @param {Object|Array} obj A serialized JSON macaroon
994 | * @returns {Macaroon}
995 | */
996 | const importJSONV2 = function(obj) {
997 | // The Go macaroon library omits the version, so we'll assume that
998 | // it is 2 in that case. See https://github.com/go-macaroon/macaroon/issues/35
999 | if (obj.v !== 2 && obj.v !== undefined) {
1000 | throw new Error(`Unsupported macaroon version ${obj.v}`);
1001 | }
1002 | const params = {
1003 | version: 2,
1004 | signatureBytes: v2JSONField(obj, 's', true),
1005 | locationStr: bytesToString(v2JSONField(obj, 'l', false)),
1006 | identifierBytes: v2JSONField(obj, 'i', true),
1007 | };
1008 | if (obj.c) {
1009 | if (!Array.isArray(obj.c)) {
1010 | throw new Error('caveats field does not hold an array');
1011 | }
1012 | params.caveats = obj.c.map(caveat => {
1013 | return {
1014 | identifierBytes: v2JSONField(caveat, 'i', true),
1015 | locationStr: bytesToString(v2JSONField(caveat, 'l'), false),
1016 | vidBytes: v2JSONField(caveat, 'v', false)
1017 | };
1018 | });
1019 | }
1020 | return new Macaroon(params);
1021 | };
1022 |
1023 | /**
1024 | * Read a JSON field that might be in base64 or string
1025 | * format.
1026 | * @param {Object} obj A deserialized JSON object.
1027 | * @param {string} key The key name.
1028 | * @param {boolean} required Whether the key is required to exist.
1029 | * @returns {Uint8Array} - The value of the key (or null if not present).
1030 | */
1031 | const v2JSONField = function(obj, key, required) {
1032 | if (obj.hasOwnProperty(key)) {
1033 | return stringToBytes(obj[key]);
1034 | }
1035 | const key64 = key + '64';
1036 | if (obj.hasOwnProperty(key64)) {
1037 | return base64ToBytes(obj[key64]);
1038 | }
1039 | if (required) {
1040 | throw new Error('Expected key: ' + key);
1041 | }
1042 | return null;
1043 | };
1044 |
1045 | /**
1046 | * Import a macaroon from the v2 binary format
1047 | * @param {ByteReader} buf A buffer holding the serialized macaroon.
1048 | * @returns {Macaroon}
1049 | */
1050 | const importBinaryV2 = function(buf) {
1051 | const version = buf.readByte();
1052 | if (version !== 2) {
1053 | throw new Error(`Only version 2 is supported, found version ${version}`);
1054 | }
1055 | const params = {
1056 | version: version,
1057 | };
1058 | params.locationStr = bytesToString(readFieldV2Optional(buf, FIELD_LOCATION));
1059 | params.identifierBytes = readFieldV2(buf, FIELD_IDENTIFIER);
1060 | readFieldV2(buf, FIELD_EOS);
1061 | params.caveats= [];
1062 | for (;;) {
1063 | if (readFieldV2Optional(buf, FIELD_EOS)) {
1064 | break;
1065 | }
1066 | const cav = {};
1067 | cav.locationStr = bytesToString(readFieldV2Optional(buf, FIELD_LOCATION));
1068 | cav.identifierBytes = readFieldV2(buf, FIELD_IDENTIFIER);
1069 | cav.vidBytes = readFieldV2Optional(buf, FIELD_VID);
1070 | readFieldV2(buf, FIELD_EOS);
1071 | params.caveats.push(cav);
1072 | }
1073 | params.signatureBytes = readFieldV2(buf, FIELD_SIGNATURE);
1074 | if (buf.length !== 0) {
1075 | throw new Error('unexpected extra data at end of macaroon');
1076 | }
1077 | return new Macaroon(params);
1078 | };
1079 |
1080 | const isASCIIHex = function(charCode) {
1081 | return (48 <= charCode && charCode <= 58) || (97 <= charCode && charCode <= 102);
1082 | };
1083 |
1084 | /**
1085 | * Import a macaroon from binary format (currently only supports V2 format).
1086 | * @param {Uint8Array} buf The serialized macaroon.
1087 | */
1088 | const importBinary = function(buf) {
1089 | if (buf.length === 0) {
1090 | throw new Error('Empty macaroon data');
1091 | }
1092 | const version = buf.peekByte();
1093 | if (version === 2) {
1094 | return importBinaryV2(buf);
1095 | }
1096 | if (isASCIIHex(version)) {
1097 | // It's a hex digit - version 1 binary format.
1098 | throw new Error('Version 1 binary format not supported');
1099 | }
1100 | throw new Error('Cannot determine data format of binary-encoded macaroon');
1101 | };
1102 |
1103 | /**
1104 | Create a new Macaroon with the given root key, identifier, location
1105 | and signature and return it.
1106 | @param {Object} - The necessary values to generate a macaroon.
1107 | It contains the following fields:
1108 | identifier: {String | Uint8Array}
1109 | location: {String} (optional)
1110 | rootKey: {String | Uint8Array}
1111 | version: {int} (optional; defaults to 2).
1112 | @returns {Macaroon}
1113 | @alias module:macaroon
1114 | */
1115 | const newMacaroon = function({identifier, location, rootKey, version} = {}) {
1116 | const identifierBytes = requireBytes(identifier, 'Macaroon identifier');
1117 | const rootKeyBytes = requireBytes(rootKey, 'Macaroon root key');
1118 | return new Macaroon({
1119 | version: version === undefined ? 2 : version,
1120 | identifierBytes: identifierBytes,
1121 | locationStr: maybeString(location, 'Macaroon location'),
1122 | signatureBytes: bitsToBytes(keyedHash(
1123 | makeKey(bytesToBits(rootKeyBytes)),
1124 | bytesToBits(identifierBytes))),
1125 | });
1126 | };
1127 |
1128 | /**
1129 | Gathers discharge macaroons for all third party caveats in the supplied
1130 | macaroon (and any subsequent caveats required by those) calling getDischarge
1131 | to acquire each discharge macaroon.
1132 | @param {Macaroon} macaroon - The macaroon to discharge.
1133 | @param {Function} getDischarge - Called with 5 arguments.
1134 | macaroon.location {String}
1135 | caveat.location {String}
1136 | caveat.id {String}
1137 | success {Function}
1138 | failure {Function}
1139 | @param {Function} onOk - Called with an array argument holding the macaroon
1140 | as the first element followed by all the discharge macaroons. All the
1141 | discharge macaroons will be bound to the primary macaroon.
1142 | @param {Function} onError - Called if an error occurs during discharge.
1143 | @alias module:macaroon
1144 | */
1145 | const dischargeMacaroon = function (macaroon, getDischarge, onOk, onError) {
1146 | const primarySig = macaroon.signature;
1147 | const discharges = [macaroon];
1148 | let pendingCount = 0;
1149 | let errorCalled = false;
1150 | const firstPartyLocation = macaroon.location;
1151 | let dischargeCaveats;
1152 | const dischargedCallback = dm => {
1153 | if (errorCalled) {
1154 | return;
1155 | }
1156 | dm.bindToRoot(primarySig);
1157 | discharges.push(dm);
1158 | pendingCount--;
1159 | dischargeCaveats(dm);
1160 | };
1161 | const dischargedErrorCallback = err => {
1162 | if (!errorCalled) {
1163 | onError(err);
1164 | errorCalled = true;
1165 | }
1166 | };
1167 | dischargeCaveats = m => {
1168 | let cav, i;
1169 | for (i = 0; i < m._caveats.length; i++) {
1170 | cav = m._caveats[i];
1171 | if (!cav._vidBits) {
1172 | continue;
1173 | }
1174 | getDischarge(
1175 | firstPartyLocation,
1176 | cav._locationStr,
1177 | bitsToBytes(cav._identifierBits),
1178 | dischargedCallback,
1179 | dischargedErrorCallback
1180 | );
1181 | pendingCount++;
1182 | }
1183 | if (pendingCount === 0) {
1184 | onOk(discharges);
1185 | return;
1186 | }
1187 | };
1188 | dischargeCaveats(macaroon);
1189 | };
1190 |
1191 | module.exports = {
1192 | newMacaroon,
1193 | dischargeMacaroon,
1194 | importMacaroons,
1195 | importMacaroon,
1196 | bytesToBase64: bytesToBase64,
1197 | base64ToBytes: base64ToBytes,
1198 | };
1199 |
--------------------------------------------------------------------------------