diff --git a/package.json b/package.json index cccad15f6..d059e0940 100644 --- a/package.json +++ b/package.json @@ -150,6 +150,7 @@ "rimraf": "2.6.2", "sanitize.css": "^12.0.1", "semver": "5.4.1", + "session_util_wrapper": "https://github.com/oxen-io/libsession-util-nodejs", "styled-components": "5.1.1", "uuid": "8.3.2" }, diff --git a/ts/test/session/unit/libsession_wrapper/libsession_wrapper_test.ts b/ts/test/session/unit/libsession_wrapper/libsession_wrapper_test.ts new file mode 100644 index 000000000..a8d115ec2 --- /dev/null +++ b/ts/test/session/unit/libsession_wrapper/libsession_wrapper_test.ts @@ -0,0 +1,222 @@ +import { expect } from 'chai'; + +import * as SessionUtilWrapper from 'session_util_wrapper'; +import { stringToUint8Array } from '../../../../session/utils/String'; +import { from_hex, to_hex } from 'libsodium-wrappers-sumo'; +import { concatUInt8Array } from '../../../../session/crypto'; + +describe('libsession_wrapper', () => { + it.skip('[config][user_profile][c]', () => { + // Note: To run this test, you need to compile the libsession wrapper for node (and not for electron). + // To do this, you can cd to the node_module/libsession_wrapper folder and do + // yarn configure && yarn build + + const edSecretKey = from_hex( + '0123456789abcdef0123456789abcdef000000000000000000000000000000004cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7' + ); + + // Initialize a brand new, empty config because we have no dump data to deal with. + const conf = new SessionUtilWrapper.UserConfigWrapper(edSecretKey, null); + + // We don't need to push anything, since this is an empty config + expect(conf.needsPush()).to.be.eql(false); + expect(conf.needsDump()).to.be.eql(false); + + // Since it's empty there shouldn't be a name. + expect(conf.getName()).to.be.null; + + let pushResult = conf.push(); + + expect(pushResult.seqno).to.be.eq(0); + expect(pushResult.data.length).to.be.eq(256); + + expect(conf.encryptionDomain()).to.be.eq('UserProfile'); + expect(conf.storageNamespace()).to.be.eq(2); + expect(to_hex(pushResult.data)).to.be.deep.eq( + '9ffb5347e061ac40d937ae4f1a890031475bdc11653f94c8ae1d516ffda71d9ee9cdaf9fbaeb15d835cdc7b3b6ecc120361f004ff172dd5e757c80ede10e88945536e6841255a7bca73664ab8a0607fcfe2579c05bb3d9d4b34ac1de2921e703783ce39e317a512cb9d4e3b59176cbde47b5ba24a03065bf8fefe3e8ca2609e0ad10c7c9c3f81dc6d3a399bda0c190e8a228d0acb22863ab84c2d0c411be74dac4de1f8bc18539635db01ea1ef7f28e505703d67786cb419690edd4bd8c92926fc1d6449eaccc31d7d9639e1b36222e5672b87d1e34b7860308c3f40b3997f39fecf6ceb889323826fa69e001816307799fc9fed302a90faa1e43f7cd7367c3c' + ); + + // This should also be unset: + const picResult = conf.getProfilePic(); + expect(picResult.url).to.be.null; + expect(picResult.key).to.be.null; + + // Now let's go set a profile name and picture: + conf.setProfilePic('http://example.org/omg-pic-123.bmp', stringToUint8Array('secret')); + conf.setName('Kallie'); + + // Retrieve them just to make sure they set properly: + const name = conf.getName(); + + expect(name).to.be.not.null; + expect(name).to.be.eq('Kallie'); + + const picture = conf.getProfilePic(); + + expect(picture.url).to.be.eq('http://example.org/omg-pic-123.bmp'); + expect(picture.key).to.be.deep.eq(stringToUint8Array('secret')); + + // Since we've made changes, we should need to push new config to the swarm, *and* should need + // to dump the updated state: + + expect(conf.needsDump()).to.be.true; + expect(conf.needsPush()).to.be.true; + + // incremented since we made changes (this only increments once between + // dumps; even though we changed two fields here). + pushResult = conf.push(); + expect(pushResult.seqno).to.be.eq(1); + + const exp_hash0 = from_hex('ea173b57beca8af18c3519a7bbf69c3e7a05d1c049fa9558341d8ebb48b0c965'); + + // prettier-ignore + const exp_push1_start = "d" + + "1:#" +"i1e"+ + "1:&" +"d"+ + "1:n" +"6:Kallie"+ + "1:p" +"34:http://example.org/omg-pic-123.bmp"+ + "1:q" +"6:secret"+ + "e"+ + "1:<"+ "l"+ + "l" +"i0e" +"32:" ; + // prettier-ignore + const exp_push1_end = "de"+ "e"+ + "e"+ + "1:="+ "d"+ + "1:n" +"0:"+ + "1:p" +"0:"+ + "1:q" +"0:"+ + "e"+ + "e"; + + // The data to be actually pushed, expanded like this to make it somewhat human-readable: + const exp_push1_decrypted = concatUInt8Array( + stringToUint8Array(exp_push1_start), + exp_hash0, + stringToUint8Array(exp_push1_end) + ); + const exp_push1_encrypted = from_hex( + 'a2952190dcb9797bc48e48f6dc7b3254d004bde9091cfc9ec3433cbc5939a3726deb04f58a546d7d79e6f80ea185d43bf93278398556304998ae882304075c77f15c67f9914c4d10005a661f29ff7a79e0a9de7f21725ba3b5a6c19eaa3797671b8fa4008d62e9af2744629cbb46664c4d8048e2867f66ed9254120371bdb24e95b2d92341fa3b1f695046113a768ceb7522269f937ead5591bfa8a5eeee3010474002f2db9de043f0f0d1cfb1066a03e7b5d6cfb70a8f84a20cd2df5a510cd3d175708015a52dd4a105886d916db0005dbea5706e5a5dc37ffd0a0ca2824b524da2e2ad181a48bb38e21ed9abe136014a4ee1e472cb2f53102db2a46afa9d68' + ); + + expect(to_hex(pushResult.data)).to.be.deep.eq(to_hex(exp_push1_encrypted)); + + // We haven't dumped, so still need to dump: + expect(conf.needsDump()).to.be.true; + // We did call push, but we haven't confirmed it as stored yet, so this will still return true: + expect(conf.needsPush()).to.be.true; + + let dumped = conf.dump(); + // (in a real client we'd now store this to disk) + expect(conf.needsDump()).to.be.false; + + // prettier-ignore + const expected_dump = concatUInt8Array(stringToUint8Array("d" + + "1:!" +"i2e"+ + "1:$" + `${exp_push1_decrypted.length}` + ':'), exp_push1_decrypted, stringToUint8Array("e")); + expect(to_hex(dumped)).to.be.deep.eq(to_hex(expected_dump)); + + // So now imagine we got back confirmation from the swarm that the push has been stored: + conf.confirmPushed(pushResult.seqno); + expect(conf.needsPush()).to.be.false; + expect(conf.needsDump()).to.be.true; + + conf.dump(); + expect(conf.needsDump()).to.be.false; + + // Now we're going to set up a second, competing config object (in the real world this would be + // another Session client somewhere). + + // Start with an empty config, as above: + + const conf2 = new SessionUtilWrapper.UserConfigWrapper(edSecretKey, null); + + expect(conf2.needsDump()).to.be.false; + + // Now imagine we just pulled down the encrypted string from the swarm; we merge it into conf2: + const accepted = conf2.merge([exp_push1_encrypted]); + expect(accepted).to.be.eq(1); + + // Our state has changed, so we need to dump: + expect(conf2.needsDump()).to.be.true; + conf2.dump(); + // (store in db) + expect(conf2.needsDump()).to.be.false; + + // We *don't* need to push: even though we updated, all we did is update to the merged data (and + // didn't have any sort of merge conflict needed): + expect(conf2.needsPush()).to.be.false; + + // Now let's create a conflicting update: + // Change the name on both clients: + conf.setName('Nibbler'); + conf2.setName('Raz'); + + // And, on conf2, we're also going to change the profile pic: + conf2.setProfilePic('http://new.example.com/pic', stringToUint8Array('qwert\0yuio')); + + // Both have changes, so push need a push + expect(conf.needsPush()).to.be.true; + expect(conf2.needsPush()).to.be.true; + pushResult = conf.push(); + expect(pushResult.seqno).to.be.eq(2); // incremented, since we made a field change + + let pushResult2 = conf2.push(); + expect(pushResult2.seqno).to.be.eq(2); // incremented, since we made a field change + + // (store in db) + conf.dump(); + conf2.dump(); + + // Since we set different things, we're going to get back different serialized data to be + // pushed: + expect(to_hex(pushResult.data)).to.not.be.deep.eq(to_hex(pushResult2.data)); + + // Now imagine that each client pushed its `seqno=2` config to the swarm, but then each client + // also fetches new messages and pulls down the other client's `seqno=2` value. + + // Feed the new config into each other. (This array could hold multiple configs if we pulled + // down more than one). + conf2.merge([pushResult.data]); + conf.merge([pushResult2.data]); + + // Now after the merge we *will* want to push from both client, since both will have generated a + // merge conflict update (with seqno = 3). + expect(conf.needsPush()).to.be.true; + expect(conf2.needsPush()).to.be.true; + + pushResult = conf.push(); + pushResult2 = conf2.push(); + expect(pushResult.seqno).to.be.eq(3); + expect(pushResult2.seqno).to.be.eq(3); + + // They should have resolved the conflict to the same thing: + expect(conf.getName()).to.be.eq('Nibbler'); + expect(conf2.getName()).to.be.eq('Nibbler'); + + // (Note that they could have also both resolved to "Raz" here, but the hash of the serialized + // message just happens to have a higher hash -- and thus gets priority -- for this particular + // test). + + // Since only one of them set a profile pic there should be no conflict there: + const pic = conf.getProfilePic(); + const pic2 = conf2.getProfilePic(); + expect(pic.url).to.be.eq('http://new.example.com/pic'); + expect(pic2.url).to.be.eq('http://new.example.com/pic'); + + expect(pic.key).to.be.deep.eq(stringToUint8Array('qwert\0yuio')); + expect(pic2.key).to.be.deep.eq(stringToUint8Array('qwert\0yuio')); + + conf.confirmPushed(pushResult.seqno); + conf2.confirmPushed(pushResult2.seqno); + + conf.dump(); + conf2.dump(); + // (store in db) + + expect(conf.needsPush()).to.be.false; + expect(conf.needsDump()).to.be.false; + expect(conf2.needsPush()).to.be.false; + expect(conf2.needsDump()).to.be.false; + }); +}); diff --git a/yarn.lock b/yarn.lock index 250d89e1a..24c053925 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2180,7 +2180,7 @@ abab@^2.0.5, abab@^2.0.6: resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291" integrity sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA== -abbrev@1: +abbrev@1, abbrev@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== @@ -6569,6 +6569,11 @@ nan@^2.14.0, nan@^2.14.2: resolved "https://registry.yarnpkg.com/nan/-/nan-2.15.0.tgz#3f34a473ff18e15c1b5626b62903b5ad6e665fee" integrity sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ== +nan@^2.17.0: + version "2.17.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.17.0.tgz#c0150a2368a182f033e9aa5195ec76ea41a199cb" + integrity sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ== + nano-css@^5.3.1: version "5.3.5" resolved "https://registry.yarnpkg.com/nano-css/-/nano-css-5.3.5.tgz#3075ea29ffdeb0c7cb6d25edb21d8f7fa8e8fe8e" @@ -6657,6 +6662,22 @@ node-gyp@9.0.0: tar "^6.1.2" which "^2.0.2" +node-gyp@^9.3.0: + version "9.3.0" + resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-9.3.0.tgz#f8eefe77f0ad8edb3b3b898409b53e697642b319" + integrity sha512-A6rJWfXFz7TQNjpldJ915WFb1LnhO4lIve3ANPbWreuEoLoKlFT3sxIepPBkLhM27crW8YmN+pjlgbasH6cH/Q== + dependencies: + env-paths "^2.2.0" + glob "^7.1.4" + graceful-fs "^4.2.6" + make-fetch-happen "^10.0.3" + nopt "^6.0.0" + npmlog "^6.0.0" + rimraf "^3.0.2" + semver "^7.3.5" + tar "^6.1.2" + which "^2.0.2" + node-loader@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/node-loader/-/node-loader-2.0.0.tgz#9109a6d828703fd3e0aa03c1baec12a798071562" @@ -6683,6 +6704,13 @@ nopt@^5.0.0: dependencies: abbrev "1" +nopt@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-6.0.0.tgz#245801d8ebf409c6df22ab9d95b65e1309cdb16d" + integrity sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g== + dependencies: + abbrev "^1.0.0" + nopt@~3.0.6: version "3.0.6" resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9" @@ -8175,6 +8203,14 @@ serialize-javascript@6.0.0: dependencies: randombytes "^2.1.0" +"session_util_wrapper@https://github.com/oxen-io/libsession-util-nodejs": + version "0.1.0" + resolved "https://github.com/oxen-io/libsession-util-nodejs#08bebd19042dee4d53f617bf93d51ab8fe9d8282" + dependencies: + bindings "^1.5.0" + nan "^2.17.0" + node-gyp "^9.3.0" + set-blocking@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"