@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
JavaScript
/**
* 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*/];
}
});
}); })];
});
}); };