445 lines
16 KiB
JavaScript
445 lines
16 KiB
JavaScript
'use strict';
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const lockfile = require('proper-lockfile');
|
|
const DB = require("./database");
|
|
|
|
var _I = ""; //Instance support
|
|
for (let arg of process.argv)
|
|
if (/^-I=/.test(arg)) {
|
|
_I = arg.split(/=(.*)/s)[1];
|
|
break;
|
|
}
|
|
|
|
const {
|
|
SHARES_PER_NODE,
|
|
SHARE_THRESHOLD,
|
|
SHUFFLE_INTERVAL
|
|
} = require("./_constants")["keys"];
|
|
|
|
const PRIV_EKEY_MIN = 32,
|
|
PRIV_EKEY_MAX = 48,
|
|
PRIME_FILE_TYPE = 'binary',
|
|
INDEX_FILE_TYPE = 'utf-8',
|
|
UNSIGNED_INT_MIN = 0,
|
|
UNSIGNED_INT_MAX = 4294967295,
|
|
INDEX_FILE_NAME_LENGTH = 16,
|
|
INDEX_FILE_EXT = '.txt',
|
|
MIN_DUMMY_FILES = 16,
|
|
MAX_DUMMY_FILES = 24,
|
|
MIN_DUMMY_SIZE_MUL = 0.5,
|
|
MAX_DUMMY_SIZE_MUL = 1.5,
|
|
SIZE_FACTOR = 100,
|
|
LOCK_RETRY_MIN_TIME = 1 * 1000,
|
|
LOCK_RETRY_MAX_TIME = 2 * 1000;
|
|
|
|
var node_priv, e_key, node_id, node_pub; //containers for node-key wrapper
|
|
const _x = {
|
|
get node_priv() {
|
|
if (!node_priv || !e_key)
|
|
throw Error("keys not set");
|
|
return Crypto.AES.decrypt(node_priv, e_key);
|
|
},
|
|
set node_priv(key) {
|
|
node_pub = floCrypto.getPubKeyHex(key);
|
|
node_id = floCrypto.getFloID(node_pub);
|
|
if (!key || !node_pub || !node_id)
|
|
throw Error("Invalid Keys");
|
|
let n = floCrypto.randInt(PRIV_EKEY_MIN, PRIV_EKEY_MAX)
|
|
e_key = floCrypto.randString(n);
|
|
node_priv = Crypto.AES.encrypt(key, e_key);
|
|
},
|
|
args_dir: path.resolve(__dirname, '..', 'args'),
|
|
get index_dir() {
|
|
return path.join(this.args_dir, `indexes${_I}`)
|
|
},
|
|
get prime_file() {
|
|
return path.join(this.args_dir, `prime_index${_I}.b`)
|
|
},
|
|
get index_file() {
|
|
try {
|
|
let data = fs.readFileSync(this.prime_file, PRIME_FILE_TYPE),
|
|
fname = Crypto.AES.decrypt(data, this.node_priv);
|
|
return path.join(this.index_dir, fname + INDEX_FILE_EXT);
|
|
} catch (error) {
|
|
console.debug(error);
|
|
throw Error("Prime-Index Missing/Corrupted");
|
|
}
|
|
}
|
|
}
|
|
|
|
function initialize() {
|
|
return new Promise((resolve, reject) => {
|
|
fs.readFile(_x.prime_file, PRIME_FILE_TYPE, (err, res) => {
|
|
var data, cur_filename, new_filename, priv_key;
|
|
try {
|
|
priv_key = _x.node_priv;
|
|
} catch (error) {
|
|
return reject(error);
|
|
}
|
|
if (!err) {
|
|
if (res.length) { //prime file not empty
|
|
try {
|
|
cur_filename = Crypto.AES.decrypt(res, priv_key);
|
|
} catch (error) {
|
|
console.debug(error);
|
|
return reject("Prime file corrupted");
|
|
} try { //read data from index file
|
|
let tmp = fs.readFileSync(path.join(_x.index_dir, cur_filename + INDEX_FILE_EXT), INDEX_FILE_TYPE);
|
|
tmp = Crypto.AES.decrypt(tmp, priv_key);
|
|
JSON.parse(tmp); //check if data is JSON parse-able
|
|
data = tmp;
|
|
} catch (error) {
|
|
console.debug(error);
|
|
return reject("Index file corrupted");
|
|
}
|
|
}
|
|
}
|
|
try {
|
|
if (!fs.existsSync(_x.index_dir)) {
|
|
fs.mkdirSync(_x.index_dir);
|
|
}
|
|
} catch (error) {
|
|
console.debug(error);
|
|
return reject("Index directory creation failed");
|
|
}
|
|
try { //delete all old dummy files
|
|
let files = fs.readdirSync(_x.index_dir);
|
|
for (const file of files)
|
|
if (!cur_filename || file !== cur_filename + INDEX_FILE_EXT) //check if file is current file
|
|
fs.unlinkSync(path.join(_x.index_dir, file));
|
|
} catch (error) {
|
|
console.debug(error);
|
|
return reject("Clear index directory failed");
|
|
} try { //create files (dummy and new index file)
|
|
let N = floCrypto.randInt(MIN_DUMMY_FILES, MAX_DUMMY_FILES),
|
|
k = floCrypto.randInt(0, N);
|
|
if (typeof data === 'undefined' || data.length == 0) //no existing data, initialize
|
|
data = JSON.stringify({});
|
|
let data_size = data.length;
|
|
for (let i = 0; i <= N; i++) {
|
|
let f_data, f_name = floCrypto.randString(INDEX_FILE_NAME_LENGTH);
|
|
if (i == k) {
|
|
new_filename = f_name;
|
|
f_data = data;
|
|
} else {
|
|
let d_size = data_size * (floCrypto.randInt(MIN_DUMMY_SIZE_MUL * SIZE_FACTOR, MAX_DUMMY_SIZE_MUL * SIZE_FACTOR) / SIZE_FACTOR);
|
|
f_data = floCrypto.randString(d_size, false);
|
|
}
|
|
f_data = Crypto.AES.encrypt(f_data, priv_key);
|
|
fs.writeFileSync(path.join(_x.index_dir, f_name + INDEX_FILE_EXT), f_data, INDEX_FILE_TYPE);
|
|
}
|
|
} catch (error) {
|
|
console.debug(error);
|
|
return reject("Index file creation failed");
|
|
} try { //update prime file
|
|
let en_filename = Crypto.AES.encrypt(new_filename, priv_key);
|
|
fs.writeFileSync(_x.prime_file, en_filename, PRIME_FILE_TYPE);
|
|
} catch (error) {
|
|
console.debug(error);
|
|
return reject("Update prime file failed");
|
|
}
|
|
if (cur_filename)
|
|
fs.unlink(path.join(_x.index_dir, cur_filename + INDEX_FILE_EXT), err => err ? console.debug(err) : null);
|
|
shuffle.interval = setInterval(shuffle, SHUFFLE_INTERVAL);
|
|
resolve("Key management initiated");
|
|
})
|
|
})
|
|
}
|
|
|
|
function shuffle() {
|
|
readIndexFile().then(data => {
|
|
let new_filename, cur_filename = Crypto.AES.decrypt(fs.readFileSync(_x.prime_file, PRIME_FILE_TYPE), _x.node_priv);
|
|
fs.readdir(_x.index_dir, (err, files) => {
|
|
if (err)
|
|
return console.error(err);
|
|
data = JSON.stringify(data);
|
|
let data_size = data.length;
|
|
for (let file of files) {
|
|
let f_data, f_name = floCrypto.randString(INDEX_FILE_NAME_LENGTH);
|
|
if (file === cur_filename + INDEX_FILE_EXT) {
|
|
new_filename = f_name;
|
|
f_data = data;
|
|
} else {
|
|
let d_size = data_size * (floCrypto.randInt(MIN_DUMMY_SIZE_MUL * SIZE_FACTOR, MAX_DUMMY_SIZE_MUL * SIZE_FACTOR) / SIZE_FACTOR);
|
|
f_data = floCrypto.randString(d_size, false);
|
|
}
|
|
f_data = Crypto.AES.encrypt(f_data, _x.node_priv);
|
|
//rename and rewrite the file
|
|
try {
|
|
fs.renameSync(path.join(_x.index_dir, file), path.join(_x.index_dir, f_name + INDEX_FILE_EXT));
|
|
fs.writeFileSync(path.join(_x.index_dir, f_name + INDEX_FILE_EXT), f_data, INDEX_FILE_TYPE);
|
|
} catch (error) {
|
|
console.error(error)
|
|
}
|
|
}
|
|
//update prime file
|
|
if (!new_filename)
|
|
return console.error("Index file has not been renamed");
|
|
let en_filename = Crypto.AES.encrypt(new_filename, _x.node_priv);
|
|
try {
|
|
fs.writeFileSync(_x.prime_file, en_filename, PRIME_FILE_TYPE);
|
|
} catch (error) {
|
|
console.error(error);
|
|
}
|
|
})
|
|
}).catch(error => console.error(error))
|
|
}
|
|
|
|
function readIndexFile() {
|
|
return new Promise((resolve, reject) => {
|
|
fs.readFile(_x.index_file, INDEX_FILE_TYPE, (err, data) => {
|
|
if (err) {
|
|
console.debug(err);
|
|
return reject('Unable to read Index file');
|
|
}
|
|
try {
|
|
data = JSON.parse(Crypto.AES.decrypt(data, _x.node_priv));
|
|
resolve(data);
|
|
} catch {
|
|
reject("Index file corrupted");
|
|
}
|
|
})
|
|
})
|
|
}
|
|
|
|
function writeIndexFile(data) {
|
|
return new Promise((resolve, reject) => {
|
|
let en_data = Crypto.AES.encrypt(JSON.stringify(data), _x.node_priv);
|
|
fs.writeFile(_x.index_file, en_data, INDEX_FILE_TYPE, (err) => {
|
|
if (err) {
|
|
console.debug(err);
|
|
return reject('Unable to write Index file');
|
|
} else resolve("Updated Index file");
|
|
})
|
|
})
|
|
}
|
|
|
|
function getShares(group, id, ignoreDiscarded = true) {
|
|
return new Promise((resolve, reject) => {
|
|
checkIfDiscarded(id).then(result => {
|
|
if (ignoreDiscarded && result != false)
|
|
return reject("Trying to get share for discarded ID");
|
|
readIndexFile().then(data => {
|
|
if (!(group in data))
|
|
reject("Group not found in Index file");
|
|
else if (!(id in data[group]))
|
|
reject("ID not found in Index file");
|
|
else {
|
|
let ref = data[group][id].shift();
|
|
DB.query("SELECT share FROM sinkShares WHERE num IN (?)", [data[group][id]])
|
|
.then(result => resolve({ ref, shares: result.map(r => Crypto.AES.decrypt(r.share, _x.node_priv)) }))
|
|
.catch(error => reject(error))
|
|
}
|
|
}).catch(error => reject(error))
|
|
}).catch(error => reject(error))
|
|
})
|
|
}
|
|
|
|
function storeShareAtRandom(share) {
|
|
return new Promise((resolve, reject) => {
|
|
let rand = floCrypto.randInt(UNSIGNED_INT_MIN, UNSIGNED_INT_MAX);
|
|
DB.query("INSERT INTO sinkShares(num, share) VALUE (?)", [[rand, share]])
|
|
.then(result => resolve(result.insertId)).catch(error => {
|
|
if (error.code === "ER_DUP_ENTRY")
|
|
storeShareAtRandom(share) //try again (with diff rand_num)
|
|
.then(result => resolve(result))
|
|
.catch(error => reject(error))
|
|
else
|
|
reject(error);
|
|
})
|
|
})
|
|
}
|
|
|
|
function addShare(group, id, ref, share) {
|
|
return new Promise((resolve, reject) => {
|
|
checkIfDiscarded(id).then(result => {
|
|
if (result != false)
|
|
return reject("Trying to store share for discarded ID");
|
|
lockfile.lock(_x.index_file, { retries: { forever: true, minTimeout: LOCK_RETRY_MIN_TIME, maxTimeout: LOCK_RETRY_MAX_TIME } }).then(release => {
|
|
const releaseAndReject = err => {
|
|
release().then(_ => null).catch(error => console.error(error));
|
|
reject(err);
|
|
}
|
|
readIndexFile().then(data => {
|
|
if (!(group in data))
|
|
data[group] = {};
|
|
if (!(id in data[group]))
|
|
data[group][id] = [ref];
|
|
else if (ref < data[group][id][0])
|
|
return reject("reference is lower than current");
|
|
else if (ref > data[group][id][0]) {
|
|
let old_shares = data[group][id];
|
|
data[group][id] = [ref];
|
|
old_shares.shift();
|
|
DB.query("DELETE FROM sinkShares WHERE num in (?)", [old_shares])//delete old shares
|
|
.then(_ => null).catch(error => console.error(error));
|
|
}
|
|
let encrypted_share = Crypto.AES.encrypt(share, _x.node_priv);
|
|
console.debug(ref, '|sinkID:', id, '|EnShare:', encrypted_share);
|
|
storeShareAtRandom(encrypted_share).then(i => {
|
|
data[group][id].push(i);
|
|
writeIndexFile(data).then(_ => resolve(i)).catch(error => reject(error))
|
|
.finally(_ => release().then(_ => null).catch(error => console.error(error)));
|
|
}).catch(error => releaseAndReject(error))
|
|
}).catch(error => releaseAndReject(error))
|
|
}).catch(error => reject(error))
|
|
}).catch(error => reject(error))
|
|
})
|
|
}
|
|
|
|
function generateShares(sinkKey, total_n, min_n) {
|
|
let shares = floCrypto.createShamirsSecretShares(sinkKey, total_n * SHARES_PER_NODE, min_n * SHARES_PER_NODE * SHARE_THRESHOLD);
|
|
let node_shares = Array(total_n);
|
|
for (let i = 0; i < total_n; i++)
|
|
node_shares[i] = shares.splice(0, SHARES_PER_NODE);
|
|
return node_shares;
|
|
}
|
|
|
|
function getStoredList(group = null) {
|
|
return new Promise((resolve, reject) => {
|
|
readIndexFile().then(data => {
|
|
if (group !== null) {
|
|
if (group in data)
|
|
resolve(Object.keys(data.group));
|
|
else
|
|
reject("Group not found in Index file");
|
|
} else {
|
|
let ids = {};
|
|
for (let group in data)
|
|
ids[group] = Object.keys(data[group]);
|
|
resolve(ids);
|
|
}
|
|
}).catch(error => reject(error))
|
|
})
|
|
}
|
|
|
|
function getDiscardedList() {
|
|
return new Promise((resolve, reject) => {
|
|
DB.query("SELECT floID, discard_time FROM discardedSinks")
|
|
.then(result => resolve(Object.fromEntries(result.map(r => [r.floID, r.discard_time]))))
|
|
.catch(error => reject(error))
|
|
})
|
|
}
|
|
|
|
function checkIfDiscarded(id) {
|
|
return new Promise((resolve, reject) => {
|
|
DB.query("SELECT discard_time FROM discardedSinks WHERE floID=?", [id])
|
|
.then(result => resolve(result.length ? result[0].discard_time : false))
|
|
.catch(error => reject(error))
|
|
})
|
|
}
|
|
|
|
function discardSink(id) {
|
|
return new Promise((resolve, reject) => {
|
|
DB.query("INSERT INTO discardedSinks(floID) VALUE (?)", [id])
|
|
.then(result => resolve(`Discarded ${id}`))
|
|
.catch(error => reject(error))
|
|
})
|
|
}
|
|
|
|
//Sink groups and chest
|
|
const sink_groups = {
|
|
get EXCHANGE() { return "exchange" },
|
|
get CONVERT() { return "convert" },
|
|
get BLOCKCHAIN_BONDS() { return "blockchain_bonds" },
|
|
get BOBS_FUND() { return "bobs_fund" },
|
|
get list() { //total list
|
|
return [this.EXCHANGE, this.CONVERT, this.BLOCKCHAIN_BONDS, this.BOBS_FUND]
|
|
},
|
|
get initial_list() { //list to generate when starting exchange
|
|
return [this.EXCHANGE, this.CONVERT]
|
|
},
|
|
get generate_list() { //list allowed to generate
|
|
return [this.EXCHANGE, this.CONVERT, this.BLOCKCHAIN_BONDS, this.BOBS_FUND]
|
|
}
|
|
};
|
|
|
|
const sink_ids = {}, sink_chest = {
|
|
reset() {
|
|
for (let i in sink_ids)
|
|
delete sink_ids[i];
|
|
},
|
|
set_id(group, id, value) {
|
|
if (!(group in sink_ids))
|
|
sink_ids[group] = {};
|
|
sink_ids[group][id] = value;
|
|
},
|
|
rm_id(group, id) {
|
|
return delete sink_ids[group][id];
|
|
},
|
|
get_id(group, id) {
|
|
return sink_ids[group][id];
|
|
},
|
|
list(group) {
|
|
return Object.keys(sink_ids[group] || {});
|
|
},
|
|
active_list(group) {
|
|
let ids = [];
|
|
if (group in sink_ids)
|
|
for (let id in sink_ids[group])
|
|
if (sink_ids[group][id])
|
|
ids.push(id);
|
|
return ids;
|
|
},
|
|
includes(group, id) {
|
|
return group in sink_ids ? (id in sink_ids[group]) : null;
|
|
},
|
|
isActive(group, id) {
|
|
return group in sink_ids ? (id in sink_ids && sink_ids[id]) : null;
|
|
},
|
|
pick(group) {
|
|
let ids = this.list(group),
|
|
i = floCrypto.randInt(0, ids.length - 1);
|
|
return ids[i];
|
|
},
|
|
active_pick(group) {
|
|
let ids = this.active_list(group),
|
|
i = floCrypto.randInt(0, ids.length - 1);
|
|
return ids[i];
|
|
},
|
|
find_group(id) {
|
|
let group = null;
|
|
for (let g in sink_ids)
|
|
if (id in sink_ids[g]) {
|
|
group = g; break;
|
|
}
|
|
return group;
|
|
},
|
|
get_all() {
|
|
let ids = {};
|
|
for (let g in sink_ids)
|
|
ids[g] = Object.keys(sink_ids[g])
|
|
return ids;
|
|
}
|
|
};
|
|
|
|
module.exports = {
|
|
init: initialize,
|
|
getShares,
|
|
addShare,
|
|
generateShares,
|
|
getStoredList,
|
|
getDiscardedList,
|
|
checkIfDiscarded,
|
|
discardSink,
|
|
set node_priv(key) {
|
|
_x.node_priv = key;
|
|
},
|
|
get node_priv() {
|
|
return _x.node_priv;
|
|
},
|
|
get node_id() {
|
|
return node_id;
|
|
},
|
|
get node_pub() {
|
|
return node_pub;
|
|
},
|
|
get sink_groups() {
|
|
return sink_groups;
|
|
},
|
|
get sink_chest() {
|
|
return sink_chest;
|
|
}
|
|
} |