diff --git a/index.js b/index.js index 8010480ce..5926e31d5 100644 --- a/index.js +++ b/index.js @@ -46,5 +46,7 @@ module.exports = { Networks: require('bitcoinjs-lib/src/networks'), ECDSA: require('bitcoinjs-lib/src/ecdsa'), SharedMetadata: require('./src/sharedMetadata'), - Contacts: require('./src/contacts') + Contacts: require('./src/contacts'), + SharedMetadataAPI: require('./src/sharedMetadataAPI'), + R: require('ramda') }; diff --git a/package.json b/package.json index 71c8cc28b..0800dfd37 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "bs58": "2.0.*", "es6-promise": "^3.0.2", "isomorphic-fetch": "^2.2.0", + "jwt-decode": "^2.1.0", "pbkdf2": "3.0.4", "ramda": "^0.22.1", "randombytes": "^2.0.1", diff --git a/src/blockchain-wallet.js b/src/blockchain-wallet.js index 7187edc02..e0da664fd 100644 --- a/src/blockchain-wallet.js +++ b/src/blockchain-wallet.js @@ -23,6 +23,7 @@ var AccountInfo = require('./account-info'); var Metadata = require('./metadata'); var constants = require('./constants'); var Payment = require('./payment'); +var SharedMetadata = require('./sharedMetadata'); // Wallet @@ -77,6 +78,8 @@ function Wallet (object) { this._latestBlock = null; this._accountInfo = null; this._external = null; + // handle second password and non-upgraded wallets + this._sharedMetadata = SharedMetadata.fromMasterHDNode(this.hdwallet.getMasterHDNode()); } Object.defineProperties(Wallet.prototype, { diff --git a/src/sharedMetadata.js b/src/sharedMetadata.js index 7c33bc813..49e101252 100644 --- a/src/sharedMetadata.js +++ b/src/sharedMetadata.js @@ -1,15 +1,14 @@ -// var master = Blockchain.MyWallet.wallet.hdwallet.getMasterHDNode() -// var m = Blockchain.SharedMetadata.fromMasterHDNode(master) 'use strict'; const WalletCrypto = require('./wallet-crypto'); const Bitcoin = require('bitcoinjs-lib'); const crypto = require('crypto'); -const API = require('./api'); const MyWallet = require('./wallet'); const Contacts = require('./contacts'); const Helpers = require('./helpers'); const Metadata = require('./metadata'); +const jwtDecode = require('jwt-decode'); +const API = require('./sharedMetadataAPI'); import * as R from 'ramda' class SharedMetadata { @@ -21,131 +20,45 @@ class SharedMetadata { this._keyPair = mdidHDNode.keyPair; this._auth_token = null; this._sequence = Promise.resolve(); - this.authorize(); - } - - get mdid() { - return this._mdid; - } - get priv() { - return this._priv; } + get mdid() { return this._mdid; } + get node() { return this._node; } + get token() { return this._auth_token; } } -SharedMetadata.sign = function (keyPair, message) { - return Bitcoin.message.sign(keyPair, message) -}; - -SharedMetadata.verify = function (mdid, signature, message) { - return Bitcoin.message.verify (mdid, signature, message); -} +// should be overwritten by iOS +SharedMetadata.sign = Bitcoin.message.sign; +SharedMetadata.verify = Bitcoin.message.verify -SharedMetadata.request = function (method, endpoint, data, authToken) { - var url = API.API_ROOT_URL + 'metadata/' + endpoint; - var options = { - headers: { 'Content-Type': 'application/json' }, - credentials: 'omit' - }; - if (authToken) { - options.headers.Authorization = 'Bearer ' + authToken; +SharedMetadata.signChallenge = R.curry((key, r) => ( + { + nonce: r.nonce, + signature: SharedMetadata.sign(key, r.nonce).toString('base64'), + mdid: key.getAddress() } - - // encodeFormData :: Object -> url encoded params - var encodeFormData = function (data) { - if (!data) return ''; - var encoded = Object.keys(data).map(function (k) { - return encodeURIComponent(k) + '=' + encodeURIComponent(data[k]); - }).join('&'); - return encoded ? '?' + encoded : encoded; - }; - - if (data && data !== {}) { - if (method === 'GET') { - url += encodeFormData(data); - } else { - options.body = JSON.stringify(data); - } +)); + +SharedMetadata.getAuthToken = (mdidHDNode) => + API.getAuth().then(SharedMetadata.signChallenge(mdidHDNode.keyPair)) + .then(API.postAuth); + +SharedMetadata.isValidToken = (token) => { + try { + const decoded = jwtDecode(token); + var expDate = new Date(decoded.exp * 1000); + var now = new Date(); + return now < expDate; + } catch (e) { + return false; } - - options.method = method; - - var handleNetworkError = function (e) { - return Promise.reject({ error: 'SHARED_METADATA_CONNECT_ERROR', message: e }); - }; - - var checkStatus = function (response) { - if (response.ok) { - return response.json(); - } else { - return response.text().then(Promise.reject.bind(Promise)); - } - }; - - return fetch(url, options) - .catch(handleNetworkError) - .then(checkStatus); -}; - -SharedMetadata.getAuthToken = function (mdidHDNode) { - const S = SharedMetadata; - const mdid = mdidHDNode.getAddress(); - const key = mdidHDNode.keyPair; - return S.request('GET','auth') - .then( (r) => ({ nonce: r.nonce - , signature: S.sign(key, r.nonce).toString('base64') - , mdid: mdid})) - .then( (d) => S.request('POST', 'auth' , d)) - .then( (r) => r.token); }; SharedMetadata.prototype.authorize = function () { - return this.next(() => { - return SharedMetadata.getAuthToken(this._node) - .then((token) => { - this._auth_token = token; - return token; - }) - }); -} -SharedMetadata.prototype.getMessages = function (onlyNew) { - return this.next( - SharedMetadata.request.bind(this, 'GET', 'messages', onlyNew ? {new: true} : {}, this._auth_token) - ); -}; - -SharedMetadata.prototype.getMessage = function (id) { - return this.next( - SharedMetadata.request.bind(this, 'GET', 'message/' + id, null, this._auth_token) - ); -}; - -SharedMetadata.prototype.processMessage = function (id) { - return this.next( - SharedMetadata.request.bind(this, 'PUT', 'message/' + id + '/processed', null, this._auth_token) - ); -}; - -SharedMetadata.prototype.trustContact = function (contactMdid) { - return this.next( - SharedMetadata.request.bind(this, 'PUT', 'trusted/' + contactMdid, null, this._auth_token) - ); -}; - -SharedMetadata.prototype.getTrustedList = function () { - return this.next( - SharedMetadata.request.bind(this, 'GET', 'trusted', null, this._auth_token) - ); -}; - -SharedMetadata.prototype.getTrusted = function (contactMdid) { - return this.next( - SharedMetadata.request.bind(this, 'GET', 'trusted/' + contactMdid, null, this._auth_token) - ); -}; - -SharedMetadata.prototype.removeContact = function (contactMdid) { + const saveToken = (r) => { this._auth_token = r.token; return r.token; }; return this.next( - SharedMetadata.request.bind(this, 'DELETE', 'trusted/' + contactMdid, null, this._auth_token) + () => SharedMetadata.isValidToken(this.token) + ? Promise.resolve(this.token) + : SharedMetadata.getAuthToken(this.node).then(saveToken) ); }; @@ -155,7 +68,6 @@ SharedMetadata.prototype.removeContact = function (contactMdid) { // type: type, // payload: encrypted, // signature: this.sign(encrypted), -// // sender: this.mdid, // recipient: mdidRecipient // }; // return this.request('POST', 'messages', body); @@ -207,32 +119,33 @@ SharedMetadata.prototype.removeContact = function (contactMdid) { // return msgP.then(f.bind(this)); // }; // -SharedMetadata.prototype.publishXPUB = function () { - return this.next(() => { - var myDirectory = new Metadata(this._keyPair); - myDirectory.fetch(); - return myDirectory.update({xpub: this._xpub}); - }); -}; +// SharedMetadata.prototype.publishXPUB = function () { +// return this.next(() => { +// var myDirectory = new Metadata(this._keyPair); +// myDirectory.fetch(); +// return myDirectory.update({xpub: this._xpub}); +// }); +// }; +// +// SharedMetadata.prototype.getXPUB = function (contactMDID) { +// return this.next(Metadata.read.bind(undefined, contactMDID)); +// }; -SharedMetadata.prototype.getXPUB = function (contactMDID) { - return this.next(Metadata.read.bind(undefined, contactMDID)); -}; // createInvitation :: Promise InvitationID SharedMetadata.prototype.createInvitation = function () { - return this.next(SharedMetadata.request.bind(this, 'POST', 'share', undefined, this._auth_token)); + return this.authorize().then((t) => this.next(API.createInvitation.bind(null, t))); }; // readInvitation :: String -> Promise RequesterID -SharedMetadata.prototype.readInvitation = function (id) { - return this.next(SharedMetadata.request.bind(this, 'GET', 'share/' + id, undefined, this._auth_token)); +SharedMetadata.prototype.readInvitation = function (uuid) { + return this.authorize().then((t) => this.next(API.readInvitation.bind(null, t, uuid))); }; // acceptInvitation :: String -> Promise () -SharedMetadata.prototype.acceptInvitation = function (id) { - return this.next(SharedMetadata.request.bind(this, 'POST', 'share/' + id, undefined, this._auth_token)); +SharedMetadata.prototype.acceptInvitation = function (uuid) { + return this.authorize().then((t) => this.next(API.acceptInvitation.bind(null, t, uuid))); }; // deleteInvitation :: String -> Promise () -SharedMetadata.prototype.deleteInvitation = function (id) { - return this.next(SharedMetadata.request.bind(this, 'DELETE', 'share/' + id, undefined, this._auth_token)); +SharedMetadata.prototype.deleteInvitation = function (uuid) { + return this.authorize().then((t) => this.next(API.deleteInvitation.bind(null, t, uuid))); }; SharedMetadata.fromMDIDHDNode = function (mdidHDNode) { @@ -240,7 +153,6 @@ SharedMetadata.fromMDIDHDNode = function (mdidHDNode) { }; SharedMetadata.fromMasterHDNode = function (masterHDNode) { - // var masterHDNode = MyWallet.wallet.hdwallet.getMasterHDNode(cipher); var hash = WalletCrypto.sha256('info.blockchain.mdid'); var purpose = hash.slice(0, 4).readUInt32BE(0) & 0x7FFFFFFF; var mdidHDNode = masterHDNode.deriveHardened(purpose); @@ -249,7 +161,7 @@ SharedMetadata.fromMasterHDNode = function (masterHDNode) { SharedMetadata.prototype.next = function (f) { var nextInSeq = this._sequence.then(f); - this._sequence = nextInSeq.then(Helpers.noop, Helpers.noop); + this._sequence = nextInSeq.then(x => x, x => x); return nextInSeq; }; diff --git a/src/sharedMetadataAPI.js b/src/sharedMetadataAPI.js new file mode 100644 index 000000000..437845223 --- /dev/null +++ b/src/sharedMetadataAPI.js @@ -0,0 +1,68 @@ +'use strict'; +const API = require('./api'); +const S = {}; + +S.request = function (method, endpoint, data, authToken) { + var url = API.API_ROOT_URL + 'metadata/' + endpoint; + var options = { + headers: { 'Content-Type': 'application/json' }, + credentials: 'omit' + }; + if (authToken) { + options.headers.Authorization = 'Bearer ' + authToken; + } + // encodeFormData :: Object -> url encoded params + var encodeFormData = function (data) { + if (!data) return ''; + var encoded = Object.keys(data).map(function (k) { + return encodeURIComponent(k) + '=' + encodeURIComponent(data[k]); + }).join('&'); + return encoded ? '?' + encoded : encoded; + }; + if (data && data !== {}) { + if (method === 'GET') { + url += encodeFormData(data); + } else { + options.body = JSON.stringify(data); + } + } + options.method = method; + var handleNetworkError = function (e) { + return Promise.reject({ error: 'SHARED_METADATA_CONNECT_ERROR', message: e }); + }; + var checkStatus = function (response) { + if (response.ok) { + return response.json(); + } else { + return response.text().then(Promise.reject.bind(Promise)); + } + }; + return fetch(url, options) + .catch(handleNetworkError) + .then(checkStatus); +}; + +// authentication +S.getAuth = () => S.request('GET', 'auth'); +S.postAuth = (data) => S.request('POST', 'auth', data); + +// messages +S.getMessages = (token, onlyNew) => S.request('GET', 'messages', onlyNew ? {new: true} : {}, token); +S.getMessage = (token, uuid) => S.request('GET', 'message/' + uuid, null, token); +S.sendMessage = (token, recipient, payload, signature, type) => + S.request('POST', 'messages', {type, payload, signature, recipient}, token); +S.processMessage = (token, uuid) => S.request('PUT', 'message/' + uuid + '/processed', null, token); + +// trusted contact list +S.addTrusted = (token, mdid) => S.request('PUT', 'trusted/' + mdid, null, token); +S.getTrusted = (token, mdid) => S.request('GET', 'trusted/' + mdid, null, token); +S.deleteTrusted = (token, mdid) => S.request('DELETE', 'trusted/' + mdid, null, mdid); +S.getTrustedList = (token) => S.request('GET', 'trusted', null, token); + +// invitation process +S.createInvitation = (token) => S.request('POST', 'share', null, token); +S.readInvitation = (token, uuid) => S.request('GET', 'share/' + uuid, null, token); +S.acceptInvitation = (token, uuid) => S.request('POST', 'share/' + uuid, null, token); +S.deleteInvitation = (token, uuid) => S.request('DELETE', 'share/' + uuid, null, token); + +module.exports = S;