diff --git a/keyserver/flow-typed/npm/@commapp/olm_vx.x.x.js b/keyserver/flow-typed/npm/@commapp/olm_vx.x.x.js --- a/keyserver/flow-typed/npm/@commapp/olm_vx.x.x.js +++ b/keyserver/flow-typed/npm/@commapp/olm_vx.x.x.js @@ -176,6 +176,7 @@ PRIVATE_KEY_LENGTH: typeof PRIVATE_KEY_LENGTH, Account: typeof Account, Utility: typeof Utility, + Session: typeof Session, }; } diff --git a/keyserver/src/utils/olm-utils.test.js b/keyserver/src/utils/olm-utils.test.js --- a/keyserver/src/utils/olm-utils.test.js +++ b/keyserver/src/utils/olm-utils.test.js @@ -5,16 +5,214 @@ import { getOlmUtility } from '../utils/olm-utils.js'; describe('olm.Account', () => { + const alphabet = + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 '; + + const randomString = length => + Array.from( + { length }, + () => alphabet[Math.floor(Math.random() * alphabet.length)], + ).join(''); + + const initAccount = (mark_prekey_published = true) => { + const account = new olm.Account(); + account.create(); + account.generate_prekey(); + account.generate_one_time_keys(1); + if (mark_prekey_published) { + account.mark_prekey_as_published(); + } + return account; + }; + + const createSession = ( + aliceSession, + aliceAccount, + bobAccount, + regen = false, + forget = false, + invalid_sign = false, + ) => { + const bobOneTimeKeys = JSON.parse(bobAccount.one_time_keys()).curve25519; + bobAccount.mark_keys_as_published(); + const otk_id = Object.keys(bobOneTimeKeys)[0]; + + if (regen) { + bobAccount.generate_prekey(); + if (forget) { + bobAccount.forget_old_prekey(); + } + } + + if (invalid_sign) { + try { + aliceSession.create_outbound( + aliceAccount, + JSON.parse(bobAccount.identity_keys()).curve25519, + JSON.parse(bobAccount.identity_keys()).ed25519, + String(Object.values(JSON.parse(bobAccount.prekey()).curve25519)[0]), + bobAccount.sign(randomString(32)), + bobOneTimeKeys[otk_id], + ); + } catch (error) { + expect(error.message).toBe('OLM.BAD_SIGNATURE'); + return false; + } + + try { + aliceSession.create_outbound( + aliceAccount, + JSON.parse(bobAccount.identity_keys()).curve25519, + JSON.parse(bobAccount.identity_keys()).ed25519, + String(Object.values(JSON.parse(bobAccount.prekey()).curve25519)[0]), + randomString(43), + bobOneTimeKeys[otk_id], + ); + } catch (error) { + expect(error.message).toBe('OLM.INVALID_BASE64'); + return false; + } + } + + aliceSession.create_outbound( + aliceAccount, + JSON.parse(bobAccount.identity_keys()).curve25519, + JSON.parse(bobAccount.identity_keys()).ed25519, + String(Object.values(JSON.parse(bobAccount.prekey()).curve25519)[0]), + String(bobAccount.prekey_signature()), + bobOneTimeKeys[otk_id], + ); + + return aliceSession; + }; + + const testRatchet = (aliceSession, bobSession, bobAccount, num_msg = 1) => { + let test_text = randomString(40); + let encrypted = aliceSession.encrypt(test_text); + expect(encrypted.type).toEqual(0); + + try { + bobSession.create_inbound(bobAccount, encrypted.body); + } catch (error) { + expect(error.message).toBe('OLM.BAD_MESSAGE_KEY_ID'); + return false; + } + + bobAccount.remove_one_time_keys(bobSession); + let decrypted = bobSession.decrypt(encrypted.type, encrypted.body); + expect(decrypted).toEqual(test_text); + + test_text = randomString(40); + encrypted = bobSession.encrypt(test_text); + expect(encrypted.type).toEqual(1); + decrypted = aliceSession.decrypt(encrypted.type, encrypted.body); + expect(decrypted).toEqual(test_text); + + for (let index = 1; index < num_msg; index++) { + test_text = randomString(40); + encrypted = aliceSession.encrypt(test_text); + expect(encrypted.type).toEqual(1); + decrypted = bobSession.decrypt(encrypted.type, encrypted.body); + expect(decrypted).toEqual(test_text); + + test_text = randomString(40); + encrypted = bobSession.encrypt(test_text); + expect(encrypted.type).toEqual(1); + decrypted = aliceSession.decrypt(encrypted.type, encrypted.body); + expect(decrypted).toEqual(test_text); + } + + return true; + }; + it('should get Olm Utility', async () => { await olm.init(); const utility = getOlmUtility(); expect(utility).toBeDefined(); }); - it('should be able to generate and return prekey', async () => { + + it('should generate, regenerate, forget, and publish prekey', async () => { await olm.init(); - const account = new olm.Account(); - account.create(); + const account = initAccount(false); + + expect(account.last_prekey_publish_time()).toEqual(0); + expect(account.prekey()).toBeDefined(); + expect(account.unpublished_prekey()).toBeDefined(); + account.mark_prekey_as_published(); + const last_published = account.last_prekey_publish_time(); + expect(last_published).toBeGreaterThan(0); + + try { + console.log(account.unpublished_prekey()); + } catch (error) { + expect(error.message).toContain('NO_UNPUBLISHED_PREKEY'); + } + account.forget_old_prekey(); + account.generate_prekey(); expect(account.prekey()).toBeDefined(); + expect(account.unpublished_prekey()).toBeDefined(); + + expect(account.last_prekey_publish_time()).toEqual(last_published); + account.mark_prekey_as_published(); + expect(account.last_prekey_publish_time()).toBeGreaterThanOrEqual( + last_published, + ); + account.forget_old_prekey(); + }); + + it('should encrypt and decrypt', async () => { + await olm.init(); + const aliceAccount = initAccount(); + const bobAccount = initAccount(); + const aliceSession = new olm.Session(); + const bobSession = new olm.Session(); + + createSession(aliceSession, aliceAccount, bobAccount); + expect(testRatchet(aliceSession, bobSession, bobAccount)).toBeTrue; + }); + + it('should encrypt and decrypt, even after a prekey is rotated', async () => { + await olm.init(); + const aliceAccount = initAccount(); + const bobAccount = initAccount(); + const aliceSession = new olm.Session(); + const bobSession = new olm.Session(); + + createSession(aliceSession, aliceAccount, bobAccount, true); + expect(testRatchet(aliceSession, bobSession, bobAccount)).toBeTrue; + }); + + it('should not encrypt and decrypt, after the old prekey is forgotten', async () => { + await olm.init(); + const aliceAccount = initAccount(); + const bobAccount = initAccount(); + const aliceSession = new olm.Session(); + const bobSession = new olm.Session(); + + createSession(aliceSession, aliceAccount, bobAccount, true, true); + expect(testRatchet(aliceSession, bobSession, bobAccount)).toBeFalse; + }); + + it('should encrypt and decrypt repeatedly', async () => { + await olm.init(); + const aliceAccount = initAccount(); + const bobAccount = initAccount(); + const aliceSession = new olm.Session(); + const bobSession = new olm.Session(); + + createSession(aliceSession, aliceAccount, bobAccount, false, false); + expect(testRatchet(aliceSession, bobSession, bobAccount, 100)).toBeTrue; + }); + + it('should not encrypt and decrypt if prekey is not signed correctly', async () => { + await olm.init(); + const aliceAccount = initAccount(); + const bobAccount = initAccount(); + const aliceSession = new olm.Session(); + + expect( + createSession(aliceSession, aliceAccount, bobAccount, false, false, true), + ).toBeFalse; }); }); diff --git a/web/flow-typed/npm/@commapp/olm_vx.x.x.js b/web/flow-typed/npm/@commapp/olm_vx.x.x.js --- a/web/flow-typed/npm/@commapp/olm_vx.x.x.js +++ b/web/flow-typed/npm/@commapp/olm_vx.x.x.js @@ -176,6 +176,7 @@ PRIVATE_KEY_LENGTH: typeof PRIVATE_KEY_LENGTH, Account: typeof Account, Utility: typeof Utility, + Session: typeof Session, }; }