UNPKG

@gamechanger-finance/unimatrix

Version:

Unimatrix Sync is a decentralized, privacy preserving, transaction witness sharing and pairing solution for multisignatures or deferred signatures. It was originally created for GameChanger Wallet to improve it's multisignature user experience and boost t

368 lines (367 loc) 19.7 kB
/** * Decentralized, encrypted, privacy preserving, self-integrity aware protocol * using GunDb as shared key-value cache. * @module unimatrix * * @remarks * * There are 2 types of data that can be stored in JSON objects: **Items** or (Item) **Announcements** * * The key-value store design consists on: * - a key that is a hash of a concatenated list of private and/or public strings. The order of the list builds a 'path' * - a value that is a store of private encrypted and public serialized data. * - encryption (and hash) usually reuse the secret in the path among the public strings and the validation method name. * - data validation on read and write events links and checks data against the path and the validation method used. * - to read or write a value you must know its full path, and it must comply the validation method used. * - the strings, or secrets used for the path are considered a private channel. * * Security: * - usually every stored data on key-value store is encrypted with a secret id * - usually if you know the id you can read/write encrypted data * - every key of the key-value store is a hash of the secret id, the (type) validator involved and the unique path of the item * - encryption and data lookup depends on knowing the secret id, the validator and the path of the item. * - non valid data types, invalid data structures, data with hash mismatch, or miss-encrypted data is automatically discarded by a reading peer * - id behaves like a session token, and it should be renewed after usage, for ex: after a finalized multisig operation * - new private channels cannot be expected for when you don't know channel parameters, good for avoiding DDoS attacks * - at scale all key-value pairs populate under a common root, making listening for all pairs difficult * - fake value injection attacks are filtered by encryption and validators * - **Items**: * - Items are checked against type validity tests and hash-matching tests against provided path, * - therefore provide the strongest security, * - knowing the right id for reading/writing encrypted data means nothing if data does not match required path. * - **Announcements**: * - are only checked against type validity, * - therefore provide the weakest security, * - anyone with the right id can read/write the encrypted data. * - but this is acceptable because of data validation * - for example, announcing a sign request of injected transactions can be easily discarded by signer validators checking against transaction body validity * - usually users should announce signing requests and validators in dapps should check transaction body * * * Basic operations: * - `setData`: encrypts and writes data at a path * - `getData`: reads and decrypt first valid data from a path with a timeout * - `onData`: reads all incoming data at a path and tries to validate and decrypt it * */ var __assign = (this && this.__assign) || function () { __assign = Object.assign || function(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __generator = (this && this.__generator) || function (thisArg, body) { var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; function verb(n) { return function (v) { return step([n, v]); }; } function step(op) { if (f) throw new TypeError("Generator is already executing."); while (g && (g = 0, op[0] && (_ = 0)), _) try { if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; if (y = 0, t) op = [op[0] & 2, t.value]; switch (op[0]) { case 0: case 1: t = op; break; case 4: _.label++; return { value: op[1], done: false }; case 5: _.label++; y = op[1]; op = [0]; continue; case 7: op = _.ops.pop(); _.trys.pop(); continue; default: if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } if (t[2]) _.ops.pop(); _.trys.pop(); continue; } op = body.call(thisArg, _); } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } }; import { JSONStringify, logger, } from "./common"; import sha512 from 'crypto-js/sha512'; /** * root node name on GunDB */ export var ROOT_NODE_KEY = "root"; /** * default timeout in milliseconds for getters like `getData` */ export var GET_TIMEOUT_MS = 1000 * 10; /** * Function that generates a Unimatrix hash string based on channel parameters that will be used as key for storing a `UnimatrixEncryptedDataStore` on a GunDb node key-value structure * @param args */ export var genDataKey = function (args) { if (args.path.length <= 0 || !args.id || !args.validator) throw new Error("Missing parts. Cannot generate unimatrix key"); if (args.path.some(function (x) { return !x || !(x === null || x === void 0 ? void 0 : x.trim()); })) throw new Error("Path items cannot be empty. Cannot generate unimatrix key"); var path = "".concat(args.id, ":").concat(args.validator, ":").concat(args.path.join('/')); var key = sha512(path).toString(); if (logger) logger.log("[UNIMATRIX] KEY", { path: path, key: key }); return { key: key, path: path }; }; /** * Listener function that triggers the `on()` callback every time an **Item** or an **Announcement** (`UnimatrixDataStore`) is received on a specific channel. * @param args */ export var onData = function (args) { if (!args.db) throw new Error("Missing GunDb connection"); if (!args.validators) throw new Error("Missing validator definitions"); if (!args.validator) throw new Error("Missing validator tag"); // if(!args.encryptData) // throw new Error("Missing encrypting function"); if (!args.decryptData) throw new Error("Missing decrypting function"); var validatorFn = args.validators[args.validator]; if (!validatorFn) throw new Error("Unknown validator"); var halt = false; var stop = function () { try { halt = true; node.off(); } catch (err) { } ; }; var _a = genDataKey(args), key = _a.key, path = _a.path; var node = args.db .get(ROOT_NODE_KEY) .get(key); if (args === null || args === void 0 ? void 0 : args.timeout) { setTimeout(function () { if (halt) return; stop(); if (logger) logger.error("[UNIMATRIX] GET TimeoutError", { path: path, key: key, timeout: args.timeout }); return args.on({ store: undefined, node: node, timeoutError: true, stop: stop, }); }, args.timeout); } node.on(function (_node, _key) { try { if (halt) { stop(); } var _a = args.decryptData(__assign(__assign({}, args), { store: _node })), file = _a.file, updatedAt = _a.updatedAt; var _b = file || {}, data = _b.data, error = _b.error; if (!error) { var isValid = validatorFn(__assign(__assign({}, args), { store: { file: file, updatedAt: updatedAt } })); if (isValid !== true) { if (logger) logger.error("[UNIMATRIX] GET ValidationError", { path: path, key: key, file: file, validationError: isValid, updatedAt: updatedAt }); return args.on({ store: undefined, node: node, validationError: isValid, stop: stop, }); } if (logger) logger.info("[UNIMATRIX] GET", { path: path, key: key, file: file, updatedAt: updatedAt }); return args.on({ store: { file: { data: data, error: undefined, }, updatedAt: updatedAt }, node: node, stop: stop, }); } else { if (logger) logger.warn("[UNIMATRIX] GET ReceivedError", { path: path, key: key, file: file, updatedAt: updatedAt }); return args.on({ store: { file: { data: undefined, error: error, }, updatedAt: updatedAt }, node: node, userError: error, stop: stop, }); } } catch (err) { //most likely a data injection attack, so raising this error would be just causing too much noise if (logger) logger.error("[UNIMATRIX] GET Error: ".concat(err, "."), { path: path, key: key, _node: _node, _key: _key }); } }, { change: (args === null || args === void 0 ? void 0 : args.change) !== undefined ? args.change : true }); }; /** * Getter promise that gets a specific **Item** or **Announcement** (`UnimatrixDataStore`) from a specific channel. * @param args */ export var getData = function (args) { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) { return [2 /*return*/, new Promise(function (resolve, reject) { try { onData(__assign(__assign({}, args), { timeout: (args === null || args === void 0 ? void 0 : args.timeout) !== undefined ? args.timeout : GET_TIMEOUT_MS, on: function (_a) { var _b, _c; var store = _a.store, validationError = _a.validationError, timeoutError = _a.timeoutError, node = _a.node, stop = _a.stop; if ((args === null || args === void 0 ? void 0 : args.throwValidationErrors) && validationError) { stop(); return reject(validationError); } if ((args === null || args === void 0 ? void 0 : args.throwUserErrors) && ((_b = store === null || store === void 0 ? void 0 : store.file) === null || _b === void 0 ? void 0 : _b.error)) { stop(); return reject((_c = store === null || store === void 0 ? void 0 : store.file) === null || _c === void 0 ? void 0 : _c.error); } if (timeoutError) { stop(); if (args === null || args === void 0 ? void 0 : args.throwTimeoutErrors) return reject("TimeoutError"); else return resolve(undefined); } stop(); return resolve(store); } })); } catch (err) { return reject("GetDataError. ".concat(err)); } })]; }); }); }; var gunPut = function (node, val) { return new Promise(function (res, rej) { var sub = node.once(function (data, _key) { var _a; if (data !== val) node.put(val, function (ack) { var _a; (_a = sub === null || sub === void 0 ? void 0 : sub.off) === null || _a === void 0 ? void 0 : _a.call(sub); sub = undefined; if (ack === null || ack === void 0 ? void 0 : ack.err) { res(String(ack === null || ack === void 0 ? void 0 : ack.err)); } res(true); }); else { (_a = sub === null || sub === void 0 ? void 0 : sub.off) === null || _a === void 0 ? void 0 : _a.call(sub); sub = undefined; res(true); } }); }); }; /** * Setter promise that puts an **Item** or **Announcement** (`UnimatrixDataStore`) on a specific channel. * @param args */ export var setData = function (args) { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) { return [2 /*return*/, new Promise(function (resolve, reject) { return __awaiter(void 0, void 0, void 0, function () { var validatorFn, _a, key, path, updatedAt, isValid, store, encryptedStore, node, result, retrievedData, err_1; var _b, _c, _d, _e, _f, _g, _h, _j, _k; return __generator(this, function (_l) { switch (_l.label) { case 0: _l.trys.push([0, 4, , 5]); if (!args.db) throw new Error("Missing GunDb connection"); if (!args.validators) throw new Error("Missing validator definitions"); if (!args.validator) throw new Error("Missing validator tag"); if (!args.encryptData) throw new Error("Missing encrypting function"); if (!args.decryptData) throw new Error("Missing decrypting function"); validatorFn = args.validators[args.validator]; if (!validatorFn) throw new Error("Unknown validator"); _a = genDataKey(args), key = _a.key, path = _a.path; updatedAt = ((_b = args === null || args === void 0 ? void 0 : args.store) === null || _b === void 0 ? void 0 : _b.updatedAt) || Date.now(); if ((_d = (_c = args === null || args === void 0 ? void 0 : args.store) === null || _c === void 0 ? void 0 : _c.file) === null || _d === void 0 ? void 0 : _d.data) { isValid = validatorFn(__assign(__assign({}, args), { store: args.store })); if (isValid !== true) throw new Error(isValid); } store = { file: args.store.file, updatedAt: updatedAt }; encryptedStore = args.encryptData(__assign(__assign({}, args), store)); node = args.db .get(ROOT_NODE_KEY) .get(key); return [4 /*yield*/, gunPut(node, encryptedStore)]; case 1: result = _l.sent(); if (result !== true) { throw new Error(result); } else { if (logger) logger.log("[UNIMATRIX] SET", { path: path, key: key, store: store, result: result }); } if (!(args === null || args === void 0 ? void 0 : args.checkByFetching)) return [3 /*break*/, 3]; return [4 /*yield*/, getData(args)]; case 2: retrievedData = _l.sent(); if (JSONStringify({ file: { data: (_e = retrievedData === null || retrievedData === void 0 ? void 0 : retrievedData.file) === null || _e === void 0 ? void 0 : _e.data, error: (_f = retrievedData === null || retrievedData === void 0 ? void 0 : retrievedData.file) === null || _f === void 0 ? void 0 : _f.error }, updatedAt: retrievedData === null || retrievedData === void 0 ? void 0 : retrievedData.updatedAt, }) !== JSONStringify({ file: { data: (_g = store.file) === null || _g === void 0 ? void 0 : _g.data, error: (_h = store.file) === null || _h === void 0 ? void 0 : _h.error }, updatedAt: store === null || store === void 0 ? void 0 : store.updatedAt, })) if (logger) logger.warn("[UNIMATRIX] SET Error. Value not set or race condition detected.", { path: path, key: key, file: (_j = args === null || args === void 0 ? void 0 : args.store) === null || _j === void 0 ? void 0 : _j.file, retrievedData: retrievedData, node: node, result: result }); _l.label = 3; case 3: return [2 /*return*/, resolve(store)]; case 4: err_1 = _l.sent(); if (logger) logger.warn("[UNIMATRIX] SET Error. ".concat(err_1), { id: args.id, path: args.path, file: (_k = args === null || args === void 0 ? void 0 : args.store) === null || _k === void 0 ? void 0 : _k.file }); return [2 /*return*/, reject(err_1)]; case 5: return [2 /*return*/]; } }); }); })]; }); }); };