├── .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 | --------------------------------------------------------------------------------