From 86e9e8972b30863c9f12116cd52e04fe526a6305 Mon Sep 17 00:00:00 2001 From: Buck Perley Date: Mon, 21 Jun 2021 23:06:37 -0500 Subject: [PATCH] add npm prepare script, fix verifiers, refactor helper methods (#9) * add npm prepare script * fix verifier typo * update fix for verifiers * refactor and add some tests --- .travis.yml | 2 +- .vscode/settings.json | 10 + README.md | 6 +- dist/caveat.d.ts | 71 ------- dist/caveat.js | 261 -------------------------- dist/helpers.d.ts | 7 - dist/helpers.js | 36 ---- dist/identifier.d.ts | 42 ----- dist/identifier.js | 119 ------------ dist/index.d.ts | 5 - dist/index.js | 18 -- dist/lsat.d.ts | 126 ------------- dist/lsat.js | 381 -------------------------------------- dist/satisfiers.d.ts | 11 -- dist/satisfiers.js | 33 ---- dist/types/index.d.ts | 2 - dist/types/index.js | 14 -- dist/types/lsat.d.ts | 41 ---- dist/types/lsat.js | 2 - dist/types/satisfier.d.ts | 31 ---- dist/types/satisfier.js | 2 - package.json | 1 + src/caveat.ts | 56 +----- src/helpers.ts | 6 +- src/index.ts | 1 + src/macaroon.ts | 68 +++++++ src/types/index.ts | 15 ++ tests/caveat.spec.ts | 50 +++-- tests/macaroon.spec.ts | 95 ++++++++++ tests/utilities.ts | 6 +- yarn.lock | 24 +-- 31 files changed, 248 insertions(+), 1294 deletions(-) create mode 100644 .vscode/settings.json delete mode 100644 dist/caveat.d.ts delete mode 100644 dist/caveat.js delete mode 100644 dist/helpers.d.ts delete mode 100644 dist/helpers.js delete mode 100644 dist/identifier.d.ts delete mode 100644 dist/identifier.js delete mode 100644 dist/index.d.ts delete mode 100644 dist/index.js delete mode 100644 dist/lsat.d.ts delete mode 100644 dist/lsat.js delete mode 100644 dist/satisfiers.d.ts delete mode 100644 dist/satisfiers.js delete mode 100644 dist/types/index.d.ts delete mode 100644 dist/types/index.js delete mode 100644 dist/types/lsat.d.ts delete mode 100644 dist/types/lsat.js delete mode 100644 dist/types/satisfier.d.ts delete mode 100644 dist/types/satisfier.js create mode 100644 src/macaroon.ts create mode 100644 tests/macaroon.spec.ts diff --git a/.travis.yml b/.travis.yml index b6e7112..065f498 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ language: node_js node_js: - - "10" + - "12" cache: yarn script: - yarn lint diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..24d72ec --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,10 @@ +{ + "html.format.unformatted": "", + "editor.defaultFormatter": "esbenp.prettier-vscode", + "[javascript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[typescript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + } +} diff --git a/README.md b/README.md index a964f14..6ddc25d 100644 --- a/README.md +++ b/README.md @@ -129,13 +129,13 @@ function. `satisfyFinal` will test only the last caveat on a macaroon of the mat and `satisfyPrevious` compares each caveat of the same condition against each other. This allows more flexible attenuation where you can ensure, for example, that every "new" caveat is not less restrictive than a previously added one. In the case of an expiration, you probably want to have a satisfier -that tests that a newer `expiration` is sooner than the first `expiration` added, otherwise, a client -could add their own expiration further into the future. +that tests that a newer `expiration` is sooner than the first `expiration` added, otherwise, a client could +add their own expiration further into the future. The exported `Satisfier` interface described in the docs provides more details on creating your own satisfiers -#### `verifyFirstPartyMacaroon` +#### `verifyMacaroonCaveats` This can only be run by the creator of the macaroon since the signing secret is required to verify the macaroon. This will run all necessary checks (requires satisfiers to be passed diff --git a/dist/caveat.d.ts b/dist/caveat.d.ts deleted file mode 100644 index ddd4267..0000000 --- a/dist/caveat.d.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { CaveatOptions, Satisfier } from './types'; -/** - * @description Creates a new error describing a problem with creating a new caveat - * @extends Error - */ -export declare class ErrInvalidCaveat extends Error { - constructor(...params: any[]); -} -/** - * @typedef {Object} Caveat - * @description A caveat is a class with a condition, value and a comparator. They - * are used in macaroons to evaluate the validity of a macaroon. The Caveat class - * provides a method for turning a string into a caveat object (decode) and a way to - * turn a caveat into a string that can be encoded into a macaroon. - */ -export declare class Caveat { - condition: string; - value: string | number; - comp: string; - /** - * Create a caveat - * @param {Object} options - options to create a caveat from - * @param {string} options.condition - condition that will be evaluated, e.g. "expiration", "ip", etc. - * @param {string} options.value - the value that the caveat should equal. When added to a macaroon this is what - * the request is evaluated against. - * @param {string} [comp="="] - one of "=", "<", ">" which describes how the value is compared. So "time<1576799124987" - * would mean we will evaluate a time that is less than "1576799124987" - */ - constructor(options: CaveatOptions); - fromOptions(options: CaveatOptions): this; - /** - * @returns {string} Caveat as string value. e.g. `expiration=1576799124987` - */ - encode(): string; - /** - * - * @param {string} c - create a new caveat from a string - * @returns {Caveat} - */ - static decode(c: string): Caveat; -} -/** - * @description hasCaveat will take a macaroon and a caveat and evaluate whether or not - * that caveat exists on the macaroon - * @param {string} rawMac - raw macaroon to determine caveats from - * @param {Caveat|string} c - Caveat to test against macaroon - * @returns {boolean} - */ -export declare function hasCaveat(rawMac: string, c: Caveat | string): string | boolean | ErrInvalidCaveat; -/** - * @description A function that verifies the caveats on a macaroon. - * The functionality mimics that of loop's lsat utilities. - * @param caveats a list of caveats to verify - * @param {Satisfier} satisfiers a single satisfier or list of satisfiers used to verify caveats - * @param {Object} [options] An optional options object that will be passed to the satisfiers. - * In many circumstances this will be a request object, for example when this is used in a server - * @returns {boolean} - */ -export declare function verifyCaveats(caveats: Caveat[], satisfiers: Satisfier | Satisfier[], options?: object): boolean; -/** - * @description verifyFirstPartyMacaroon will check if a macaroon is valid or - * not based on a set of satisfiers to pass as general caveat verifiers. This will also run - * against caveat.verityCaveats to ensure that satisfyPrevious will validate - * @param {string} macaroon A raw macaroon to run a verifier against - * @param {String} secret The secret key used to sign the macaroon - * @param {(Satisfier | Satisfier[])} satisfiers a single satisfier or list of satisfiers used to verify caveats - * @param {Object} [options] An optional options object that will be passed to the satisfiers. - * In many circumstances this will be a request object, for example when this is used in a server - * @returns {boolean} - */ -export declare function verifyFirstPartyMacaroon(rawMac: string, secret: string, satisfiers?: Satisfier | Satisfier[], options?: any): boolean; diff --git a/dist/caveat.js b/dist/caveat.js deleted file mode 100644 index 7249a04..0000000 --- a/dist/caveat.js +++ /dev/null @@ -1,261 +0,0 @@ -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); - __setModuleDefault(result, mod); - return result; -}; -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.verifyFirstPartyMacaroon = exports.verifyCaveats = exports.hasCaveat = exports.Caveat = exports.ErrInvalidCaveat = void 0; -/** - * @file Provides utilities for managing, analyzing, and validating caveats - * @author Buck Perley - */ -const bsert_1 = __importDefault(require("bsert")); -const Macaroon = __importStar(require("macaroon")); -let TextEncoder; -if (typeof window !== 'undefined' && window && window.TextEncoder) { - TextEncoder = window.TextEncoder; -} -else { - // No window.TextEncoder if it's node.js. - const util = require('util'); - TextEncoder = util.TextEncoder; -} -const utf8Encoder = new TextEncoder(); -const isValue = (x) => x !== undefined && x !== null; -const stringToBytes = (s) => isValue(s) ? utf8Encoder.encode(s) : s; -/** - * @description Creates a new error describing a problem with creating a new caveat - * @extends Error - */ -class ErrInvalidCaveat extends Error { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - constructor(...params) { - // Pass remaining arguments (including vendor specific ones) to parent constructor - super(...params); - // Maintains proper stack trace for where our error was thrown (only available on V8) - if (Error.captureStackTrace) { - Error.captureStackTrace(this, ErrInvalidCaveat); - } - this.name = 'ErrInvalidCaveat'; - // Custom debugging information - this.message = `Caveat must be of the form "condition[<,=,>]value"`; - } -} -exports.ErrInvalidCaveat = ErrInvalidCaveat; -const validComp = new Set(['<', '>', '=']); -/** - * @typedef {Object} Caveat - * @description A caveat is a class with a condition, value and a comparator. They - * are used in macaroons to evaluate the validity of a macaroon. The Caveat class - * provides a method for turning a string into a caveat object (decode) and a way to - * turn a caveat into a string that can be encoded into a macaroon. - */ -class Caveat { - /** - * Create a caveat - * @param {Object} options - options to create a caveat from - * @param {string} options.condition - condition that will be evaluated, e.g. "expiration", "ip", etc. - * @param {string} options.value - the value that the caveat should equal. When added to a macaroon this is what - * the request is evaluated against. - * @param {string} [comp="="] - one of "=", "<", ">" which describes how the value is compared. So "time<1576799124987" - * would mean we will evaluate a time that is less than "1576799124987" - */ - constructor(options) { - this.condition = ''; - this.value = ''; - this.comp = '='; - if (options) - this.fromOptions(options); - } - fromOptions(options) { - bsert_1.default(options, 'Data required to create new caveat'); - bsert_1.default(typeof options.condition === 'string' && options.condition.length, 'Require a condition'); - this.condition = options.condition; - bsert_1.default(options.value, 'Requires a value to create a caveat'); - options.value.toString(); - this.value = options.value; - if (options.comp) { - if (!validComp.has(options.comp)) - throw new ErrInvalidCaveat(); - this.comp = options.comp; - } - return this; - } - /** - * @returns {string} Caveat as string value. e.g. `expiration=1576799124987` - */ - encode() { - return `${this.condition}${this.comp}${this.value}`; - } - /** - * - * @param {string} c - create a new caveat from a string - * @returns {Caveat} - */ - static decode(c) { - let compIndex; - for (let i = 0; i < c.length; i++) { - if (validComp.has(c[i])) { - compIndex = i; - break; - } - } - if (!compIndex) - throw new ErrInvalidCaveat(); - const condition = c.slice(0, compIndex).trim(); - const comp = c[compIndex].trim(); - const value = c.slice(compIndex + 1).trim(); - return new this({ condition, comp, value }); - } -} -exports.Caveat = Caveat; -/** - * @description hasCaveat will take a macaroon and a caveat and evaluate whether or not - * that caveat exists on the macaroon - * @param {string} rawMac - raw macaroon to determine caveats from - * @param {Caveat|string} c - Caveat to test against macaroon - * @returns {boolean} - */ -function hasCaveat(rawMac, c) { - const macaroon = Macaroon.importMacaroon(rawMac)._exportAsJSONObjectV2(); - let caveat; - if (typeof c === 'string') - caveat = Caveat.decode(c); - else - caveat = c; - const condition = caveat.condition; - if (macaroon.c == undefined) { - return false; - } - let value; - macaroon.c.forEach((packet) => { - try { - if (packet.i != undefined) { - const test = Caveat.decode(packet.i); - if (condition === test.condition) - value = test.value; - } - } - catch (e) { - // ignore if caveat is unable to be decoded since we don't know it anyway - } - }); - if (value) - return value; - return false; -} -exports.hasCaveat = hasCaveat; -/** - * @description A function that verifies the caveats on a macaroon. - * The functionality mimics that of loop's lsat utilities. - * @param caveats a list of caveats to verify - * @param {Satisfier} satisfiers a single satisfier or list of satisfiers used to verify caveats - * @param {Object} [options] An optional options object that will be passed to the satisfiers. - * In many circumstances this will be a request object, for example when this is used in a server - * @returns {boolean} - */ -function verifyCaveats(caveats, satisfiers, options = {}) { - bsert_1.default(satisfiers, 'Must have satisfiers in order to verify caveats'); - // if there are no satisfiers then we can just assume everything is verified - if (!satisfiers) - return true; - else if (!Array.isArray(satisfiers)) - satisfiers = [satisfiers]; - // create map of satisfiers keyed by their conditions - const caveatSatisfiers = new Map(); - for (const satisfier of satisfiers) { - caveatSatisfiers.set(satisfier.condition, satisfier); - } - // create a map of relevant caveats to satisfiers keyed by condition - // with an array of caveats for each condition - const relevantCaveats = new Map(); - for (const caveat of caveats) { - // skip if condition is not in our satisfier map - const condition = caveat.condition; - if (!caveatSatisfiers.has(condition)) - continue; - if (!relevantCaveats.has(condition)) - relevantCaveats.set(condition, []); - const caveatArray = relevantCaveats.get(condition); - caveatArray.push(caveat); - relevantCaveats.set(condition, caveatArray); - } - // for each condition in the caveat map - for (const [condition, caveatsList] of relevantCaveats) { - // get the satisifer for that condition - const satisfier = caveatSatisfiers.get(condition); - // loop through the array of caveats - for (let i = 0; i < caveatsList.length - 1; i++) { - // confirm satisfyPrevious - const prevCaveat = caveatsList[i]; - const curCaveat = caveatsList[i + 1]; - if (!satisfier.satisfyPrevious(prevCaveat, curCaveat, options)) - return false; - } - // check satisfyFinal for the final caveat - if (!satisfier.satisfyFinal(caveatsList[caveatsList.length - 1], options)) - return false; - } - return true; -} -exports.verifyCaveats = verifyCaveats; -/** - * @description verifyFirstPartyMacaroon will check if a macaroon is valid or - * not based on a set of satisfiers to pass as general caveat verifiers. This will also run - * against caveat.verityCaveats to ensure that satisfyPrevious will validate - * @param {string} macaroon A raw macaroon to run a verifier against - * @param {String} secret The secret key used to sign the macaroon - * @param {(Satisfier | Satisfier[])} satisfiers a single satisfier or list of satisfiers used to verify caveats - * @param {Object} [options] An optional options object that will be passed to the satisfiers. - * In many circumstances this will be a request object, for example when this is used in a server - * @returns {boolean} - */ -function verifyFirstPartyMacaroon(rawMac, secret, satisfiers, -// eslint-disable-next-line @typescript-eslint/no-explicit-any -options = {}) { - // if given a raw macaroon string, convert to a Macaroon class - const macaroon = Macaroon.importMacaroon(rawMac); - const secretBytesArray = stringToBytes(secret); - const verify = function (rawCaveat) { - const caveat = Caveat.decode(rawCaveat); - if (satisfiers) { - if (!Array.isArray(satisfiers)) - satisfiers = [satisfiers]; - for (const satisfier of satisfiers) { - if (satisfier.condition !== caveat.condition) - return "not satisifed"; - const valid = satisfier.satisfyFinal(caveat, options); - if (valid) { - return null; - } - return "not satisfied"; - } - } - }; - try { - macaroon.verify(secretBytesArray, verify); - } - catch (e) { - return false; - } - return true; -} -exports.verifyFirstPartyMacaroon = verifyFirstPartyMacaroon; diff --git a/dist/helpers.d.ts b/dist/helpers.d.ts deleted file mode 100644 index a497cf0..0000000 --- a/dist/helpers.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * @description Given a string, determine if it is in hex encoding or not. - * @param {string} h - string to evaluate - */ -export declare function isHex(h: string): boolean; -export declare function decode(req: string): any; -export declare function getIdFromRequest(req: string): string; diff --git a/dist/helpers.js b/dist/helpers.js deleted file mode 100644 index 35fc637..0000000 --- a/dist/helpers.js +++ /dev/null @@ -1,36 +0,0 @@ -"use strict"; -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.getIdFromRequest = exports.decode = exports.isHex = void 0; -const bolt11_1 = __importDefault(require("bolt11")); -const bsert_1 = __importDefault(require("bsert")); -/** - * @description Given a string, determine if it is in hex encoding or not. - * @param {string} h - string to evaluate - */ -function isHex(h) { - return Buffer.from(h, 'hex').toString('hex') === h; -} -exports.isHex = isHex; -// A wrapper around bolt11's decode to handle -// simnet invoices -// eslint-disable-next-line @typescript-eslint/no-explicit-any -function decode(req) { - let network; - if (req.indexOf('lnsb') === 0) - network = { bech32: 'sb' }; - return bolt11_1.default.decode(req, network); -} -exports.decode = decode; -function getIdFromRequest(req) { - const request = decode(req); - const hashTag = request.tags.find((tag) => tag.tagName === 'payment_hash'); - bsert_1.default(hashTag && hashTag.data, 'Could not find payment hash on invoice request'); - const paymentHash = hashTag === null || hashTag === void 0 ? void 0 : hashTag.data.toString(); - if (!paymentHash || !paymentHash.length) - throw new Error('Could not get payment hash from payment request'); - return paymentHash; -} -exports.getIdFromRequest = getIdFromRequest; diff --git a/dist/identifier.d.ts b/dist/identifier.d.ts deleted file mode 100644 index 197fddb..0000000 --- a/dist/identifier.d.ts +++ /dev/null @@ -1,42 +0,0 @@ -declare const bufio: any; -import { IdentifierOptions } from './types'; -export declare const LATEST_VERSION = 0; -export declare const TOKEN_ID_SIZE = 32; -export declare class ErrUnknownVersion extends Error { - constructor(version: number | string, ...params: any[]); -} -/** - * @description An identifier encodes information about our LSAT that can be used as a unique identifier - * and is used to generate a macaroon. - * @extends Struct - */ -export declare class Identifier extends bufio.Struct { - /** - * - * @param {Object} options - options to create a new Identifier - * @param {number} version - version of the identifier used to determine encoding of the raw bytes - * @param {Buffer} paymentHash - paymentHash of the invoice associated with the LSAT. - * @param {Buffer} tokenId - random 32-byte id used to identify the LSAT by - */ - constructor(options: IdentifierOptions | void); - fromOptions(options: IdentifierOptions): this; - /** - * Convert identifier to string - * @returns {string} - */ - toString(): string; - static fromString(str: string): Identifier; - /** - * Utility for encoding the Identifier into a buffer based on version - * @param {bufio.BufferWriter} bw - Buffer writer for creating an Identifier Buffer - * @returns {Identifier} - */ - write(bw: any): this; - /** - * Utility for reading raw Identifier bytes and converting to a new Identifier - * @param {bufio.BufferReader} br - Buffer Reader to read bytes - * @returns {Identifier} - */ - read(br: any): this; -} -export {}; diff --git a/dist/identifier.js b/dist/identifier.js deleted file mode 100644 index 5e43a93..0000000 --- a/dist/identifier.js +++ /dev/null @@ -1,119 +0,0 @@ -"use strict"; -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.Identifier = exports.ErrUnknownVersion = exports.TOKEN_ID_SIZE = exports.LATEST_VERSION = void 0; -const assert = require('assert'); -const bufio = require('bufio'); -const crypto_1 = __importDefault(require("crypto")); -const v4_1 = __importDefault(require("uuid/v4")); -exports.LATEST_VERSION = 0; -exports.TOKEN_ID_SIZE = 32; -class ErrUnknownVersion extends Error { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - constructor(version, ...params) { - // Pass remaining arguments (including vendor specific ones) to parent constructor - super(...params); - // Maintains proper stack trace for where our error was thrown (only available on V8) - if (Error.captureStackTrace) { - Error.captureStackTrace(this, ErrUnknownVersion); - } - this.name = 'ErrUnknownVersion'; - // Custom debugging information - this.message = `${this.name}:${version}`; - } -} -exports.ErrUnknownVersion = ErrUnknownVersion; -/** - * @description An identifier encodes information about our LSAT that can be used as a unique identifier - * and is used to generate a macaroon. - * @extends Struct - */ -class Identifier extends bufio.Struct { - /** - * - * @param {Object} options - options to create a new Identifier - * @param {number} version - version of the identifier used to determine encoding of the raw bytes - * @param {Buffer} paymentHash - paymentHash of the invoice associated with the LSAT. - * @param {Buffer} tokenId - random 32-byte id used to identify the LSAT by - */ - constructor(options) { - super(options); - this.version = exports.LATEST_VERSION; - this.paymentHash = null; - this.tokenId = null; - if (options) - this.fromOptions(options); - } - fromOptions(options) { - if (options.version && options.version > exports.LATEST_VERSION) - throw new ErrUnknownVersion(options.version); - else if (options.version) - this.version = options.version; - assert(typeof this.version === 'number', 'Identifier version must be a number'); - assert(options.paymentHash.length === 32, `Expected 32-byte hash, instead got ${options.paymentHash.length}`); - this.paymentHash = options.paymentHash; - // TODO: generate random uuidv4 id (and hash to 32 to match length) - if (!options.tokenId) { - const id = v4_1.default(); - this.tokenId = crypto_1.default - .createHash('sha256') - .update(Buffer.from(id)) - .digest(); - } - else { - this.tokenId = options.tokenId; - } - assert(this.tokenId.length === exports.TOKEN_ID_SIZE, 'Token Id of unexpected size'); - return this; - } - /** - * Convert identifier to string - * @returns {string} - */ - toString() { - return this.toHex(); - } - static fromString(str) { - return new this().fromHex(str); - } - /** - * Utility for encoding the Identifier into a buffer based on version - * @param {bufio.BufferWriter} bw - Buffer writer for creating an Identifier Buffer - * @returns {Identifier} - */ - write(bw) { - bw.writeU16BE(this.version); - switch (this.version) { - case 0: - // write payment hash - bw.writeHash(this.paymentHash); - // check format of tokenId - assert(Buffer.isBuffer(this.tokenId) && - this.tokenId.length === exports.TOKEN_ID_SIZE, `Token ID must be ${exports.TOKEN_ID_SIZE}-byte hash`); - // write tokenId - bw.writeBytes(this.tokenId); - return this; - default: - throw new ErrUnknownVersion(this.version); - } - } - /** - * Utility for reading raw Identifier bytes and converting to a new Identifier - * @param {bufio.BufferReader} br - Buffer Reader to read bytes - * @returns {Identifier} - */ - read(br) { - this.version = br.readU16BE(); - switch (this.version) { - case 0: - this.paymentHash = br.readHash(); - this.tokenId = br.readBytes(exports.TOKEN_ID_SIZE); - return this; - default: - throw new ErrUnknownVersion(this.version); - } - } -} -exports.Identifier = Identifier; diff --git a/dist/index.d.ts b/dist/index.d.ts deleted file mode 100644 index 63bccb7..0000000 --- a/dist/index.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from './identifier'; -export * from './caveat'; -export * from './lsat'; -export * from './types'; -export { expirationSatisfier } from './satisfiers'; diff --git a/dist/index.js b/dist/index.js deleted file mode 100644 index 9ebf2dc..0000000 --- a/dist/index.js +++ /dev/null @@ -1,18 +0,0 @@ -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __exportStar = (this && this.__exportStar) || function(m, exports) { - for (var p in m) if (p !== "default" && !exports.hasOwnProperty(p)) __createBinding(exports, m, p); -}; -Object.defineProperty(exports, "__esModule", { value: true }); -__exportStar(require("./identifier"), exports); -__exportStar(require("./caveat"), exports); -__exportStar(require("./lsat"), exports); -__exportStar(require("./types"), exports); -var satisfiers_1 = require("./satisfiers"); -Object.defineProperty(exports, "expirationSatisfier", { enumerable: true, get: function () { return satisfiers_1.expirationSatisfier; } }); diff --git a/dist/lsat.d.ts b/dist/lsat.d.ts deleted file mode 100644 index 1a6717d..0000000 --- a/dist/lsat.d.ts +++ /dev/null @@ -1,126 +0,0 @@ -declare const bufio: any; -import * as Macaroon from 'macaroon'; -import { Caveat } from '.'; -import { LsatOptions } from './types'; -declare type LsatJson = { - validUntil: number; - isPending: boolean; - isSatisfied: boolean; - invoiceAmount: number; -} & LsatOptions; -/** Helpers */ -export declare function parseChallengePart(challenge: string): string; -/** - * @description A a class for creating and converting LSATs - */ -export declare class Lsat extends bufio.Struct { - id: string; - baseMacaroon: string; - paymentHash: string; - paymentPreimage: string | null; - validUntil: number; - timeCreated: number; - invoice: string; - amountPaid: number | null; - routingFeePaid: number | null; - invoiceAmount: number; - static type: string; - constructor(options: LsatOptions); - fromOptions(options: LsatOptions): this; - /** - * @description Determine if the LSAT is expired or not. This is based on the - * `validUntil` property of the lsat which is evaluated at creation time - * based on the macaroon and any existing expiration caveats - * @returns {boolean} - */ - isExpired(): boolean; - /** - * @description Determines if the lsat is pending based on if it has a preimage - * @returns {boolean} - */ - isPending(): boolean; - /** - * @description Determines if the lsat is valid based on a valid preimage or not - * @returns {boolean} - */ - isSatisfied(): boolean; - /** - * @description Gets the base macaroon from the lsat - * @returns {MacaroonInterface} - */ - getMacaroon(): Macaroon.MacaroonJSONV2; - /** - * @description A utility for returning the expiration date of the LSAT's macaroon based on - * an optional caveat - * @param {string} [macaroon] - raw macaroon to get expiration date from if exists as a caveat. If - * none is provided then it will use LSAT's base macaroon. Will throw if neither exists - * @returns {number} expiration date - */ - getExpirationFromMacaroon(macaroon?: string): number; - /** - * @description A utility for setting the preimage for an LSAT. This method will validate the preimage and throw - * if it is either of the incorrect length or does not match the paymentHash - * @param {string} preimage - 32-byte hex string of the preimage that is used as proof of payment of a lightning invoice - */ - setPreimage(preimage: string): void; - /** - * @description Add a first party caveat onto the lsat's base macaroon. - * This method does not validate the caveat being added. So, for example, a - * caveat that would fail validation on submission could still be added (e.g. an - * expiration that is less restrictive then a previous one). This should be done by - * the implementer - * @param {Caveat} caveat - caveat to add to the macaroon - * @returns {void} - */ - addFirstPartyCaveat(caveat: Caveat): void; - /** - * @description Get a list of caveats from the base macaroon - * @returns {Caveat[]} caveats - list of caveats - */ - getCaveats(): Caveat[]; - /** - * @description Converts the lsat into a valid LSAT token for use in an http - * Authorization header. This will return a string in the form: "LSAT [macaroon]:[preimage?]". - * If no preimage is available the last character should be a colon, which would be - * an "incomplete" LSAT - * @returns {string} - */ - toToken(): string; - /** - * @description Converts LSAT into a challenge header to return in the WWW-Authenticate response - * header. Returns base64 encoded string with macaroon and invoice information prefixed with - * authentication type ("LSAT") - * @returns {string} - */ - toChallenge(): string; - toJSON(): LsatJson; - addInvoice(invoice: string): void; - /** - * @description generates a new LSAT from an invoice and an optional invoice - * @param {string} macaroon - macaroon to parse and generate relevant lsat properties from - * @param {string} [invoice] - optional invoice which can provide other relevant information for the lsat - */ - static fromMacaroon(macaroon: string, invoice?: string): Lsat; - /** - * @description Create an LSAT from an http Authorization header. A useful utility - * when trying to parse an LSAT sent in a request and determining its validity - * @param {string} token - LSAT token sent in request - * @param {string} invoice - optional payment request information to intialize lsat with - * @returns {Lsat} - */ - static fromToken(token: string, invoice?: string): Lsat; - /** - * @description Validates and converts an LSAT challenge from a WWW-Authenticate header - * response into an LSAT object. This method expects an invoice and a macaroon in the challenge - * @param {string} challenge - * @returns {Lsat} - */ - static fromChallenge(challenge: string): Lsat; - /** - * @description Given an LSAT WWW-Authenticate challenge header (with token type, "LSAT", prefix) - * will return an Lsat. - * @param header - */ - static fromHeader(header: string): Lsat; -} -export {}; diff --git a/dist/lsat.js b/dist/lsat.js deleted file mode 100644 index 119be3f..0000000 --- a/dist/lsat.js +++ /dev/null @@ -1,381 +0,0 @@ -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); - __setModuleDefault(result, mod); - return result; -}; -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.Lsat = exports.parseChallengePart = void 0; -const assert = require('bsert'); -const bufio = require('bufio'); -const crypto_1 = __importDefault(require("crypto")); -const Macaroon = __importStar(require("macaroon")); -const _1 = require("."); -const helpers_1 = require("./helpers"); -/** Helpers */ -function parseChallengePart(challenge) { - let macaroon; - const separatorIndex = challenge.indexOf('='); - assert(separatorIndex > -1, 'Incorrectly encoded macaroon challenge. Missing "=" separator.'); - // slice off `[challengeType]=` - const splitIndex = challenge.length - 1 - separatorIndex; - macaroon = challenge.slice(-splitIndex); - assert(macaroon.length, 'Incorrectly encoded macaroon challenge'); - assert(macaroon[0] === '"' && macaroon[macaroon.length - 1] === '"', 'Incorecctly encoded macaroon challenge, must be enclosed in double quotes.'); - macaroon = macaroon.slice(1, macaroon.length - 1); - return macaroon; -} -exports.parseChallengePart = parseChallengePart; -/** - * @description A a class for creating and converting LSATs - */ -class Lsat extends bufio.Struct { - constructor(options) { - super(options); - this.id = ''; - this.validUntil = 0; - this.invoice = ''; - this.baseMacaroon = ''; - this.paymentHash = Buffer.alloc(32).toString('hex'); - this.timeCreated = Date.now(); - this.paymentPreimage = null; - this.amountPaid = 0; - this.routingFeePaid = 0; - this.invoiceAmount = 0; - if (options) - this.fromOptions(options); - } - fromOptions(options) { - assert(typeof options.baseMacaroon === 'string', 'Require serialized macaroon'); - this.baseMacaroon = options.baseMacaroon; - assert(typeof options.id === 'string', 'Require string id'); - this.id = options.id; - assert(typeof options.paymentHash === 'string', 'Require paymentHash'); - this.paymentHash = options.paymentHash; - const expiration = this.getExpirationFromMacaroon(options.baseMacaroon); - if (expiration) - this.validUntil = expiration; - if (options.invoice) { - this.addInvoice(options.invoice); - } - if (options.timeCreated) - this.timeCreated = options.timeCreated; - if (options.paymentPreimage) - this.paymentPreimage = options.paymentPreimage; - if (options.amountPaid) - this.amountPaid = options.amountPaid; - if (options.routingFeePaid) - this.routingFeePaid = options.routingFeePaid; - return this; - } - /** - * @description Determine if the LSAT is expired or not. This is based on the - * `validUntil` property of the lsat which is evaluated at creation time - * based on the macaroon and any existing expiration caveats - * @returns {boolean} - */ - isExpired() { - if (this.validUntil === 0) - return false; - return this.validUntil < Date.now(); - } - /** - * @description Determines if the lsat is pending based on if it has a preimage - * @returns {boolean} - */ - isPending() { - return this.paymentPreimage ? false : true; - } - /** - * @description Determines if the lsat is valid based on a valid preimage or not - * @returns {boolean} - */ - isSatisfied() { - if (!this.paymentHash) - return false; - if (!this.paymentPreimage) - return false; - const hash = crypto_1.default - .createHash('sha256') - .update(Buffer.from(this.paymentPreimage, 'hex')) - .digest('hex'); - if (hash !== this.paymentHash) - return false; - return true; - } - /** - * @description Gets the base macaroon from the lsat - * @returns {MacaroonInterface} - */ - getMacaroon() { - return Macaroon.importMacaroon(this.baseMacaroon)._exportAsJSONObjectV2(); - } - /** - * @description A utility for returning the expiration date of the LSAT's macaroon based on - * an optional caveat - * @param {string} [macaroon] - raw macaroon to get expiration date from if exists as a caveat. If - * none is provided then it will use LSAT's base macaroon. Will throw if neither exists - * @returns {number} expiration date - */ - getExpirationFromMacaroon(macaroon) { - if (!macaroon) - macaroon = this.baseMacaroon; - assert(macaroon, 'Missing macaroon'); - const caveatPackets = Macaroon.importMacaroon(macaroon)._exportAsJSONObjectV2().c; - const expirationCaveats = []; - if (caveatPackets == undefined) { - return 0; - } - for (const cav of caveatPackets) { - if (cav.i == undefined) { - continue; - } - const caveat = _1.Caveat.decode(cav.i); - if (caveat.condition === 'expiration') - expirationCaveats.push(caveat); - } - // return zero if no expiration caveat - if (!expirationCaveats.length) - return 0; - // want to return the last expiration caveat - return Number(expirationCaveats[expirationCaveats.length - 1].value); - } - /** - * @description A utility for setting the preimage for an LSAT. This method will validate the preimage and throw - * if it is either of the incorrect length or does not match the paymentHash - * @param {string} preimage - 32-byte hex string of the preimage that is used as proof of payment of a lightning invoice - */ - setPreimage(preimage) { - assert(helpers_1.isHex(preimage) && preimage.length === 64, 'Must pass valid 32-byte hash for lsat secret'); - const hash = crypto_1.default - .createHash('sha256') - .update(Buffer.from(preimage, 'hex')) - .digest('hex'); - assert(hash === this.paymentHash, "Hash of preimage did not match LSAT's paymentHash"); - this.paymentPreimage = preimage; - } - /** - * @description Add a first party caveat onto the lsat's base macaroon. - * This method does not validate the caveat being added. So, for example, a - * caveat that would fail validation on submission could still be added (e.g. an - * expiration that is less restrictive then a previous one). This should be done by - * the implementer - * @param {Caveat} caveat - caveat to add to the macaroon - * @returns {void} - */ - addFirstPartyCaveat(caveat) { - assert(caveat instanceof _1.Caveat, 'Require a Caveat object to add to macaroon'); - const mac = Macaroon.importMacaroon(this.baseMacaroon); - mac.addFirstPartyCaveat(caveat.encode()); - this.baseMacaroon = Macaroon.bytesToBase64(mac._exportBinaryV2()); - } - /** - * @description Get a list of caveats from the base macaroon - * @returns {Caveat[]} caveats - list of caveats - */ - getCaveats() { - const caveats = []; - const caveatPackets = this.getMacaroon().c; - if (caveatPackets == undefined) { - return caveats; - } - for (const cav of caveatPackets) { - if (cav.i == undefined) { - continue; - } - caveats.push(_1.Caveat.decode(cav.i)); - } - return caveats; - } - /** - * @description Converts the lsat into a valid LSAT token for use in an http - * Authorization header. This will return a string in the form: "LSAT [macaroon]:[preimage?]". - * If no preimage is available the last character should be a colon, which would be - * an "incomplete" LSAT - * @returns {string} - */ - toToken() { - return `LSAT ${this.baseMacaroon}:${this.paymentPreimage || ''}`; - } - /** - * @description Converts LSAT into a challenge header to return in the WWW-Authenticate response - * header. Returns base64 encoded string with macaroon and invoice information prefixed with - * authentication type ("LSAT") - * @returns {string} - */ - toChallenge() { - assert(this.invoice, `Can't create a challenge without a payment request/invoice`); - const challenge = `macaroon="${this.baseMacaroon}", invoice="${this.invoice}"`; - return `LSAT ${challenge}`; - } - toJSON() { - return { - id: this.id, - validUntil: this.validUntil, - invoice: this.invoice, - baseMacaroon: this.baseMacaroon, - paymentHash: this.paymentHash, - timeCreated: this.timeCreated, - paymentPreimage: this.paymentPreimage || undefined, - amountPaid: this.amountPaid || undefined, - invoiceAmount: this.invoiceAmount, - routingFeePaid: this.routingFeePaid || undefined, - isPending: this.isPending(), - isSatisfied: this.isSatisfied() - }; - } - addInvoice(invoice) { - assert(this.paymentHash, 'Cannot add invoice data to an LSAT without paymentHash'); - try { - const data = helpers_1.decode(invoice); - const { satoshis: tokens } = data; - const hashTag = data.tags.find((tag) => tag.tagName === 'payment_hash'); - assert(hashTag, 'Could not find payment hash on invoice request'); - const paymentHash = hashTag === null || hashTag === void 0 ? void 0 : hashTag.data; - assert(paymentHash === this.paymentHash, 'paymentHash from invoice did not match LSAT'); - this.invoiceAmount = tokens || 0; - this.invoice = invoice; - } - catch (e) { - throw new Error(`Problem adding invoice data to LSAT: ${e.message}`); - } - } - // Static API - /** - * @description generates a new LSAT from an invoice and an optional invoice - * @param {string} macaroon - macaroon to parse and generate relevant lsat properties from - * @param {string} [invoice] - optional invoice which can provide other relevant information for the lsat - */ - static fromMacaroon(macaroon, invoice) { - assert(typeof macaroon === 'string', 'Requires a raw macaroon string for macaroon to generate LSAT'); - const identifier = Macaroon.importMacaroon(macaroon)._exportAsJSONObjectV2().i; - let id; - try { - if (identifier == undefined) { - throw new Error(`macaroon identifier undefined`); - } - id = _1.Identifier.fromString(identifier); - } - catch (e) { - throw new Error(`Unexpected encoding for macaroon identifier: ${e.message}`); - } - const options = { - id: identifier, - baseMacaroon: macaroon, - paymentHash: id.paymentHash.toString('hex'), - }; - const lsat = new this(options); - if (invoice) { - lsat.addInvoice(invoice); - } - return lsat; - } - /** - * @description Create an LSAT from an http Authorization header. A useful utility - * when trying to parse an LSAT sent in a request and determining its validity - * @param {string} token - LSAT token sent in request - * @param {string} invoice - optional payment request information to intialize lsat with - * @returns {Lsat} - */ - static fromToken(token, invoice) { - assert(token.includes(this.type), 'Token must include LSAT prefix'); - token = token.slice(this.type.length).trim(); - const [macaroon, preimage] = token.split(':'); - const lsat = Lsat.fromMacaroon(macaroon, invoice); - if (preimage) - lsat.setPreimage(preimage); - return lsat; - } - /** - * @description Validates and converts an LSAT challenge from a WWW-Authenticate header - * response into an LSAT object. This method expects an invoice and a macaroon in the challenge - * @param {string} challenge - * @returns {Lsat} - */ - static fromChallenge(challenge) { - const macChallenge = 'macaroon='; - const invoiceChallenge = 'invoice='; - let challenges; - challenges = challenge.split(','); - // add support for challenges that are separated with just a space - if (challenges.length < 2) - challenges = challenge.split(' '); - // if we still don't have at least two, then there was a malformed header/challenge - assert(challenges.length >= 2, 'Expected at least two challenges in the LSAT: invoice and macaroon'); - let macaroon = '', invoice = ''; - // get the indexes of the challenge strings so that we can split them - // kind of convoluted but it takes into account challenges being in the wrong order - // and for excess challenges that we can ignore - for (const c of challenges) { - // check if we're looking at the macaroon challenge - if (!macaroon.length && c.indexOf(macChallenge) > -1) { - try { - macaroon = parseChallengePart(c); - } - catch (e) { - throw new Error(`Problem parsing macaroon challenge: ${e.message}`); - } - } - // check if we're looking at the invoice challenge - if (!invoice.length && c.indexOf(invoiceChallenge) > -1) { - try { - invoice = parseChallengePart(c); - } - catch (e) { - throw new Error(`Problem parsing macaroon challenge: ${e.message}`); - } - } - // if there are other challenges but we have mac and invoice then we can break - // as they are not LSAT relevant anyway - if (invoice.length && macaroon.length) - break; - } - assert(invoice.length && macaroon.length, 'Expected WWW-Authenticate challenge with macaroon and invoice data'); - const paymentHash = helpers_1.getIdFromRequest(invoice); - let identifier; - const mac = Macaroon.importMacaroon(macaroon); - identifier = mac._exportAsJSONObjectV2().i; - if (identifier == undefined) { - identifier = mac._exportAsJSONObjectV2().i64; - if (identifier == undefined) { - throw new Error(`Problem parsing macaroon identifier`); - } - } - return new this({ - id: identifier, - baseMacaroon: macaroon, - paymentHash, - invoice: invoice, - }); - } - /** - * @description Given an LSAT WWW-Authenticate challenge header (with token type, "LSAT", prefix) - * will return an Lsat. - * @param header - */ - static fromHeader(header) { - // remove the token type prefix to get the challenge - const challenge = header.slice(this.type.length).trim(); - assert(header.length !== challenge.length, 'header missing token type prefix "LSAT"'); - return Lsat.fromChallenge(challenge); - } -} -exports.Lsat = Lsat; -Lsat.type = 'LSAT'; diff --git a/dist/satisfiers.d.ts b/dist/satisfiers.d.ts deleted file mode 100644 index f77d353..0000000 --- a/dist/satisfiers.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * @file Useful satisfiers that are independent of environment, for example, - * ones that don't require the request object in a server as these can be used anywhere. - */ -import { Satisfier } from '.'; -/** - * @description A satisfier for validating expiration caveats on macaroon. Used in the exported - * boltwallConfig TIME_CAVEAT_CONFIGS - * @type Satisfier - */ -export declare const expirationSatisfier: Satisfier; diff --git a/dist/satisfiers.js b/dist/satisfiers.js deleted file mode 100644 index 3e9f782..0000000 --- a/dist/satisfiers.js +++ /dev/null @@ -1,33 +0,0 @@ -"use strict"; -/** - * @file Useful satisfiers that are independent of environment, for example, - * ones that don't require the request object in a server as these can be used anywhere. - */ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.expirationSatisfier = void 0; -/** - * @description A satisfier for validating expiration caveats on macaroon. Used in the exported - * boltwallConfig TIME_CAVEAT_CONFIGS - * @type Satisfier - */ -exports.expirationSatisfier = { - condition: 'expiration', - satisfyPrevious: (prev, curr) => { - if (prev.condition !== 'expiration' || curr.condition !== 'expiration') - return false; - // fails if current expiration is later than previous - // (i.e. newer caveats should be more restrictive) - else if (prev.value < curr.value) - return false; - else - return true; - }, - satisfyFinal: (caveat) => { - if (caveat.condition !== 'expiration') - return false; - // if the expiration value is less than current time than satisfier is failed - if (caveat.value < Date.now()) - return false; - return true; - }, -}; diff --git a/dist/types/index.d.ts b/dist/types/index.d.ts deleted file mode 100644 index d980500..0000000 --- a/dist/types/index.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './lsat'; -export * from './satisfier'; diff --git a/dist/types/index.js b/dist/types/index.js deleted file mode 100644 index 82680d7..0000000 --- a/dist/types/index.js +++ /dev/null @@ -1,14 +0,0 @@ -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __exportStar = (this && this.__exportStar) || function(m, exports) { - for (var p in m) if (p !== "default" && !exports.hasOwnProperty(p)) __createBinding(exports, m, p); -}; -Object.defineProperty(exports, "__esModule", { value: true }); -__exportStar(require("./lsat"), exports); -__exportStar(require("./satisfier"), exports); diff --git a/dist/types/lsat.d.ts b/dist/types/lsat.d.ts deleted file mode 100644 index 3b4659e..0000000 --- a/dist/types/lsat.d.ts +++ /dev/null @@ -1,41 +0,0 @@ -/// -/** - * @typedef {Object} IdentifierOptions - * @property {number} version - version of the Identifier. Used for serialization - * @property {Buffer} paymentHash - payment hash of invoice associated with LSAT - * @property {Buffer} tokenId - unique identifier for the LSAT - * Describes the shape of the options for creating a new identifier struct - * which represents the constant, unique identifiers associated with a macaroon - */ -export interface IdentifierOptions { - version?: number; - paymentHash: Buffer; - tokenId?: Buffer; -} -/** - * @typedef {Object} CaveatOptions - * @property {string} condition - the key used to identify the caveat - * @property {string|number} value - value for the caveat to be compared against - * @property {string} comp - a comparator string for how the value should be evaluated - * Describes options to create a caveat. The condition is like the variable - * and the value is what it is expected to be. Encoded format would be "condition=value" - */ -export interface CaveatOptions { - condition: string; - value: string | number; - comp?: string; -} -/** - * @typedef LsatOptions - * Describes options to create an LSAT token. - */ -export interface LsatOptions { - id: string; - baseMacaroon: string; - paymentHash: string; - invoice?: string; - timeCreated?: number; - paymentPreimage?: string; - amountPaid?: number; - routingFeePaid?: number; -} diff --git a/dist/types/lsat.js b/dist/types/lsat.js deleted file mode 100644 index c8ad2e5..0000000 --- a/dist/types/lsat.js +++ /dev/null @@ -1,2 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/dist/types/satisfier.d.ts b/dist/types/satisfier.d.ts deleted file mode 100644 index 5d9ecd1..0000000 --- a/dist/types/satisfier.d.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Caveat } from '../caveat'; -/** - * @typedef {function (prev: Caveat, curr: Caveat, options: object): boolean} SatisfyPrevious - * @description A satisfier function for comparing two caveats to ensure increasing restrictiveness. - * @param {Caveat} prev - preceding caveat - * @param {Caveat} curr - current caveat - * @param {Object} options - optional object to be used to make evaluation, e.g. a request object in a server - * @returns {boolean} - */ -export declare type SatisfyPrevious = (prev: Caveat, curr: Caveat, options?: any) => boolean; -/** - * @typedef {function (caveat: Caveat, options: object): boolean} SatisfyFinal - * @description A satisfier function to evaluate if a caveat has been satisfied - * @param {Caveat} caveat - caveat to evaluate - * @param {Object} options - optional object to be used to make evaluation, e.g. a request object in a server - * @returns boolean - */ -export declare type SatisfyFinal = (caveat: Caveat, options?: any) => boolean; -/** - * @typedef {Object} Satisfier - * @description Satisfier provides a generic interface to satisfy a caveat based on its - * condition. - * @property {string} condition - used to identify the caveat to check against - * @property {SatisfyPrevious} satisfyPrevious - * @property {SatisfyFinal} satisfyFinal - */ -export interface Satisfier { - condition: string; - satisfyPrevious?: SatisfyPrevious; - satisfyFinal: SatisfyFinal; -} diff --git a/dist/types/satisfier.js b/dist/types/satisfier.js deleted file mode 100644 index c8ad2e5..0000000 --- a/dist/types/satisfier.js +++ /dev/null @@ -1,2 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/package.json b/package.json index e3e3ba4..6c9deed 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "test": "nyc ts-mocha -p tsconfig.json tests/**/*.spec.ts", "test:watch": "ts-mocha -p tsconfig.json --reporter spec --watch --watch-extensions ts tests/**/*.spec.ts", "docs": "./docs.sh", + "prepare": "npm run clean && npm run build", "prepublishOnly": "npm run clean && npm run build && npm run docs", "postversion": "git push && git push --tags" }, diff --git a/src/caveat.ts b/src/caveat.ts index b222ed3..4dcb74b 100644 --- a/src/caveat.ts +++ b/src/caveat.ts @@ -4,9 +4,9 @@ */ import assert from 'bsert' import { CaveatOptions, Satisfier } from './types' -import { stringToBytes} from "./helpers"; + import * as Macaroon from 'macaroon' -import {MacaroonJSONV2} from "macaroon/src/macaroon"; +import { MacaroonJSONV2 } from 'macaroon/src/macaroon' /** * @description Creates a new error describing a problem with creating a new caveat @@ -128,7 +128,7 @@ export function hasCaveat( else caveat = c const condition = caveat.condition - if (macaroon.c == undefined){ + if (macaroon.c == undefined) { return false } let value @@ -157,11 +157,9 @@ export function hasCaveat( */ export function verifyCaveats( caveats: Caveat[], - satisfiers: Satisfier | Satisfier[], + satisfiers?: Satisfier | Satisfier[], options: object = {} ): boolean { - assert(satisfiers, 'Must have satisfiers in order to verify caveats') - // if there are no satisfiers then we can just assume everything is verified if (!satisfiers) return true else if (!Array.isArray(satisfiers)) satisfiers = [satisfiers] @@ -208,49 +206,3 @@ export function verifyCaveats( } return true } - -/** - * @description verifyFirstPartyMacaroon will check if a macaroon is valid or - * not based on a set of satisfiers to pass as general caveat verifiers. This will also run - * against caveat.verityCaveats to ensure that satisfyPrevious will validate - * @param {string} macaroon A raw macaroon to run a verifier against - * @param {String} secret The secret key used to sign the macaroon - * @param {(Satisfier | Satisfier[])} satisfiers a single satisfier or list of satisfiers used to verify caveats - * @param {Object} [options] An optional options object that will be passed to the satisfiers. - * In many circumstances this will be a request object, for example when this is used in a server - * @returns {boolean} - */ -export function verifyFirstPartyMacaroon( - rawMac: string, - secret: string, - satisfiers?: Satisfier | Satisfier[], - // eslint-disable-next-line @typescript-eslint/no-explicit-any - options: any = {} -): boolean { - // if given a raw macaroon string, convert to a Macaroon class - const macaroon = Macaroon.importMacaroon(rawMac) - const secretBytesArray = stringToBytes(secret) - - - const verify = function (rawCaveat: string) { - const caveat = Caveat.decode(rawCaveat) - if (satisfiers) { - if (!Array.isArray(satisfiers)) satisfiers = [satisfiers] - for (const satisfier of satisfiers) { - if (satisfier.condition !== caveat.condition) return "not satisifed" - const valid = satisfier.satisfyFinal(caveat, options) - if (valid) { - return null - } - return "not satisfied" - } - } - } - try { - macaroon.verify(secretBytesArray, verify) - } catch (e) { - return false - } - return true -} - diff --git a/src/helpers.ts b/src/helpers.ts index 751fec2..f7dc984 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -1,5 +1,7 @@ import bolt11 from 'bolt11' import assert from 'bsert' +import { MacaroonClass } from './types'; +import * as Macaroon from 'macaroon' let TextEncoder if (typeof window !== 'undefined' && window && window.TextEncoder) { @@ -11,8 +13,8 @@ if (typeof window !== 'undefined' && window && window.TextEncoder) { } export const utf8Encoder = new TextEncoder(); -export const isValue = (x: string | null | undefined) => x !== undefined && x !== null; -export const stringToBytes = (s: string | null | undefined) => isValue(s) ? utf8Encoder.encode(s) : s; +export const isValue = (x: string | null | undefined): boolean => x !== undefined && x !== null; +export const stringToBytes = (s: string | null | undefined): Uint8Array => isValue(s) ? utf8Encoder.encode(s) : s; /** diff --git a/src/index.ts b/src/index.ts index 70bda99..44b4bc0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,3 +3,4 @@ export * from './caveat' export * from './lsat' export * from './types' export { expirationSatisfier } from './satisfiers' +export * from './macaroon' diff --git a/src/macaroon.ts b/src/macaroon.ts new file mode 100644 index 0000000..4bbb5c8 --- /dev/null +++ b/src/macaroon.ts @@ -0,0 +1,68 @@ +import { Caveat, verifyCaveats } from "./caveat"; +import { stringToBytes } from './helpers' +import * as Macaroon from 'macaroon' +import { MacaroonClass, Satisfier } from "./types"; + +/** + * @description utility function to get an array of caveat instances from + * a raw macaroon. + * @param {string} macaroon - raw macaroon to retrieve caveats from + * @returns {Caveat[]} array of caveats on the macaroon + */ +export function getCaveatsFromMacaroon(rawMac: string): Caveat[] { + const macaroon = Macaroon.importMacaroon(rawMac) + const caveats = [] + const rawCaveats = macaroon._exportAsJSONObjectV2()?.c + + if (rawCaveats) { + for (const c of rawCaveats) { + if (!c.i) continue; + const caveat = Caveat.decode(c.i) + caveats.push(caveat) + } + } + return caveats +} + +/** + * @description verifyMacaroonCaveats will check if a macaroon is valid or + * not based on a set of satisfiers to pass as general caveat verifiers. This will also run + * against caveat.verifyCaveats to ensure that satisfyPrevious will validate + * @param {string} macaroon A raw macaroon to run a verifier against + * @param {String} secret The secret key used to sign the macaroon + * @param {(Satisfier | Satisfier[])} satisfiers a single satisfier or list of satisfiers used to verify caveats + * @param {Object} [options] An optional options object that will be passed to the satisfiers. + * In many circumstances this will be a request object, for example when this is used in a server + * @returns {boolean} + */ +export function verifyMacaroonCaveats( + rawMac: string, + secret: string, + satisfiers?: Satisfier | Satisfier[], + // eslint-disable-next-line @typescript-eslint/no-explicit-any + options: any = {} +): boolean { + try { + const macaroon = Macaroon.importMacaroon(rawMac) + const secretBytesArray = stringToBytes(secret) + + // js-macaroon's verification takes a function as its second + // arg that runs a check against each caveat which is a less full-featured + // version of `verifyCaveats` used below since it doesn't support contextual + // checks like comparing w/ previous caveats for the same condition. + // we pass this stubbed function so signature checks can be done + // and satisfier checks will be done next if this passes. + macaroon.verify(secretBytesArray, () => null) + + const caveats = getCaveatsFromMacaroon(rawMac) + if (!caveats.length) return true; + // check caveats against satisfiers, including previous caveat checks + return verifyCaveats(caveats, satisfiers, options) + } catch (e) { + return false + } +} + +export function getRawMacaroon(mac: MacaroonClass): string { + return Macaroon.bytesToBase64(mac._exportBinaryV2()) +} diff --git a/src/types/index.ts b/src/types/index.ts index caf48d8..dccd783 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,2 +1,17 @@ +import { MacaroonJSONV2 } from 'macaroon' + export * from './lsat' export * from './satisfier' + +// js-macaroon doesn't export a type for its base class +// this throws off some of the ts linting when it wants a return type +/** + * @typedef {Object} MacaroonClass + */ +export interface MacaroonClass { + _exportAsJSONObjectV2(): MacaroonJSONV2 + addFirstPartyCaveat(caveatIdBytes: Uint8Array | string): void + _exportBinaryV2(): Uint8Array +} + +// could maybe do the above as -> typeof Macaroon.newMacaroon({...}) diff --git a/tests/caveat.spec.ts b/tests/caveat.spec.ts index 5a834f4..c630277 100644 --- a/tests/caveat.spec.ts +++ b/tests/caveat.spec.ts @@ -1,6 +1,7 @@ import { expect } from 'chai' import * as Macaroon from 'macaroon' import { Caveat, ErrInvalidCaveat, hasCaveat, verifyCaveats } from '../src' + import { Satisfier } from '../src/types' describe('Caveats', () => { @@ -55,8 +56,8 @@ describe('Caveats', () => { version: 1, rootKey: 'secret', identifier: 'pubId', - location: 'location' - }); + location: 'location', + }) macaroon.addFirstPartyCaveat(caveat.encode()) const macBin = macaroon._exportBinaryV2() @@ -92,8 +93,8 @@ describe('Caveats', () => { version: 1, rootKey: 'secret', identifier: 'pubId', - location: 'location' - }); + location: 'location', + }) const macBin3 = macaroon._exportBinaryV2() if (macBin3 == null) { @@ -112,8 +113,8 @@ describe('Caveats', () => { let caveat1: Caveat, caveat2: Caveat, caveat3: Caveat, - caveats: Caveat[], - satisfier: Satisfier + satisfier: Satisfier, + caveats: Caveat[] beforeEach(() => { caveat1 = new Caveat({ condition: '1', value: 'test' }) @@ -130,10 +131,8 @@ describe('Caveats', () => { satisfyFinal: (): boolean => true, } }) - it('should verify caveats given a set of satisfiers', () => { - const validatesCaveats = (): boolean | Error => - verifyCaveats(caveats, satisfier) + const validatesCaveats = (): boolean => verifyCaveats(caveats, satisfier) expect(validatesCaveats).to.not.throw() expect(validatesCaveats()).to.be.true @@ -163,29 +162,42 @@ describe('Caveats', () => { }) it('should be able to use an options object for verification', () => { - const testCaveat = new Caveat({condition: 'middlename', value: 'danger'}) + const testCaveat = new Caveat({ + condition: 'middlename', + value: 'danger', + }) caveats.push(testCaveat) satisfier = { condition: testCaveat.condition, // dummy satisfyPrevious function to test that it tests caveat lists correctly satisfyPrevious: (prev, cur, options): boolean => prev.value.toString().includes('test') && - cur.value.toString().includes('test') && options.body.middlename === testCaveat.value, + cur.value.toString().includes('test') && + options.body.middlename === testCaveat.value, satisfyFinal: (caveat, options): boolean => { - if (caveat.condition === testCaveat.condition && options?.body.middlename === testCaveat.value) + if ( + caveat.condition === testCaveat.condition && + options.body.middlename === testCaveat.value + ) return true - + return false }, } - let isValid = verifyCaveats(caveats, satisfier, {body: { middlename: 'bob' }}) - - expect(isValid, 'should fail when given an invalid options object').to.be.false - - isValid = verifyCaveats(caveats, satisfier, {body: { middlename: testCaveat.value }}) + let isValid = verifyCaveats(caveats, satisfier, { + body: { middlename: 'bob' }, + }) + + expect(isValid, 'should fail when given an invalid options object').to.be + .false + + isValid = verifyCaveats(caveats, satisfier, { + body: { middlename: testCaveat.value }, + }) - expect(isValid, 'should pass when given a valid options object').to.be.true + expect(isValid, 'should pass when given a valid options object').to.be + .true }) }) }) diff --git a/tests/macaroon.spec.ts b/tests/macaroon.spec.ts new file mode 100644 index 0000000..6c6808f --- /dev/null +++ b/tests/macaroon.spec.ts @@ -0,0 +1,95 @@ +import { expect } from 'chai' +import * as Macaroon from 'macaroon' +import * as caveat from '../src/caveat' +import { + Caveat, + getCaveatsFromMacaroon, + getRawMacaroon, + MacaroonClass, + Satisfier, + verifyMacaroonCaveats, +} from '../src' +import sinon from 'sinon' + +describe('macaroon', () => { + let caveat1: Caveat, + caveat2: Caveat, + caveat3: Caveat, + satisfier: Satisfier, + mac: MacaroonClass, + secret: string + + beforeEach(() => { + caveat1 = new Caveat({ condition: '1', value: 'test' }) + caveat2 = new Caveat({ condition: '1', value: 'test2' }) + caveat3 = new Caveat({ condition: '3', value: 'foobar' }) + + satisfier = { + condition: caveat1.condition, + // dummy satisfyPrevious function to test that it tests caveat lists correctly + satisfyPrevious: (prev, cur): boolean => + prev.value.toString().includes('test') && + cur.value.toString().includes('test'), + satisfyFinal: (): boolean => true, + } + secret = 'secret' + mac = Macaroon.newMacaroon({ + version: 1, + rootKey: secret, + identifier: 'pubId', + location: 'location', + }) + }) + + describe('getCaveatsFromMacaroon', () => { + it('should correctly return all caveats from raw macaroon', () => { + const testCaveats = [caveat1, caveat2, caveat3] + testCaveats.forEach(c => mac.addFirstPartyCaveat(c.encode())) + const raw = getRawMacaroon(mac) + const caveats = getCaveatsFromMacaroon(raw) + expect(caveats).to.have.lengthOf(testCaveats.length) + }) + + it('should return empty array if no caveats', () => { + const raw = getRawMacaroon(mac) + const caveats = getCaveatsFromMacaroon(raw) + expect(caveats).to.have.lengthOf(0) + }) + }) + + describe('verifyMacaroonCaveats', () => { + it('should verify the signature on a macaroon w/ no caveats', () => { + const isValid = verifyMacaroonCaveats( + getRawMacaroon(mac), + secret, + satisfier + ) + expect(isValid).to.be.true + }) + + it('should run verifyCaveats with all caveats and satisfiers', () => { + const testCaveats = [caveat1, caveat2, caveat3] + testCaveats.forEach(c => mac.addFirstPartyCaveat(c.encode())) + const spy = sinon.spy(caveat, 'verifyCaveats') + const isValid = verifyMacaroonCaveats( + getRawMacaroon(mac), + secret, + satisfier + ) + expect(isValid).to.be.true + expect(spy.calledWithMatch(testCaveats, satisfier)).to.be.true + }) + + it('should return false if caveats dont verify', () => { + satisfier.satisfyFinal = (): boolean => false + mac.addFirstPartyCaveat(caveat1.encode()) + mac.addFirstPartyCaveat(caveat2.encode()) + const isValid = verifyMacaroonCaveats( + getRawMacaroon(mac), + secret, + satisfier + ) + expect(isValid).to.be.false + }) + }) +}) diff --git a/tests/utilities.ts b/tests/utilities.ts index 09abaff..60f239a 100644 --- a/tests/utilities.ts +++ b/tests/utilities.ts @@ -3,7 +3,7 @@ import { randomBytes } from 'crypto' import { invoice } from './data' import { Identifier } from '../src' import { getIdFromRequest } from '../src/helpers' -import * as Macaroon from "macaroon"; +import * as Macaroon from 'macaroon' export function getTestBuilder(secret: string) { const paymentHash = getIdFromRequest(invoice.payreq) @@ -16,7 +16,7 @@ export function getTestBuilder(secret: string) { version: 1, rootKey: secret, identifier: identifier.toString(), - location: 'location' - }); + location: 'location', + }) return macaroon } diff --git a/yarn.lock b/yarn.lock index 2e53fe2..cba4cfe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1837,9 +1837,9 @@ lodash.flattendeep@^4.4.0: integrity sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI= lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19: - version "4.17.20" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" - integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== log-driver@^1.2.7: version "1.2.7" @@ -2458,16 +2458,16 @@ rxjs@^6.4.0: dependencies: tslib "^1.9.0" -safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@~5.2.0: - version "5.2.1" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" - integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== - -safe-buffer@~5.1.1: +safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== +safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.2.0, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + "safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" @@ -2957,9 +2957,9 @@ uglify-js@^3.1.4: integrity sha512-fvBeuXOsvqjecUtF/l1dwsrrf5y2BCUk9AOJGzGcm6tE7vegku5u/YvqjyDaAGr422PLoLnrxg3EnRvTqsdC1w== underscore@>=1.8.3, underscore@^1.9.1: - version "1.12.0" - resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.12.0.tgz#4814940551fc80587cef7840d1ebb0f16453be97" - integrity sha512-21rQzss/XPMjolTiIezSu3JAjgagXKROtNrYFEOWK109qY1Uv2tVjPTZ1ci2HgvQDA16gHYSthQIJfB+XId/rQ== + version "1.13.1" + resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.13.1.tgz#0c1c6bd2df54b6b69f2314066d65b6cde6fcf9d1" + integrity sha512-hzSoAVtJF+3ZtiFX0VgfFPHEDRm7Y/QPjGyNo4TVdnDTdft3tr8hEkD25a1jC+TjTuE7tkHGKkhwCgs9dgBB2g== universalify@^0.1.0: version "0.1.2"