UNPKG

crypto-conditions

Version:

Implementation of crypto-conditions in JavaScript

487 lines (399 loc) 17.6 kB
"use strict"; var _sliceInstanceProperty = require("@babel/runtime-corejs3/core-js-stable/instance/slice"); var _Array$from2 = require("@babel/runtime-corejs3/core-js-stable/array/from"); var _Symbol = require("@babel/runtime-corejs3/core-js-stable/symbol"); var _getIteratorMethod = require("@babel/runtime-corejs3/core-js/get-iterator-method"); var _Array$isArray = require("@babel/runtime-corejs3/core-js-stable/array/is-array"); var _Object$defineProperty = require("@babel/runtime-corejs3/core-js-stable/object/define-property"); var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault"); _Object$defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _sort = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/instance/sort")); var _from = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/array/from")); var _map = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/instance/map")); var _reduce = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/instance/reduce")); var _set = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/set")); var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/classCallCheck")); var _createClass2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/createClass")); var _defineProperty2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/defineProperty")); var _querystring = require("querystring"); var _typeRegistry = _interopRequireDefault(require("./type-registry")); var _prefixError = _interopRequireDefault(require("../errors/prefix-error")); var _parseError = _interopRequireDefault(require("../errors/parse-error")); var _missingDataError = _interopRequireDefault(require("../errors/missing-data-error")); var _base64url = _interopRequireDefault(require("../util/base64url")); var _isInteger = _interopRequireDefault(require("../util/is-integer")); var _condition = require("../schemas/condition"); function _createForOfIteratorHelper(o, allowArrayLike) { var it = typeof _Symbol !== "undefined" && _getIteratorMethod(o) || o["@@iterator"]; if (!it) { if (_Array$isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it.return != null) it.return(); } finally { if (didErr) throw err; } } }; } function _unsupportedIterableToArray(o, minLen) { var _context4; if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = _sliceInstanceProperty(_context4 = Object.prototype.toString.call(o)).call(_context4, 8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return _Array$from2(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; } // Regex for validating conditions // // This is a generic, future-proof version of the crypto-condition regular // expression. var CONDITION_REGEX = /^ni:\/\/\/sha-256;([a-zA-Z0-9_-]{0,86})\?(.+)$/; // This is a stricter version based on limitations of the current // implementation. Specifically, we can't handle bitmasks greater than 32 bits. var CONDITION_REGEX_STRICT = CONDITION_REGEX; var INTEGER_REGEX = /^0|[1-9]\d*$/; /** * Crypto-condition. * * A primary design goal of crypto-conditions was to keep the size of conditions * constant. Even a complex multi-signature can be represented by the same size * condition as a simple hashlock. * * However, this means that a condition only carries the absolute minimum * information required. It does not tell you anything about its structure. * * All that is included with a condition is the fingerprint (usually a hash of * the parts of the fulfillment that are known up-front, e.g. public keys), the * maximum fulfillment size, the set of features used and the condition type. * * This information is just enough that an implementation can tell with * certainty whether it would be able to process the corresponding fulfillment. */ var Condition = /*#__PURE__*/function () { function Condition() { (0, _classCallCheck2.default)(this, Condition); } (0, _createClass2.default)(Condition, [{ key: "getTypeId", value: /** * Return the type of this condition. * * The type is a unique integer ID assigned to each type of condition. * * @return {Number} Type corresponding to this condition. */ function getTypeId() { return this.type; } /** * Set the type. * * Sets the type ID for this condition. * * @param {Number} type Integer representation of type. */ }, { key: "setTypeId", value: function setTypeId(type) { this.type = type; } }, { key: "getTypeName", value: function getTypeName() { return _typeRegistry.default.findByTypeId(this.type).name; } /** * Return the subtypes of this condition. * * For simple condition types this is simply the set of bits representing the * features required by the condition type. * * For structural conditions, this is the bitwise OR of the bitmasks of the * condition and all its subconditions, recursively. * * @return {Number} Bitmask required to verify this condition. */ }, { key: "getSubtypes", value: function getSubtypes() { return this.subtypes; } /** * Set the subtypes. * * Sets the required subtypes to validate a fulfillment for this condition. * * @param {Number} subtypes Integer representation of subtypes. */ }, { key: "setSubtypes", value: function setSubtypes(subtypes) { this.subtypes = subtypes; } /** * Return the hash of the condition. * * A primary component of all conditions is the hash. It encodes the static * properties of the condition. This method enables the conditions to be * constant size, no matter how complex they actually are. The data used to * generate the hash consists of all the static properties of the condition * and is provided later as part of the fulfillment. * * @return {Buffer} Hash of the condition */ }, { key: "getHash", value: function getHash() { if (!this.hash) { throw new _missingDataError.default('Hash not set'); } return this.hash; } /** * Validate and set the hash of this condition. * * Typically conditions are generated from fulfillments and the hash is * calculated automatically. However, sometimes it may be necessary to * construct a condition URI from a known hash. This method enables that case. * * @param {Buffer} hash Hash as binary. */ }, { key: "setHash", value: function setHash(hash) { if (!Buffer.isBuffer(hash)) { throw new TypeError('Hash must be a Buffer'); } if (hash.length !== 32) { throw new Error('Hash is of invalid length ' + hash.length + ', should be 32'); } this.hash = hash; } /** * Return the maximum fulfillment length. * * The maximum fulfillment length is the maximum allowed length for any * fulfillment payload to fulfill this condition. * * The condition defines a maximum fulfillment length which all * implementations will enforce. This allows implementations to verify that * their local maximum fulfillment size is guaranteed to accomodate any * possible fulfillment for this condition. * * Otherwise an attacker could craft a fulfillment which exceeds the maximum * size of one implementation, but meets the maximum size of another, thereby * violating the fundamental property that fulfillments are either valid * everywhere or nowhere. * * @return {Number} Maximum length (in bytes) of any fulfillment payload that * fulfills this condition.. */ }, { key: "getCost", value: function getCost() { if (typeof this.cost !== 'number') { throw new _missingDataError.default('Cost not set'); } return this.cost; } /** * Set the maximum fulfillment length. * * The maximum fulfillment length is normally calculated automatically, when * calling `Fulfillment#getCondition`. However, when * * @param {Number} Maximum fulfillment payload length in bytes. */ }, { key: "setCost", value: function setCost(cost) { if (!(0, _isInteger.default)(cost)) { throw new TypeError('Cost must be an integer'); } else if (cost < 0) { throw new TypeError('Cost must be positive or zero'); } this.cost = cost; } /** * Generate the URI form encoding of this condition. * * Turns the condition into a URI containing only URL-safe characters. This * format is convenient for passing around conditions in URLs, JSON and other * text-based formats. * * @return {String} Condition as a URI */ }, { key: "serializeUri", value: function serializeUri() { var _context; var ConditionClass = _typeRegistry.default.findByTypeId(this.type).Class; var includeSubtypes = ConditionClass.TYPE_CATEGORY === 'compound'; return 'ni:///sha-256;' + _base64url.default.encode(this.getHash()) + '?fpt=' + this.getTypeName() + '&cost=' + this.getCost() + (includeSubtypes ? '&subtypes=' + (0, _sort.default)(_context = (0, _from.default)(this.getSubtypes())).call(_context).join(',') : ''); } /** * Serialize condition to a buffer. * * Encodes the condition as a string of bytes. This is used internally for * encoding subconditions, but can also be used to passing around conditions * in a binary protocol for instance. * * @return {Buffer} Serialized condition */ }, { key: "serializeBinary", value: function serializeBinary() { var asn1Json = this.getAsn1Json(); return _condition.Condition.encode(asn1Json); } }, { key: "getAsn1Json", value: function getAsn1Json() { var ConditionClass = _typeRegistry.default.findByTypeId(this.type).Class; var asn1Json = { type: ConditionClass.TYPE_ASN1_CONDITION, value: { fingerprint: this.getHash(), cost: this.getCost() } }; if (ConditionClass.TYPE_CATEGORY === 'compound') { var _context2, _context3; // Convert the subtypes set of type names to an array of type IDs var subtypeIds = (0, _map.default)(_context2 = (0, _map.default)(_context3 = (0, _from.default)(this.getSubtypes())).call(_context3, _typeRegistry.default.findByName)).call(_context2, function (x) { return x.typeId; }); // Allocate a large enough buffer for the subtypes bitarray var maxId = (0, _reduce.default)(subtypeIds).call(subtypeIds, function (a, b) { return Math.max(a, b); }, 0); var subtypesBuffer = Buffer.alloc(1 + (maxId >>> 3)); var _iterator = _createForOfIteratorHelper(subtypeIds), _step; try { for (_iterator.s(); !(_step = _iterator.n()).done;) { var id = _step.value; subtypesBuffer[id >>> 3] |= 1 << 7 - id % 8; } // Determine the number of unused bits at the end } catch (err) { _iterator.e(err); } finally { _iterator.f(); } var trailingZeroBits = 7 - maxId % 8; asn1Json.value.subtypes = { unused: trailingZeroBits, data: subtypesBuffer }; } return asn1Json; } /** * Ensure the condition is valid according the local rules. * * Checks the condition against the local subtypes (supported condition types) * and the local maximum fulfillment size. * * @return {Boolean} Whether the condition is valid according to local rules. */ }, { key: "validate", value: function validate() { // Get info for type ID, throws on error _typeRegistry.default.findByTypeId(this.getTypeId()); // Bitmask can have at most 32 bits with current implementation if (this.getSubtypes() > Condition.MAX_SAFE_SUBTYPES) { throw new Error('Bitmask too large to be safely represented'); } // Assert all requested features are supported by this implementation if (this.getSubtypes() & ~Condition.SUPPORTED_SUBTYPES) { throw new Error('Condition requested unsupported feature suites'); } // Assert the requested fulfillment size is supported by this implementation if (this.getCost() > Condition.MAX_COST) { throw new Error('Condition requested too large of a max fulfillment size'); } return true; } }], [{ key: "fromUri", value: // Our current implementation can only represent up to 32 bits for our subtypes // Feature suites supported by this implementation // Max fulfillment size supported by this implementation // Expose regular expressions /** * Create a Condition object from a URI. * * This method will parse a condition URI and construct a corresponding * Condition object. * * @param {String} serializedCondition URI representing the condition * @return {Condition} Resulting object */ function fromUri(serializedCondition) { if (serializedCondition instanceof Condition) { return serializedCondition; } else if (typeof serializedCondition !== 'string') { throw new Error('Serialized condition must be a string'); } var pieces = serializedCondition.split(':'); if (pieces[0] !== 'ni') { throw new _prefixError.default('Serialized condition must start with "ni:"'); } var parsed = Condition.REGEX_STRICT.exec(serializedCondition); if (!parsed) { throw new _parseError.default('Invalid condition format'); } var query = (0, _querystring.parse)(parsed[2]); var type = _typeRegistry.default.findByName(query.fpt); var cost = INTEGER_REGEX.exec(query.cost); if (!cost) { throw new _parseError.default('No or invalid cost provided'); } var condition = new Condition(); condition.setTypeId(type.typeId); if (type.Class.TYPE_CATEGORY === 'compound') { condition.setSubtypes(new _set.default(query.subtypes.split(','))); } else { // ? Should be bitmask number ? condition.setSubtypes(new _set.default()); } condition.setHash(_base64url.default.decode(parsed[1])); condition.setCost(Number(query.cost)); return condition; } /** * Create a Condition object from a binary blob. * * This method will parse a stream of binary data and construct a * corresponding Condition object. * * @param {Buffer} data Condition in binary format * @return {Condition} Resulting object */ }, { key: "fromBinary", value: function fromBinary(data) { var conditionJson = _condition.Condition.decode(data); return Condition.fromAsn1Json(conditionJson); } }, { key: "fromAsn1Json", value: function fromAsn1Json(json) { var type = _typeRegistry.default.findByAsn1ConditionType(json.type); var condition = new Condition(); condition.setTypeId(type.typeId); condition.setHash(json.value.fingerprint); condition.setCost(json.value.cost.toNumber()); if (type.Class.TYPE_CATEGORY === 'compound') { var subtypesBuffer = json.value.subtypes.data; var subtypes = new _set.default(); var byteIndex = 0; while (byteIndex < subtypesBuffer.length) { for (var i = 0; i < 8; i++) { if (1 << 7 - i & subtypesBuffer[byteIndex]) { var typeId = byteIndex * 8 + i; var typeName = _typeRegistry.default.findByTypeId(typeId).name; subtypes.add(typeName); } } byteIndex++; } condition.setSubtypes(subtypes); } else { condition.setSubtypes(new _set.default()); } return condition; } }]); return Condition; }(); (0, _defineProperty2.default)(Condition, "MAX_SAFE_SUBTYPES", 0xffffffff); (0, _defineProperty2.default)(Condition, "SUPPORTED_SUBTYPES", 0x3f); (0, _defineProperty2.default)(Condition, "MAX_COST", 2097152); (0, _defineProperty2.default)(Condition, "REGEX", CONDITION_REGEX); (0, _defineProperty2.default)(Condition, "REGEX_STRICT", CONDITION_REGEX_STRICT); var _default = Condition; exports.default = _default;