crypto-conditions
Version:
Implementation of crypto-conditions in JavaScript
487 lines (399 loc) • 17.6 kB
JavaScript
"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;