Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial XBR protocol implementation #424

Merged
merged 18 commits into from
May 21, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions lib/autobahn.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ var serializer = require('./serializer.js');
var persona = require('./auth/persona.js');
var cra = require('./auth/cra.js');
var cryptosign = require('./auth/cryptosign.js');
var xbr = require('./xbr/xbr.js');

exports.version = pjson.version;

Expand Down Expand Up @@ -71,3 +72,4 @@ exports.nacl = nacl;

exports.util = util;
exports.log = log;
exports.xbr = xbr;
37 changes: 1 addition & 36 deletions lib/connection.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,42 +28,7 @@ var Connection = function (options) {

// Deferred factory
//
if (options && options.use_es6_promises) {

if ('Promise' in global) {
// ES6-based deferred factory
//
self._defer = function () {
var deferred = {};

deferred.promise = new Promise(function (resolve, reject) {
deferred.resolve = resolve;
deferred.reject = reject;
});

return deferred;
};
} else {

log.debug("Warning: ES6 promises requested, but not found! Falling back to whenjs.");

// whenjs-based deferred factory
//
self._defer = when.defer;
}

} else if (options && options.use_deferred) {

// use explicit deferred factory, e.g. jQuery.Deferred or Q.defer
//
self._defer = options.use_deferred;

} else {

// whenjs-based deferred factory
//
self._defer = when.defer;
}
self._defer = util.deferred_factory(options);


// WAMP transport
Expand Down
54 changes: 54 additions & 0 deletions lib/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -289,9 +289,63 @@ var new_global_id = function() {
return Math.floor(Math.random() * 9007199254740992) + 1;
};

var deferred_factory = function(options) {
var defer = null;

if (options && options.use_es6_promises) {

if ('Promise' in global) {
// ES6-based deferred factory
//
defer = function () {
var deferred = {};

deferred.promise = new Promise(function (resolve, reject) {
deferred.resolve = resolve;
deferred.reject = reject;
});

return deferred;
};
} else {

log.debug("Warning: ES6 promises requested, but not found! Falling back to whenjs.");

// whenjs-based deferred factory
//
defer = when.defer;
}

} else if (options && options.use_deferred) {

// use explicit deferred factory, e.g. jQuery.Deferred or Q.defer
//
defer = options.use_deferred;

} else {

// whenjs-based deferred factory
//
defer = when.defer;
}

return defer;
};

var promise = function(d) {
if (d.promise.then) {
// whenjs has the actual user promise in an attribute
return d.promise;
} else {
return d;
}
};

exports.handle_error = handle_error;
exports.rand_normal = rand_normal;
exports.assert = assert;
exports.http_post = http_post;
exports.defaults = defaults;
exports.new_global_id = new_global_id;
exports.deferred_factory = deferred_factory;
exports.promise = promise;
125 changes: 125 additions & 0 deletions lib/xbr/buyer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
var cbor = require('cbor');
var nacl = require('tweetnacl');
var eth_accounts = require("web3-eth-accounts");
var eth_util = require("ethereumjs-util");
var util = require('../util.js');


var SimpleBuyer = function (buyerKey, maxPrice) {
this._running = false;
this._session = null;
this._channel = null;
this._balance = null;
this._keys = {};
this._maxPrice = maxPrice;
this._deferred_factory = util.deferred_factory();

var account = new eth_accounts.Accounts().privateKeyToAccount(buyerKey);
this._addr = eth_util.toBuffer(account.address);

this._keyPair = nacl.box.keyPair();
};

SimpleBuyer.prototype.start = function(session, consumerID) {
self = this;
self._session = session;
self._running = true;

var d = this._deferred_factory();

session.call('xbr.marketmaker.get_payment_channel', [self._addr]).then(
function (paymentChannel) {
self._channel = paymentChannel;
self._balance = paymentChannel['remaining'];
d.resolve(self._balance);
},
function (error) {
console.log("Call failed:", error);
d.reject(error['error']);
}
);

return util.promise(d);
};

SimpleBuyer.prototype.stop = function () {
this._running = false;
};

SimpleBuyer.prototype.balance = function () {
var d = this._deferred_factory();
this._session.call('xbr.marketmaker.get_payment_channel', [self._addr]).then(
function (paymentChannel) {
var balance = {
amount: paymentChannel['amount'],
remaining: paymentChannel['remaining'],
inflight: paymentChannel['inflight']
};
d.resolve(balance);
},
function (error) {
console.log("Call failed:", error);
d.reject(error['error']);
}
);
return util.promise(d);
};

SimpleBuyer.prototype.openChannel = function (buyerAddr, amount) {
var signature = nacl.randomBytes(64);
var d = this._deferred_factory();
this._session.call(
'xbr.marketmaker.open_payment_channel',
[buyerAddr, this._addr, amount, signature]
).then(
function (paymentChannel) {
var balance = {
amount: paymentChannel['amount'],
remaining: paymentChannel['remaining'],
inflight: paymentChannel['inflight']
};
d.resolve(balance);
},
function (error) {
console.log("Call failed:", error);
d.reject(error['error']);
}
);
return util.promise(d);
};

SimpleBuyer.prototype.closeChannel = function () {
};

SimpleBuyer.prototype.unwrap = function (keyID, ciphertext) {
self = this;
var d = self._deferred_factory();
if (!self._keys.hasOwnProperty(keyID)) {
self._keys[keyID] = false;
self._session.call(
'xbr.marketmaker.buy_key',
[self._addr, self._keyPair.publicKey, keyID, self._maxPrice, nacl.randomBytes(64)]
).then(
function (receipt) {
var sealedKey = receipt['sealed_key'];
try {
self._keys[keyID] = nacl.sealedbox.open(sealedKey, self._keyPair.publicKey,
self._keyPair.secretKey);
var nonce = ciphertext.slice(0, nacl.secretbox.nonceLength);
var message = ciphertext.slice(nacl.secretbox.nonceLength, ciphertext.length);
var decrypted = nacl.secretbox.open(message, nonce, self._keys[keyID]);
var payload = cbor.decode(decrypted);
d.resolve(payload);
} catch (e) {
d.reject(e)
}
},
function (error) {
d.reject(error['error'])
}
);
}
return util.promise(d);
};

exports.SimpleBuyer = SimpleBuyer;
50 changes: 50 additions & 0 deletions lib/xbr/keyseries.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
var cbor = require('cbor');
var nacl = require('tweetnacl');
var sealedbox = require('tweetnacl-sealedbox-js');

var KeySeries = function(apiID, prefix, price, interval, onRotate) {
this.apiID = apiID;
this.price = price;
this.interval = interval;
this.prefix = prefix;
this.onRotate = onRotate;
this._archive = {};
this._started = false;
};

KeySeries.prototype.encrypt = function(payload) {
var nonce = nacl.randomBytes(nacl.secretbox.nonceLength);
var box = nacl.secretbox(cbor.encode(payload), nonce, this._archive[this.keyID]);
var fullMessage = new Uint8Array(nonce.length + box.length);
fullMessage.set(nonce);
fullMessage.set(box, nonce.length);
return fullMessage;
};

KeySeries.prototype.encryptKey = function(keyID, buyerPubKey) {
return sealedbox.seal(this._archive[this.keyID], buyerPubKey)
};

KeySeries.prototype.start = function() {
if (!this._started) {
this._rotate(this);
this._started = true;
}
};

KeySeries.prototype._rotate = function(context) {
context.keyID = nacl.randomBytes(16);
context._archive[context.keyID] = nacl.randomBytes(nacl.secretbox.keyLength);
context.onRotate(context);
// Rotate the keys
// FIXME: make this to wait for the above onRotate callback to finish
setTimeout(context._rotate, context.interval, context);
};

KeySeries.prototype.stop = function() {
if (this._started) {
this._started = false;
}
};

exports.KeySeries = KeySeries;
89 changes: 89 additions & 0 deletions lib/xbr/seller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
var autobahn = require("../autobahn.js");
var eth_accounts = require("web3-eth-accounts");
var eth_util = require("ethereumjs-util");
var key_series = require('./keyseries');
var util = require('../util.js');


var Seller = function (sellerKey) {
self = this;
this.sellerKey = sellerKey;
this.keys = {};
this.keysMap = {};
this._providerID = eth_util.bufferToHex(eth_util.privateToPublic(sellerKey));
this._session = null;
this.sessionRegs = [];
this._deferred_factory = util.deferred_factory();

var account = new eth_accounts.Accounts().privateKeyToAccount(sellerKey);
this._addr = eth_util.toBuffer(account.address);
this._privateKey = eth_util.toBuffer(account.privateKey);
};

Seller.prototype.start = function (session) {
self._session = session;

var d = this._deferred_factory();
var procedure = 'xbr.protocol.' + self._providerID + '.sell';
session.register(procedure, self.sell).then(
function (registration) {
self.sessionRegs.push(registration);
for (var key in self.keys) {
self.keys[key].start();
}
d.resolve();
},
function (error) {
console.log("Registration failed:", error);
d.reject();
}
);
return util.promise(d);
};

Seller.prototype.sell = function (key_id, buyer_pubkey) {
if (!this.keysMap.hasOwnProperty(key_id)) {
throw "no key with ID " + key_id;
}
return this.keysMap[key_id].encryptKey(key_id, buyer_pubkey)
};

Seller.prototype.add = function (apiID, prefix, price, interval) {
var keySeries = new key_series.KeySeries(apiID, prefix, price, interval, _onRotate);
this.keys[apiID] = keySeries;
return keySeries;
};

var _onRotate = function (series) {
self.keysMap[series.keyID] = series;

self._session.call(
'xbr.marketmaker.place_offer',
[series.keyID, series.apiID, series.prefix, BigInt(Date.now() * 1000000 - 10 * 10 ** 9),
self._addr, autobahn.nacl.randomBytes(64)],
{price: series.price, provider_id: self._providerID}
).then(
function (result) {
console.log("Offer placed for key", result['key']);
},
function (error) {
console.log("Call failed:", error);
}
)
};

Seller.prototype.stop = function () {
for (var key in this.keys) {
this.keys[key].stop()
}

for (var i = 0; i < this.sessionRegs.length; i++) {
this.sessionRegs[i].unregister()
}
};

Seller.prototype.wrap = function (api_id, uri, payload) {
return this.keys[api_id].encrypt(payload)
};

exports.SimpleSeller = Seller;
2 changes: 2 additions & 0 deletions lib/xbr/xbr.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
exports.SimpleBuyer = require('./buyer.js').SimpleBuyer;
exports.SimpleSeller = require('./seller.js').SimpleSeller;
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"randombytes": ">=2.0.6",
"tweetnacl": ">= 0.14.3",
"tweetnacl-sealedbox-js": ">=1.1.0",
"web3": ">=1.0.0-beta.53",
"when": ">= 3.7.7",
"ws": ">= 1.1.4"
},
Expand Down