diff --git a/src/js/ripple/blob.js b/src/js/ripple/blob.js index bb2bb39f..5024bc96 100644 --- a/src/js/ripple/blob.js +++ b/src/js/ripple/blob.js @@ -119,7 +119,9 @@ BlobObj.prototype.init = function(fn) { self.revision = resp.body.revision; self.encrypted_secret = resp.body.encrypted_secret; + self.identity_id = resp.body.identity_id; self.missing_fields = resp.body.missing_fields; + //self.attestations = resp.body.attestation_summary; if (!self.decrypt(resp.body.blob)) { return fn(new Error('Error while decrypting blob')); @@ -561,7 +563,6 @@ BlobObj.prototype.get2FA = function (fn) { * @params {boolean} options.enabled * @params {string} options.phone * @params {string} options.country_code - * @params {string} options.via //sms, etc */ BlobObj.prototype.set2FA = function(options, fn) { @@ -572,8 +573,7 @@ BlobObj.prototype.set2FA = function(options, fn) { data : { enabled : options.enabled, phone : options.phone, - country_code : options.country_code, - via : options.via + country_code : options.country_code } }; @@ -1115,48 +1115,6 @@ BlobClient.recoverBlob = function (opts, fn) { }; }; -/** - * updateProfile - * update information stored outside the blob - HMAC signed - * @param {object} - * @param {string} opts.url - * @param {string} opts.username - * @param {string} opts.auth_secret - * @param {srring} opts.blob_id - * @param {object} opts.profile - * @param {string} opts.profile.phone - optional - * @param {string} opts.profile.country - optional - * @param {string} opts.profile.region - optional - * @param {string} opts.profile.city - optional - */ - -BlobClient.updateProfile = function (opts, fn) { - var config = { - method: 'POST', - url: opts.url + '/v1/user/' + opts.username + '/profile', - dataType: 'json', - data: opts.profile - }; - - var signedRequest = new SignedRequest(config); - var signed = signedRequest.signHmac(opts.auth_secret, opts.blob_id); - - request.post(signed.url) - .send(signed.data) - .end(function(err, resp) { - if (err) { - log.error('updateProfile:', err); - fn(new Error('Failed to update profile - XHR error')); - } else if (resp.body && resp.body.result === 'success') { - fn(null, resp.body); - } else if (resp.body) { - log.error('updateProfile:', resp.body); - } else { - fn(new Error('Failed to update profile')); - } - }); - -}; /** * updateKeys @@ -1320,6 +1278,7 @@ BlobClient.create = function(options, fn) { if (err) { fn(err); } else if (resp.body && resp.body.result === 'success') { + blob.identity_id = resp.body.identity_id; fn(null, blob, resp.body); } else if (resp.body && resp.body.result === 'error') { fn(new Error(resp.body.message)); @@ -1363,4 +1322,263 @@ BlobClient.deleteBlob = function(options, fn) { }); }; +/*** identity related functions ***/ + +/** + * updateProfile + * update information stored outside the blob - HMAC signed + * @param {object} + * @param {string} opts.url + * @param {string} opts.auth_secret + * @param {srring} opts.blob_id + * @param {object} opts.profile + * @param {array} opts.profile.attributes (optional, array of attribute objects) + * @param {array} opts.profile.addresses (optional, array of address objects) + * + * @param {string} attribute.id ... id of existing attribute + * @param {string} attribute.name ... attribute name i.e. ripple_address + * @param {string} attribute.type ... optional, sub-type of attribute + * @param {string} attribute.value ... value of attribute + * @param {string} attribute.domain ... corresponding domain + * @param {string} attribute.status ... “current”, “removed”, etc. + * @param {string} attribute.visibitlity ... “public”, ”private” + */ + +BlobClient.updateProfile = function (opts, fn) { + var config = { + method: 'POST', + url: opts.url + '/v1/profile/', + dataType: 'json', + data: opts.profile + }; + + var signedRequest = new SignedRequest(config); + var signed = signedRequest.signHmac(opts.auth_secret, opts.blob_id); + + request.post(signed.url) + .send(signed.data) + .end(function(err, resp) { + + if (err) { + log.error('updateProfile:', err); + fn(new Error('Failed to update profile - XHR error')); + } else if (resp.body && resp.body.result === 'success') { + fn(null, resp.body); + } else if (resp.body) { + log.error('updateProfile:', resp.body); + fn(new Error('Failed to update profile')); + } else { + fn(new Error('Failed to update profile')); + } + }); +}; + +/** + * getProfile + * @param {Object} opts + * @param {string} opts.url + * @param {string} opts.auth_secret + * @param {srring} opts.blob_id + */ + +BlobClient.getProfile = function (opts, fn) { + var config = { + method: 'GET', + url: opts.url + '/v1/profile/' + }; + + var signedRequest = new SignedRequest(config); + var signed = signedRequest.signHmac(opts.auth_secret, opts.blob_id); + + request.get(signed.url) + .send(signed.data) + .end(function(err, resp) { + + if (err) { + log.error('getProfile:', err); + fn(new Error('Failed to get profile - XHR error')); + } else if (resp.body && resp.body.result === 'success') { + fn(null, resp.body); + } else if (resp.body) { + log.error('getProfile:', resp.body); + fn(new Error('Failed to get profile')); + } else { + fn(new Error('Failed to get profile')); + } + }); +}; + +/** + * getAttestation + * @param {Object} opts + * @param {string} opts.url + * @param {string} opts.auth_secret + * @param {string} opts.blob_id + * @param {string} opts.type (email,phone,basic_identity) + * @param {object} opts.phone (required for type 'phone') + * @param {string} opts.email (required for type 'email') + */ + +BlobClient.getAttestation = function (opts, fn) { + var params = { }; + + if (opts.phone) params.phone = opts.phone; + if (opts.email) params.email = opts.email; + + var config = { + method: 'POST', + url: opts.url + '/v1/attestation/' + opts.type, + dataType: 'json', + data: params + }; + + var signedRequest = new SignedRequest(config); + var signed = signedRequest.signHmac(opts.auth_secret, opts.blob_id); + + request.post(signed.url) + .send(signed.data) + .end(function(err, resp) { + + if (err) { + log.error('attest:', err); + fn(new Error('attestation error - XHR error')); + } else if (resp.body && resp.body.result === 'success') { + if (resp.body.attestation) { + resp.body.decoded = BlobClient.parseAttestation(resp.body.attestation); + } + + fn(null, resp.body); + } else if (resp.body) { + log.error('attestation:', resp.body); + fn(new Error('attestation error: ' + resp.body.message || "")); + } else { + fn(new Error('attestation error')); + } + }); +}; + +/** + * getAttestationSummary + * @param {Object} opts + * @param {string} opts.url + * @param {string} opts.auth_secret + * @param {string} opts.blob_id + */ + +BlobClient.getAttestationSummary = function (opts, fn) { + + + var config = { + method: 'GET', + url: opts.url + '/v1/attestation/summary', + dataType: 'json' + }; + + if (opts.full) config.url += '?full=true'; + + var signedRequest = new SignedRequest(config); + var signed = signedRequest.signHmac(opts.auth_secret, opts.blob_id); + + request.get(signed.url) + .send(signed.data) + .end(function(err, resp) { + + if (err) { + log.error('attest:', err); + fn(new Error('attestation error - XHR error')); + } else if (resp.body && resp.body.result === 'success') { + if (resp.body.attestation) { + resp.body.decoded = BlobClient.parseAttestation(resp.body.attestation); + } + + fn(null, resp.body); + } else if (resp.body) { + log.error('attestation:', resp.body); + fn(new Error('attestation error: ' + resp.body.message || "")); + } else { + fn(new Error('attestation error')); + } + }); +}; + +/** + * updateAttestation + * @param {Object} opts + * @param {string} opts.url + * @param {string} opts.auth_secret + * @param {string} opts.blob_id + * @param {string} opts.type (email,phone,profile,identity) + * @param {object} opts.phone (required for type 'phone') + * @param {object} opts.profile (required for type 'profile') + * @param {string} opts.email (required for type 'email') + * @param {string} opts.answers (required for type 'identity') + * @param {string} opts.token (required for completing email or phone attestations) + */ + +BlobClient.updateAttestation = function (opts, fn) { + + var params = { }; + + if (opts.phone) params.phone = opts.phone; + if (opts.profile) params.profile = opts.profile; + if (opts.email) params.email = opts.email; + if (opts.token) params.token = opts.token; + if (opts.answers) params.answers = opts.answers; + + var config = { + method: 'POST', + url: opts.url + '/v1/attestation/' + opts.type + '/update', + dataType: 'json', + data: params + }; + + var signedRequest = new SignedRequest(config); + var signed = signedRequest.signHmac(opts.auth_secret, opts.blob_id); + + request.post(signed.url) + .send(signed.data) + .end(function(err, resp) { + + if (err) { + log.error('attest:', err); + fn(new Error('attestation error - XHR error')); + } else if (resp.body && resp.body.result === 'success') { + if (resp.body.attestation) { + resp.body.decoded = BlobClient.parseAttestation(resp.body.attestation); + } + + fn(null, resp.body); + } else if (resp.body) { + log.error('attestation:', resp.body); + fn(new Error('attestation error: ' + resp.body.message || "")); + } else { + fn(new Error('attestation error')); + } + }); +}; + +/** + * parseAttestation + * @param {Object} attestation + */ + +BlobClient.parseAttestation = function (attestation) { + var segments = attestation.split('.'); + var decoded; + + // base64 decode and parse JSON + try { + decoded = { + header : JSON.parse(crypt.decodeBase64(segments[0])), + payload : JSON.parse(crypt.decodeBase64(segments[1])), + signature : segments[2] + }; + + } catch (e) { + console.log("invalid attestation:", e); + } + + return decoded; +}; + exports.BlobClient = BlobClient; diff --git a/src/js/ripple/crypt.js b/src/js/ripple/crypt.js index 6395f77c..c6d29665 100644 --- a/src/js/ripple/crypt.js +++ b/src/js/ripple/crypt.js @@ -322,4 +322,12 @@ Crypt.base64UrlToBase64 = function(encodedData) { return encodedData; }; +/** + * base64 to UTF8 + */ + +Crypt.decodeBase64 = function (data) { + return sjcl.codec.utf8String.fromBits(sjcl.codec.base64.toBits(data)); +} + exports.Crypt = Crypt; diff --git a/src/js/ripple/vaultclient.js b/src/js/ripple/vaultclient.js index ec7bc650..f783f45f 100644 --- a/src/js/ripple/vaultclient.js +++ b/src/js/ripple/vaultclient.js @@ -575,8 +575,6 @@ VaultClient.prototype.generateDeviceID = function () { VaultClient.prototype.resendEmail = blobClient.resendEmail; -VaultClient.prototype.updateProfile = blobClient.updateProfile; - VaultClient.prototype.recoverBlob = blobClient.recoverBlob; VaultClient.prototype.deleteBlob = blobClient.deleteBlob; @@ -585,5 +583,11 @@ VaultClient.prototype.requestToken = blobClient.requestToken; VaultClient.prototype.verifyToken = blobClient.verifyToken; +VaultClient.prototype.getAttestation = blobClient.getAttestation; + +VaultClient.prototype.updateAttestation = blobClient.updateAttestation; + +VaultClient.prototype.getAttestationSummary = blobClient.getAttestationSummary; + //export by name exports.VaultClient = VaultClient; diff --git a/test/vault-test.js b/test/vault-test.js index 382d6116..0aaf9a7a 100644 --- a/test/vault-test.js +++ b/test/vault-test.js @@ -20,6 +20,7 @@ var exampleData = { email_token : '77825040-9096-4695-9cbc-76720f6a8649', activateLink : 'https://staging.ripple.com/client/#/register/activate/', device_id : "ac1b6f6dbca98190eb9687ba06f0e066", + identity_id : "17fddb71-a5c2-44ce-8b50-4b381339d4f2", blob: { url: 'https://id.staging.ripple.com', id: 'ef203d3e76552c0592384f909e6f61f1d1f02f61f07643ce015d8b0c9710dd2f', @@ -103,12 +104,29 @@ var recoverRes = { result: 'success' } } - + +var getProfileRes = { + "result":"success", + "addresses":[], + "attributes":[{ + "attribute_id":"4034e477-ffc9-48c4-bcbc-058293f081d8", + "identity_id":"17fddb71-a5c2-44ce-8b50-4b381339d4f2", + "name":"email", + "type":"default", + "domain":null, + "value":"example@example.com", + "visibility":"public", + "updated":null + } + ] +}; + var blob = new Blob(); blob.url = exampleData.blob.url; blob.id = exampleData.blob.id; blob.device_id = exampleData.device_id; blob.key = exampleData.blob.key; + blob.identity_id = exampleData.blob.identity_id; blob.data = exampleData.blob.data; blob.revision = exampleData.blob.data.revision; @@ -133,14 +151,12 @@ var mockDelete; if (!online) { mockRippleTxt = nock('https://ripple.com') - .persist() .get('/ripple.txt') .reply(200, rippleTxtRes, { 'Content-Type': 'text/plain' }); mockRippleTxt2 = nock('https://' + exampleData.domain) - .persist() .get('/ripple.txt') .reply(200, rippleTxtRes, { 'Content-Type': 'text/plain' @@ -153,21 +169,21 @@ if (!online) { 'Content-Type': 'text/plain' }); - mockRegister = nock('https://id.staging.ripple.com').persist(); + mockRegister = nock('https://id.staging.ripple.com'); mockRegister.filteringPath(/(v1\/user\?signature(.+))/g, 'register/') .post('/register/') .reply(200, { result: 'error', message: 'User already exists' }, { 'Content-Type': 'application/json' }); - mockDelete = nock('https://id.staging.ripple.com').persist(); + mockDelete = nock('https://id.staging.ripple.com'); mockDelete.filteringPath(/(v1\/user\/(.+))/g, 'delete/') .delete('/delete/') .reply(200, { result: 'success' }, { 'Content-Type': 'application/json' }); - mockBlob = nock('https://id.staging.ripple.com').persist(); + mockBlob = nock('https://id.staging.ripple.com'); mockBlob.get('/v1/authinfo?domain=' + exampleData.domain + '&username=' + exampleData.username.toLowerCase()) .reply(200, JSON.stringify(authInfoRes.body), { 'Content-Type': 'application/json' @@ -185,47 +201,40 @@ if (!online) { 'Content-Type': 'application/json' }); - mockRename = nock('https://id.staging.ripple.com/v1/user/').persist(); + mockRename = nock('https://id.staging.ripple.com/v1/user/'); mockRename.filteringPath(/((.+)\/rename(.+))/g, 'rename/') .post('rename/') .reply(200, {result:'success',message:'rename'}, { 'Content-Type': 'application/json' }); - mockUpdate = nock('https://id.staging.ripple.com/v1/user/').persist(); - mockUpdate.filteringPath(/((.+)\/update(.+))/g, 'update/') + mockUpdate = nock('https://id.staging.ripple.com/v1/user/'); + mockUpdate.filteringPath(/((.+)\/updatekeys(.+))/g, 'update/') .post('update/') .reply(200, {result:'success',message:'updateKeys'}, { 'Content-Type': 'application/json' }); - mockRecover = nock('https://id.staging.ripple.com/').persist(); + mockRecover = nock('https://id.staging.ripple.com/') mockRecover.filteringPath(/((.+)user\/recov\/(.+))/g, 'recov/') .get('recov/') .reply(200, recoverRes.body, { 'Content-Type': 'application/json' }); - - mockVerify = nock('https://id.staging.ripple.com/v1/user/').persist(); + + mockVerify = nock('https://id.staging.ripple.com/v1/user/'); mockVerify.filteringPath(/((.+)\/verify(.+))/g, 'verify/') .get('verify/') .reply(200, {result:'error', message:'invalid token'}, { 'Content-Type': 'application/json' }); - mockEmail = nock('https://id.staging.ripple.com/v1/user').persist(); + mockEmail = nock('https://id.staging.ripple.com/v1/user'); mockEmail.filteringPath(/((.+)\/email(.+))/g, 'email/') .post('email/') .reply(200, {result:'success'}, { 'Content-Type': 'application/json' }); - - mockProfile = nock('https://id.staging.ripple.com/v1/user').persist(); - mockProfile.filteringPath(/((.+)\/profile(.+))/g, 'profile/') - .post('profile/') - .reply(200, {result:'success'}, { - 'Content-Type': 'application/json' - }); } describe('Ripple Txt', function () { @@ -419,6 +428,7 @@ describe('VaultClient', function () { }); }); +/* describe('#updateProfile', function () { it('should update profile parameters associated with a blob', function (done) { this.timeout(10000); @@ -442,7 +452,7 @@ describe('VaultClient', function () { }); }); }); - +*/ }); @@ -693,7 +703,93 @@ describe('Blob', function () { }); }); }); - + + describe('identityVault', function() { + it('#identity - Get Attestation', function (done) { + var options = { + url : blob.url, + auth_secret : blob.data.auth_secret, + blob_id : blob.id, + }; + + options.type = 'identity'; + + nock('https://id.staging.ripple.com') + .filteringPath(/(v1\/attestation\/identity(.+))/g, '') + .post('/') + .reply(200, { + result: 'success', + status: 'verified', + attestation: 'eyJ6IjoieiJ9.eyJ6IjoieiJ9.sig', + blinded:'eyJ6IjoieiJ9.eyJ6IjoieiJ9.sig' + }, {'Content-Type': 'application/json'}); + + client.getAttestation(options, function(err, resp) { + assert.ifError(err); + assert.strictEqual(resp.result, 'success'); + assert.strictEqual(typeof resp.attestation, 'string'); + assert.strictEqual(typeof resp.blinded, 'string'); + assert.deepEqual(resp.decoded, {"header":{"z":"z"},"payload":{"z":"z"},"signature":"sig"}) + done(); + }); + }); + + it('#identity - Update Attestation', function (done) { + + var options = { + url : blob.url, + auth_secret : blob.data.auth_secret, + blob_id : blob.id, + }; + + options.type = 'identity'; + + nock('https://id.staging.ripple.com') + .filteringPath(/(v1\/attestation\/identity\/update(.+))/g, '') + .post('/') + .reply(200, { + result: 'success', + status: 'verified', + attestation: 'eyJ6IjoieiJ9.eyJ6IjoieiJ9.sig', + blinded:'eyJ6IjoieiJ9.eyJ6IjoieiJ9.sig' + }, {'Content-Type': 'application/json'}); + + client.updateAttestation(options, function(err, resp) { + assert.ifError(err); + assert.strictEqual(resp.result, 'success'); + assert.strictEqual(typeof resp.attestation, 'string'); + assert.strictEqual(typeof resp.blinded, 'string'); + assert.deepEqual(resp.decoded, {"header":{"z":"z"},"payload":{"z":"z"},"signature":"sig"}) + done(); + }); + }); + + it('#identity - Get Attestation Summary', function (done) { + + var options = { + url : blob.url, + auth_secret : blob.data.auth_secret, + blob_id : blob.id, + }; + + nock('https://id.staging.ripple.com') + .filteringPath(/(v1\/attestation\/summary(.+))/g, '') + .get('/') + .reply(200, { + result: 'success', + attestation: 'eyJ6IjoieiJ9.eyJ6IjoieiJ9.sig', + }, {'Content-Type': 'application/json'}); + + client.getAttestationSummary(options, function(err, resp) { + assert.ifError(err); + assert.strictEqual(resp.result, 'success'); + assert.strictEqual(typeof resp.attestation, 'string'); + assert.deepEqual(resp.decoded, {"header":{"z":"z"},"payload":{"z":"z"},"signature":"sig"}) + done(); + }); + }); + }); + //only do these offline if (!online) {