diff options
-rw-r--r-- | javascript/.gitignore | 1 | ||||
-rw-r--r-- | javascript/olm_inbound_group_session.js | 68 | ||||
-rw-r--r-- | javascript/olm_outbound_group_session.js | 54 | ||||
-rw-r--r-- | javascript/olm_post.js | 172 | ||||
-rw-r--r-- | javascript/olm_pre.js | 30 | ||||
-rw-r--r-- | javascript/package.json | 7 | ||||
-rw-r--r-- | javascript/test/megolm.spec.js | 68 | ||||
-rw-r--r-- | javascript/test/olm.spec.js | 94 | ||||
-rwxr-xr-x | jenkins.sh | 1 |
9 files changed, 388 insertions, 107 deletions
diff --git a/javascript/.gitignore b/javascript/.gitignore index 603fe7c..ec22345 100644 --- a/javascript/.gitignore +++ b/javascript/.gitignore @@ -2,3 +2,4 @@ /node_modules /npm-debug.log /olm.js +/reports diff --git a/javascript/olm_inbound_group_session.js b/javascript/olm_inbound_group_session.js index 1b7fcfe..2e4727f 100644 --- a/javascript/olm_inbound_group_session.js +++ b/javascript/olm_inbound_group_session.js @@ -64,33 +64,51 @@ InboundGroupSession.prototype['create'] = restore_stack(function(session_key) { InboundGroupSession.prototype['decrypt'] = restore_stack(function( message ) { - var message_array = array_from_string(message); - var message_buffer = stack(message_array); - var max_plaintext_length = inbound_group_session_method( - Module['_olm_group_decrypt_max_plaintext_length'] - )(this.ptr, message_buffer, message_array.length); - // caculating the length destroys the input buffer. - // So we copy the array to a new buffer - var message_buffer = stack(message_array); - var plaintext_buffer = stack(max_plaintext_length + NULL_BYTE_PADDING_LENGTH); - var message_index = stack(4); - var plaintext_length = inbound_group_session_method(Module["_olm_group_decrypt"])( - this.ptr, - message_buffer, message_array.length, - plaintext_buffer, max_plaintext_length, - message_index - ); + var message_buffer, plaintext_buffer, plaintext_length; - // Pointer_stringify requires a null-terminated argument (the optional - // 'len' argument doesn't work for UTF-8 data). - Module['setValue']( - plaintext_buffer+plaintext_length, - 0, "i8" - ); + try { + message_buffer = malloc(message.length); + Module['writeAsciiToMemory'](message, message_buffer, true); + + var max_plaintext_length = inbound_group_session_method( + Module['_olm_group_decrypt_max_plaintext_length'] + )(this.ptr, message_buffer, message.length); + + // caculating the length destroys the input buffer, so we need to re-copy it. + Module['writeAsciiToMemory'](message, message_buffer, true); + + plaintext_buffer = malloc(max_plaintext_length + NULL_BYTE_PADDING_LENGTH); + var message_index = stack(4); - return { - "plaintext": Pointer_stringify(plaintext_buffer), - "message_index": Module['getValue'](message_index, "i32") + plaintext_length = inbound_group_session_method( + Module["_olm_group_decrypt"] + )( + this.ptr, + message_buffer, message.length, + plaintext_buffer, max_plaintext_length, + message_index + ); + + // UTF8ToString requires a null-terminated argument, so add the + // null terminator. + Module['setValue']( + plaintext_buffer+plaintext_length, + 0, "i8" + ); + + return { + "plaintext": UTF8ToString(plaintext_buffer), + "message_index": Module['getValue'](message_index, "i32") + } + } finally { + if (message_buffer !== undefined) { + free(message_buffer); + } + if (plaintext_buffer !== undefined) { + // don't leave a copy of the plaintext in the heap. + bzero(plaintext_buffer, plaintext_length + NULL_BYTE_PADDING_LENGTH); + free(plaintext_buffer); + } } }); diff --git a/javascript/olm_outbound_group_session.js b/javascript/olm_outbound_group_session.js index 88a441d..24ea644 100644 --- a/javascript/olm_outbound_group_session.js +++ b/javascript/olm_outbound_group_session.js @@ -63,20 +63,46 @@ OutboundGroupSession.prototype['create'] = restore_stack(function() { ); }); -OutboundGroupSession.prototype['encrypt'] = restore_stack(function(plaintext) { - var plaintext_array = array_from_string(plaintext); - var message_length = outbound_group_session_method( - Module['_olm_group_encrypt_message_length'] - )(this.ptr, plaintext_array.length); - var plaintext_buffer = stack(plaintext_array); - var message_buffer = stack(message_length + NULL_BYTE_PADDING_LENGTH); - outbound_group_session_method(Module['_olm_group_encrypt'])( - this.ptr, - plaintext_buffer, plaintext_array.length, - message_buffer, message_length - ); - return Pointer_stringify(message_buffer); -}); +OutboundGroupSession.prototype['encrypt'] = function(plaintext) { + var plaintext_buffer, message_buffer, plaintext_length; + try { + plaintext_length = Module['lengthBytesUTF8'](plaintext); + + var message_length = outbound_group_session_method( + Module['_olm_group_encrypt_message_length'] + )(this.ptr, plaintext_length); + + // need to allow space for the terminator (which stringToUTF8 always + // writes), hence + 1. + plaintext_buffer = malloc(plaintext_length + 1); + Module['stringToUTF8'](plaintext, plaintext_buffer, plaintext_length + 1); + + message_buffer = malloc(message_length + NULL_BYTE_PADDING_LENGTH); + outbound_group_session_method(Module['_olm_group_encrypt'])( + this.ptr, + plaintext_buffer, plaintext_length, + message_buffer, message_length + ); + + // UTF8ToString requires a null-terminated argument, so add the + // null terminator. + Module['setValue']( + message_buffer+message_length, + 0, "i8" + ); + + return Module['UTF8ToString'](message_buffer); + } finally { + if (plaintext_buffer !== undefined) { + // don't leave a copy of the plaintext in the heap. + bzero(plaintext_buffer, plaintext_length + 1); + free(plaintext_buffer); + } + if (message_buffer !== undefined) { + free(message_buffer); + } + } +}; OutboundGroupSession.prototype['session_id'] = restore_stack(function() { var length = outbound_group_session_method( diff --git a/javascript/olm_post.js b/javascript/olm_post.js index 8951c11..65eab02 100644 --- a/javascript/olm_post.js +++ b/javascript/olm_post.js @@ -4,9 +4,11 @@ var free = Module['_free']; var Pointer_stringify = Module['Pointer_stringify']; var OLM_ERROR = Module['_olm_error'](); -/* The 'length' argument to Pointer_stringify doesn't work if the input includes - * characters >= 128; we therefore need to add a NULL character to all of our - * strings. This acts as a symbolic constant to help show what we're doing. +/* The 'length' argument to Pointer_stringify doesn't work if the input + * includes characters >= 128, which makes Pointer_stringify unreliable. We + * could use it on strings which are known to be ascii, but that seems + * dangerous. Instead we add a NULL character to all of our strings and just + * use UTF8ToString. */ var NULL_BYTE_PADDING_LENGTH = 1; @@ -40,6 +42,13 @@ function restore_stack(wrapped) { } } +/* set a memory area to zero */ +function bzero(ptr, n) { + while(n-- > 0) { + Module['HEAP8'][ptr++] = 0; + } +} + function Account() { var size = Module['_olm_account_size'](); this.buf = malloc(size); @@ -297,59 +306,102 @@ Session.prototype['matches_inbound_from'] = restore_stack(function( Session.prototype['encrypt'] = restore_stack(function( plaintext ) { - var random_length = session_method( - Module['_olm_encrypt_random_length'] - )(this.ptr); - var message_type = session_method( - Module['_olm_encrypt_message_type'] - )(this.ptr); - var plaintext_array = array_from_string(plaintext); - var message_length = session_method( - Module['_olm_encrypt_message_length'] - )(this.ptr, plaintext_array.length); - var random = random_stack(random_length); - var plaintext_buffer = stack(plaintext_array); - var message_buffer = stack(message_length + NULL_BYTE_PADDING_LENGTH); - session_method(Module['_olm_encrypt'])( - this.ptr, - plaintext_buffer, plaintext_array.length, - random, random_length, - message_buffer, message_length - ); - return { - "type": message_type, - "body": Pointer_stringify(message_buffer) - }; + var plaintext_buffer, message_buffer, plaintext_length; + try { + var random_length = session_method( + Module['_olm_encrypt_random_length'] + )(this.ptr); + var message_type = session_method( + Module['_olm_encrypt_message_type'] + )(this.ptr); + + plaintext_length = Module['lengthBytesUTF8'](plaintext); + var message_length = session_method( + Module['_olm_encrypt_message_length'] + )(this.ptr, plaintext_length); + + var random = random_stack(random_length); + + // need to allow space for the terminator (which stringToUTF8 always + // writes), hence + 1. + plaintext_buffer = malloc(plaintext_length + 1); + Module['stringToUTF8'](plaintext, plaintext_buffer, plaintext_length + 1); + + message_buffer = malloc(message_length + NULL_BYTE_PADDING_LENGTH); + + session_method(Module['_olm_encrypt'])( + this.ptr, + plaintext_buffer, plaintext_length, + random, random_length, + message_buffer, message_length + ); + + // UTF8ToString requires a null-terminated argument, so add the + // null terminator. + Module['setValue']( + message_buffer+message_length, + 0, "i8" + ); + + return { + "type": message_type, + "body": Module['UTF8ToString'](message_buffer), + }; + } finally { + if (plaintext_buffer !== undefined) { + // don't leave a copy of the plaintext in the heap. + bzero(plaintext_buffer, plaintext_length + 1); + free(plaintext_buffer); + } + if (message_buffer !== undefined) { + free(message_buffer); + } + } }); Session.prototype['decrypt'] = restore_stack(function( message_type, message ) { - var message_array = array_from_string(message); - var message_buffer = stack(message_array); - var max_plaintext_length = session_method( - Module['_olm_decrypt_max_plaintext_length'] - )(this.ptr, message_type, message_buffer, message_array.length); - // caculating the length destroys the input buffer. - // So we copy the array to a new buffer - var message_buffer = stack(message_array); - var plaintext_buffer = stack( - max_plaintext_length + NULL_BYTE_PADDING_LENGTH - ); - var plaintext_length = session_method(Module["_olm_decrypt"])( - this.ptr, message_type, - message_buffer, message.length, - plaintext_buffer, max_plaintext_length - ); - - // Pointer_stringify requires a null-terminated argument (the optional - // 'len' argument doesn't work for UTF-8 data). - Module['setValue']( - plaintext_buffer+plaintext_length, - 0, "i8" - ); + var message_buffer, plaintext_buffer, max_plaintext_length; + + try { + message_buffer = malloc(message.length); + Module['writeAsciiToMemory'](message, message_buffer, true); + + max_plaintext_length = session_method( + Module['_olm_decrypt_max_plaintext_length'] + )(this.ptr, message_type, message_buffer, message.length); + + // caculating the length destroys the input buffer, so we need to re-copy it. + Module['writeAsciiToMemory'](message, message_buffer, true); + + plaintext_buffer = malloc(max_plaintext_length + NULL_BYTE_PADDING_LENGTH); + + var plaintext_length = session_method(Module["_olm_decrypt"])( + this.ptr, message_type, + message_buffer, message.length, + plaintext_buffer, max_plaintext_length + ); + + // UTF8ToString requires a null-terminated argument, so add the + // null terminator. + Module['setValue']( + plaintext_buffer+plaintext_length, + 0, "i8" + ); + + return UTF8ToString(plaintext_buffer); + } finally { + if (message_buffer !== undefined) { + free(message_buffer); + } + if (plaintext_buffer !== undefined) { + // don't leave a copy of the plaintext in the heap. + bzero(plaintext_buffer, max_plaintext_length + NULL_BYTE_PADDING_LENGTH); + free(plaintext_buffer); + } + } - return Pointer_stringify(plaintext_buffer); }); function Utility() { @@ -419,4 +471,22 @@ olm_exports["get_library_version"] = restore_stack(function() { getValue(buf+2, 'i8'), ]; }); -}(); + +})(); + +// export the olm functions into the environment. +// +// make sure that we do this *after* populating olm_exports, so that we don't +// get a half-built window.Olm if there is an exception. + +if (typeof module !== 'undefined' && module.exports) { + // node / browserify + module.exports = olm_exports; +} + +if (typeof(window) !== 'undefined') { + // We've been imported directly into a browser. Define the global 'Olm' object. + // (we do this even if module.exports was defined, because it's useful to have + // Olm in the global scope for browserified and webpacked apps.) + window["Olm"] = olm_exports; +} diff --git a/javascript/olm_pre.js b/javascript/olm_pre.js index 50bf8c2..ae7aba5 100644 --- a/javascript/olm_pre.js +++ b/javascript/olm_pre.js @@ -2,32 +2,32 @@ var olm_exports = {}; var get_random_values; var process; // Shadow the process object so that emscripten won't get // confused by browserify -if (typeof(global) !== 'undefined' && global["window"]) { - // We're running with browserify - module["exports"] = olm_exports; - global["window"]["Olm"] = olm_exports; - get_random_values = function(buf) { - window.crypto.getRandomValues(buf); - }; -} else if (typeof(window) !== 'undefined') { - // We've been imported directly into a browser. - window["Olm"] = olm_exports; + +if (typeof(window) !== 'undefined') { + // We've in a browser (directly, via browserify, or via webpack). get_random_values = function(buf) { window.crypto.getRandomValues(buf); }; } else if (module["exports"]) { // We're running in node. - module["exports"] = olm_exports; var nodeCrypto = require("crypto"); get_random_values = function(buf) { var bytes = nodeCrypto.randomBytes(buf.length); buf.set(bytes); - } + }; process = global["process"]; } else { throw new Error("Cannot find global to attach library to"); } -var init = function() { - var module; // Shadow the Node 'module' object so that emscripten won't try - // to fiddle with it. +(function() { + /* applications should define OLM_OPTIONS in the environment to override + * emscripten module settings */ + var Module = {}; + if (typeof(OLM_OPTIONS) !== 'undefined') { + for (var key in OLM_OPTIONS) { + if (OLM_OPTIONS.hasOwnProperty(key)) { + Module[key] = OLM_OPTIONS[key]; + } + } + } diff --git a/javascript/package.json b/javascript/package.json index b65fb2e..5432883 100644 --- a/javascript/package.json +++ b/javascript/package.json @@ -9,7 +9,7 @@ ], "scripts": { "build": "make -C .. js", - "test": "echo \"Error: no test specified\" && exit 1" + "test": "jasmine-node test --verbose --junitreport --captureExceptions" }, "repository": { "type": "git", @@ -23,5 +23,8 @@ "bugs": { "url": "https://github.com/matrix-org/olm/issues" }, - "homepage": "https://github.com/matrix-org/olm#readme" + "homepage": "https://github.com/matrix-org/olm#readme", + "devDependencies": { + "jasmine-node": "^1.14.5" + } } diff --git a/javascript/test/megolm.spec.js b/javascript/test/megolm.spec.js new file mode 100644 index 0000000..8f9d24a --- /dev/null +++ b/javascript/test/megolm.spec.js @@ -0,0 +1,68 @@ +/* +Copyright 2016 OpenMarket Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +"use strict"; + +var Olm = require('../olm'); + +describe("megolm", function() { + var aliceSession, bobSession; + + beforeEach(function() { + aliceSession = new Olm.OutboundGroupSession(); + bobSession = new Olm.InboundGroupSession(); + }); + + afterEach(function() { + if (aliceSession !== undefined) { + aliceSession.free(); + aliceSession = undefined; + } + + if (bobSession !== undefined) { + bobSession.free(); + bobSession = undefined; + } + }); + + it("should encrypt and decrypt", function() { + aliceSession.create(); + expect(aliceSession.message_index()).toEqual(0); + bobSession.create(aliceSession.session_key()); + + var TEST_TEXT='têst1'; + var encrypted = aliceSession.encrypt(TEST_TEXT); + var decrypted = bobSession.decrypt(encrypted); + console.log(TEST_TEXT, "->", decrypted); + expect(decrypted.plaintext).toEqual(TEST_TEXT); + expect(decrypted.message_index).toEqual(0); + + TEST_TEXT='hot beverage: ☕'; + encrypted = aliceSession.encrypt(TEST_TEXT); + decrypted = bobSession.decrypt(encrypted); + console.log(TEST_TEXT, "->", decrypted); + expect(decrypted.plaintext).toEqual(TEST_TEXT); + expect(decrypted.message_index).toEqual(1); + + // shorter text, to spot buffer overruns + TEST_TEXT='☕'; + encrypted = aliceSession.encrypt(TEST_TEXT); + decrypted = bobSession.decrypt(encrypted); + console.log(TEST_TEXT, "->", decrypted); + expect(decrypted.plaintext).toEqual(TEST_TEXT); + expect(decrypted.message_index).toEqual(2); + }); +}); diff --git a/javascript/test/olm.spec.js b/javascript/test/olm.spec.js new file mode 100644 index 0000000..b7cc3ae --- /dev/null +++ b/javascript/test/olm.spec.js @@ -0,0 +1,94 @@ +/* +Copyright 2016 OpenMarket Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +"use strict"; + +var Olm = require('../olm'); + +if (!Object.keys) { + Object.keys = function(o) { + var k=[], p; + for (p in o) if (Object.prototype.hasOwnProperty.call(o,p)) k.push(p); + return k; + } +} + +describe("olm", function() { + var aliceAccount, bobAccount; + var aliceSession, bobSession; + + beforeEach(function() { + aliceAccount = new Olm.Account(); + bobAccount = new Olm.Account(); + aliceSession = new Olm.Session(); + bobSession = new Olm.Session(); + }); + + afterEach(function() { + if (aliceAccount !== undefined) { + aliceAccount.free(); + aliceAccount = undefined; + } + + if (bobAccount !== undefined) { + bobAccount.free(); + bobAccount = undefined; + } + + if (aliceSession !== undefined) { + aliceSession.free(); + aliceSession = undefined; + } + + if (bobSession !== undefined) { + bobSession.free(); + bobSession = undefined; + } + }); + + it('should encrypt and decrypt', function() { + aliceAccount.create(); + bobAccount.create(); + + bobAccount.generate_one_time_keys(1); + var bobOneTimeKeys = JSON.parse(bobAccount.one_time_keys()).curve25519; + bobAccount.mark_keys_as_published(); + + var bobIdKey = JSON.parse(bobAccount.identity_keys()).curve25519; + + var otk_id = Object.keys(bobOneTimeKeys)[0]; + + aliceSession.create_outbound( + aliceAccount, bobIdKey, bobOneTimeKeys[otk_id] + ); + + var TEST_TEXT='têst1'; + var encrypted = aliceSession.encrypt(TEST_TEXT); + expect(encrypted.type).toEqual(0); + bobSession.create_inbound(bobAccount, encrypted.body); + bobAccount.remove_one_time_keys(bobSession); + var decrypted = bobSession.decrypt(encrypted.type, encrypted.body); + console.log(TEST_TEXT, "->", decrypted); + expect(decrypted).toEqual(TEST_TEXT); + + TEST_TEXT='hot beverage: ☕'; + encrypted = bobSession.encrypt(TEST_TEXT); + expect(encrypted.type).toEqual(1); + decrypted = aliceSession.decrypt(encrypted.type, encrypted.body); + console.log(TEST_TEXT, "->", decrypted); + expect(decrypted).toEqual(TEST_TEXT); + }); +}); @@ -11,4 +11,5 @@ make test . ~/.emsdk_set_env.sh make js +(cd javascript && npm run test) npm pack javascript |