From c73e35b1b6f1f716ff63964e2e9ec55b5f981e98 Mon Sep 17 00:00:00 2001 From: Scott Nonnenberg Date: Thu, 4 Mar 2021 12:01:34 -0800 Subject: [PATCH] Support for translating Desktop sessions to libsignal-client sessions --- protos/LibSignal-Client.proto | 107 ++ test/setup-test-node.js | 4 + ts/test-both/util/sessionTranslation_test.ts | 985 +++++++++++++++++++ ts/util/index.ts | 6 + ts/util/lint/exceptions.json | 74 +- ts/util/lint/linter.ts | 1 + ts/util/sessionTranslation.ts | 409 ++++++++ 7 files changed, 1513 insertions(+), 73 deletions(-) create mode 100644 protos/LibSignal-Client.proto create mode 100644 ts/test-both/util/sessionTranslation_test.ts create mode 100644 ts/util/sessionTranslation.ts diff --git a/protos/LibSignal-Client.proto b/protos/LibSignal-Client.proto new file mode 100644 index 000000000..634a29eba --- /dev/null +++ b/protos/LibSignal-Client.proto @@ -0,0 +1,107 @@ +syntax = "proto3"; + +// +// Copyright 2020-2021 Signal Messenger, LLC. +// SPDX-License-Identifier: AGPL-3.0-only +// + +package signal.proto.storage; + +message SessionStructure { + message Chain { + bytes sender_ratchet_key = 1; + bytes sender_ratchet_key_private = 2; + + message ChainKey { + uint32 index = 1; + bytes key = 2; + } + + ChainKey chain_key = 3; + + message MessageKey { + uint32 index = 1; + bytes cipher_key = 2; + bytes mac_key = 3; + bytes iv = 4; + } + + repeated MessageKey message_keys = 4; + } + + message PendingPreKey { + uint32 pre_key_id = 1; + int32 signed_pre_key_id = 3; + bytes base_key = 2; + } + + uint32 session_version = 1; + bytes local_identity_public = 2; + bytes remote_identity_public = 3; + + bytes root_key = 4; + uint32 previous_counter = 5; + + Chain sender_chain = 6; + // The order is significant; keys at the end are "older" and will get trimmed. + repeated Chain receiver_chains = 7; + + PendingPreKey pending_pre_key = 9; + + uint32 remote_registration_id = 10; + uint32 local_registration_id = 11; + + bool needs_refresh = 12; + bytes alice_base_key = 13; +} + +message RecordStructure { + SessionStructure current_session = 1; + // The order is significant; sessions at the end are "older" and will get trimmed. + repeated SessionStructure previous_sessions = 2; +} + +message PreKeyRecordStructure { + uint32 id = 1; + bytes public_key = 2; + bytes private_key = 3; +} + +message SignedPreKeyRecordStructure { + uint32 id = 1; + bytes public_key = 2; + bytes private_key = 3; + bytes signature = 4; + fixed64 timestamp = 5; +} + +message IdentityKeyPairStructure { + bytes public_key = 1; + bytes private_key = 2; +} + +message SenderKeyStateStructure { + message SenderChainKey { + uint32 iteration = 1; + bytes seed = 2; + } + + message SenderMessageKey { + uint32 iteration = 1; + bytes seed = 2; + } + + message SenderSigningKey { + bytes public = 1; + bytes private = 2; + } + + uint32 sender_key_id = 1; + SenderChainKey sender_chain_key = 2; + SenderSigningKey sender_signing_key = 3; + repeated SenderMessageKey sender_message_keys = 4; +} + +message SenderKeyRecordStructure { + repeated SenderKeyStateStructure sender_key_states = 1; +} \ No newline at end of file diff --git a/test/setup-test-node.js b/test/setup-test-node.js index 43b0d1b86..0441fa01d 100644 --- a/test/setup-test-node.js +++ b/test/setup-test-node.js @@ -3,6 +3,7 @@ /* eslint-disable no-console */ +const ByteBuffer = require('../components/bytebuffer/dist/ByteBufferAB.js'); const { setEnvironment, Environment } = require('../ts/environment'); before(() => { @@ -17,6 +18,9 @@ global.window = { error: (...args) => console.error(...args), }, i18n: key => `i18n(${key})`, + dcodeIO: { + ByteBuffer, + }, }; // For ducks/network.getEmptyState() diff --git a/ts/test-both/util/sessionTranslation_test.ts b/ts/test-both/util/sessionTranslation_test.ts new file mode 100644 index 000000000..61b5c8fa7 --- /dev/null +++ b/ts/test-both/util/sessionTranslation_test.ts @@ -0,0 +1,985 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { assert } from 'chai'; + +import { + LocalUserDataType, + sessionRecordToProtobuf, +} from '../../util/sessionTranslation'; +import { base64ToArrayBuffer } from '../../Crypto'; + +const getRecordCopy = (record: any): any => JSON.parse(JSON.stringify(record)); + +describe('sessionTranslation', () => { + let ourData: LocalUserDataType; + + beforeEach(() => { + ourData = { + identityKeyPublic: base64ToArrayBuffer( + 'Baioqfzc/5JD6b+GNqapPouf6eHK7xr9ynLJHnvl+444' + ), + registrationId: 3554, + }; + }); + + it('Throws if given an empty object', () => { + const record: any = {}; + assert.throws( + () => sessionRecordToProtobuf(record, ourData), + 'toProtobuf: Record had no sessions!' + ); + }); + + it('Generates expected protobuf with minimal record', () => { + const record: any = { + sessions: { + '\u0005W‡¿“\u0000lÈ\nyª\u000eümB0\u0017j„.Û£³-s\u0016č(O_M': { + registrationId: 4243, + currentRatchet: { + rootKey: + 'Ë\u00035/üœÚšg\u0003Xeûú\u0010—\u0000ü\u0002¶»o5\u001cƒ—­¥\u0004Ðÿ«', + lastRemoteEphemeralKey: + '\u0005\n7\u001cmT…b!è\u000eÍ\u0007\u0016m4g³\u0005‘üœIYŒê\b\u0011ÏÎPs', + previousCounter: 2, + ephemeralKeyPair: { + privKey: 'ä—ãÅ«ªŠàøí)ˆá\u0005Á"ŒsJM.¨¡\u0012r(N\f9Ô\b', + pubKey: '\u0005+\u00134–«1\u0000\u0013l *ãKçnºÖó³íTSŸ&Œ{ù ͂>1', + }, + }, + indexInfo: { + remoteIdentityKey: '\u0005¨¨©üÏäúo፩êO¢çúxr»Æ¿rœ²GžùiT@', + closed: -1, + baseKey: '\u0005W‡¿“\u0000lÈ\nyª\u000eümB0\u0017j„.Û£³-s\u0016č(O_M', + baseKeyType: 2, + }, + oldRatchetList: [], + '\u0005\n7\u001cmT…b!è\u000eÍ\u0007\u0016m4g³\u0005‘üœIYŒê\b\u0011ÏÎPs': { + messageKeys: { + '0': 'Îgó¯‘2àvñ‘X_õ\u0014–Ç\u0000öl\u001f4J>ŒÐÏ{`-Ü5¦', + '4': 'c¿<µâ¼Xµƒ!Ù¯µ®[—n<žìîúcoå©n\u0013"l]Ð', + }, + chainKey: { + counter: 5, + key: 'Z{òÙ8سAÝdSZ†k\n×\u001cô¡\u001b[YÒ¶ƒ\u0016a°\u0004<', + }, + chainType: 2, + }, + '\u0005+\u00134–«1\u0000\u0013l *ãKçnºÖó³íTSŸ&Œ{ù ͂>1': { + messageKeys: {}, + chainKey: { + counter: -1, + key: + "èB?7\u000f¯\u001e\u0010¨\u0007}:“?¹\u0010$\\ë~ª\u0000gM0՘'£\u0005", + }, + chainType: 1, + }, + }, + }, + version: 'v1', + }; + + const expected = { + currentSession: { + sessionVersion: 1, + localIdentityPublic: 'Baioqfzc/5JD6b+GNqapPouf6eHK7xr9ynLJHnvl+444', + remoteIdentityPublic: 'BaioqfzP5JD6b+GNqepPouf6eHK7xr9ynLJHnvlpgVRA', + rootKey: 'ywM1L/yc2ppnA1hl+/oQlwD8Ara7bzUcg5etpQTQ/6s=', + previousCounter: 2, + senderChain: { + senderRatchetKey: 'BSsTNJarMQATbCAq40vnbrrW87PtVFOfJox7+SDNgj4x', + senderRatchetKeyPrivate: + '5JfjxauqiuD47SmI4QXBIoxzSk0uqKEScp0oTgw51Ag=', + chainKey: { + index: -1, + key: '6EI/Nw+vHhCoB499OpM/kLkQJFzrfqoAZ00w1ZgnowU=', + }, + }, + receiverChains: [ + { + senderRatchetKey: 'BQo3HG1UhWIh6A7NBxZtNGezBZH8nElZjOqNCBHPzlBz', + chainKey: { + index: 5, + key: 'Wnvy2TjYs0HdZFNahmsKw5cc9KEbW1nSwraDFmGwBDw=', + }, + messageKeys: [ + { + index: 0, + cipherKey: 'xVreEbT7Vtrxs85JyGBj6Y+UWftQz4H72F5kWV4cxqM=', + iv: 'TcRanSxZVWbuIq0xDRGnEw==', + macKey: '5fW9aIKXhtwWp/5alNJUIXInZbztf2ywzQSpYrXoQ3A=', + }, + { + index: 4, + cipherKey: 'A99HjM4pUugsQ5+2v48FGTGEhZPoW6wzW9MqSc11QQ4=', + iv: 'bE8Ei2Rkaoz4SKRwdG4+tQ==', + macKey: 'TOTdbAf0bCHOzcQ3lBaIm3yqmpEqvvldD0qTuDFmkAI=', + }, + ], + }, + ], + remoteRegistrationId: 4243, + localRegistrationId: 3554, + aliceBaseKey: 'BVeHv5MAbMgKeaoO/G1CMBdqhC7bo7Mtc4EWxI0oT19N', + }, + }; + + const recordCopy = getRecordCopy(record); + + const actual = sessionRecordToProtobuf(record, ourData); + + assert.deepEqual(expected, actual.toJSON()); + + // We want to ensure that conversion doesn't modify incoming data + assert.deepEqual(record, recordCopy); + }); + + it('Generates expected protobuf with many old receiver chains', () => { + const record: any = { + sessions: { + '\u0005W‡¿“\u0000lÈ\nyª\u000eümB0\u0017j„.Û£³-s\u0016č(O_M': { + registrationId: 4243, + currentRatchet: { + rootKey: + 'Ë\u00035/üœÚšg\u0003Xeûú\u0010—\u0000ü\u0002¶»o5\u001cƒ—­¥\u0004Ðÿ«', + lastRemoteEphemeralKey: + '\u0005\n7\u001cmT…b!è\u000eÍ\u0007\u0016m4g³\u0005‘üœIYŒê\b\u0011ÏÎPs', + previousCounter: 2, + ephemeralKeyPair: { + privKey: 'ä—ãÅ«ªŠàøí)ˆá\u0005Á"ŒsJM.¨¡\u0012r(N\f9Ô\b', + pubKey: '\u0005+\u00134–«1\u0000\u0013l *ãKçnºÖó³íTSŸ&Œ{ù ͂>1', + }, + }, + indexInfo: { + remoteIdentityKey: '\u0005¨¨©üÏäúo፩êO¢çúxr»Æ¿rœ²GžùiT@', + closed: -1, + baseKey: '\u0005W‡¿“\u0000lÈ\nyª\u000eümB0\u0017j„.Û£³-s\u0016č(O_M', + baseKeyType: 2, + }, + oldRatchetList: [ + { + added: 1605579954962, + ephemeralKey: + '\u00050»­\n¨ÊA‘ä\u0006¢Ç´d\u0002\u00129}%î}ΩTc}8€¼\u0011n\\', + }, + { + added: 1605580408250, + ephemeralKey: + '\u0005^Ä\nò›À¢\u0000\u000f­A\\6+Ó\u001a÷&×$¸¬ÑÔ|‘x‚ƒÄÈ?þv~íkx â¬.ðo™òDg\u001eß.\r', + }, + { + added: 1606766530935, + ephemeralKey: + '\u0005\u0014@ž½M†,à\bóó™…}¨`i¿\u0000©I\u0001ôG\u001f”:Ù{ó\u0005 ', + }, + { + added: 1608326293655, + ephemeralKey: '\u0005µÒ\u0014?È¢+ÑR÷ç?3šDºƒ\\@0‹†\u0004®+-\bŽr\t', + }, + { + added: 1609871105317, + ephemeralKey: + '\u0005„±@íN"Í\u0019HS{$ï\u0017”[Ñ\\\u001a*;>P\u0000\u001f\u000eHNaù)', + }, + { + added: 1611707063523, + ephemeralKey: '\u0005Þg”Åkéƒ\u0001\u0013—¡ÿûNXÈ(9\u0006¤’w˜®/عRi‹JI', + }, + { + added: 1612211156372, + ephemeralKey: '\u0005:[ÛOˆ–pd¯ ÂÙç\u0010Oއw{}ý\bw–9Àߝ=“\u0014Z', + }, + ], + '\u00050»­\n¨ÊA‘ä\u0006¢Ç´d\u0002\u00129}%î}ΩTc}8€¼\u0011n\\': { + messageKeys: {}, + chainKey: { + counter: 0, + }, + chainType: 2, + }, + '\u0005^Ä\nò›À¢\u0000\u000f­A\\6+Ó\u001a÷&×$¸¬ÑÔ|‘x‚ƒÄÈ?þv~íkx â¬.ðo™òDg\u001eß.\r': { + messageKeys: { + '4': '©}j›¿Š¼\u0014q\tŠ¥”Á”ñ\u0003: ÷ÞrƒñûÔµ%Æ\u001a', + }, + chainKey: { + counter: 6, + }, + chainType: 2, + }, + '\u0005\u0014@ž½M†,à\bóó™…}¨`i¿\u0000©I\u0001ôG\u001f”:Ù{ó\u0005 ': { + messageKeys: {}, + chainKey: { + counter: 0, + }, + chainType: 2, + }, + '\u0005µÒ\u0014?È¢+ÑR÷ç?3šDºƒ\\@0‹†\u0004®+-\bŽr\t': { + messageKeys: {}, + chainKey: { + counter: 2, + }, + chainType: 2, + }, + '\u0005„±@íN"Í\u0019HS{$ï\u0017”[Ñ\\\u001a*;>P\u0000\u001f\u000eHNaù)': { + messageKeys: { + '0': "1kÏ\u001cí+«<º‚\b'VÌ!×¼«PÃ[üáy;l'ƒ€€Ž", + '2': 'ö\u00047%L-…Wm)†›\u001d£ääíNô.Ô8…ÃÉ4r´ó^2', + '3': '¨¿¦›7T]\u001c\u001c“à4:x\u0019¿\u0002YÉÀ\u001bâjr¸»¤¢0,*', + '5': '™¥\u0006·q“gó4þ\u0011®ˆU4F\u001cl©\bŒäô…»ÊÇƎ[', + }, + chainKey: { + counter: 5, + }, + chainType: 2, + }, + '\u0005Þg”Åkéƒ\u0001\u0013—¡ÿûNXÈ(9\u0006¤’w˜®/عRi‹JI': { + messageKeys: { + '0': "]'8ŽWÄ\u0007…n˜º­Ö{ÿ7]ôäÄ!é\u000btA@°b¢)\u001ar", + '2': '­ÄfGÇjÖxÅö:×RÔi)M\u0019©IE+¨`þKá—;£Û½', + '3': '¦Õhýø`€Ö“PéPs;\u001e\u000bE}¨¿–õ\u0003uªøå\u00062(×G', + '9': 'Ï^—<‘Õú̃\u0001i´;ït¼\u001aÑ?ï\u0014lãàƸƒ\u001a8“/m', + }, + chainKey: { + counter: 11, + }, + chainType: 2, + }, + '\u0005:[ÛOˆ–pd¯ ÂÙç\u0010Oއw{}ý\bw–9Àߝ=“\u0014Z': { + messageKeys: { + '0': '!\u00115\\W~|¯oa2\u001e\u0004Vž8Ï¡d}\u001b\u001a8^QÖfvÕ"‹', + }, + chainKey: { + counter: 1, + }, + chainType: 2, + }, + '\u0005\n7\u001cmT…b!è\u000eÍ\u0007\u0016m4g³\u0005‘üœIYŒê\b\u0011ÏÎPs': { + messageKeys: { + '0': 'Îgó¯‘2àvñ‘X_õ\u0014–Ç\u0000öl\u001f4J>ŒÐÏ{`-Ü5¦', + '4': 'c¿<µâ¼Xµƒ!Ù¯µ®[—n<žìîúcoå©n\u0013"l]Ð', + }, + chainKey: { + counter: 5, + key: 'Z{òÙ8سAÝdSZ†k\n×\u001cô¡\u001b[YÒ¶ƒ\u0016a°\u0004<', + }, + chainType: 2, + }, + '\u0005+\u00134–«1\u0000\u0013l *ãKçnºÖó³íTSŸ&Œ{ù ͂>1': { + messageKeys: {}, + chainKey: { + counter: -1, + key: + "èB?7\u000f¯\u001e\u0010¨\u0007}:“?¹\u0010$\\ë~ª\u0000gM0՘'£\u0005", + }, + chainType: 1, + }, + }, + }, + version: 'v1', + }; + + const expected = { + currentSession: { + sessionVersion: 1, + localIdentityPublic: 'Baioqfzc/5JD6b+GNqapPouf6eHK7xr9ynLJHnvl+444', + remoteIdentityPublic: 'BaioqfzP5JD6b+GNqepPouf6eHK7xr9ynLJHnvlpgVRA', + rootKey: 'ywM1L/yc2ppnA1hl+/oQlwD8Ara7bzUcg5etpQTQ/6s=', + previousCounter: 2, + senderChain: { + senderRatchetKey: 'BSsTNJarMQATbCAq40vnbrrW87PtVFOfJox7+SDNgj4x', + senderRatchetKeyPrivate: + '5JfjxauqiuD47SmI4QXBIoxzSk0uqKEScp0oTgw51Ag=', + chainKey: { + index: -1, + key: '6EI/Nw+vHhCoB499OpM/kLkQJFzrfqoAZ00w1ZgnowU=', + }, + }, + receiverChains: [ + { + senderRatchetKey: 'BQo3HG1UhWIh6A7NBxZtNGezBZH8nElZjOqNCBHPzlBz', + chainKey: { + index: 5, + key: 'Wnvy2TjYs0HdZFNahmsKw5cc9KEbW1nSwraDFmGwBDw=', + }, + messageKeys: [ + { + index: 0, + cipherKey: 'xVreEbT7Vtrxs85JyGBj6Y+UWftQz4H72F5kWV4cxqM=', + iv: 'TcRanSxZVWbuIq0xDRGnEw==', + macKey: '5fW9aIKXhtwWp/5alNJUIXInZbztf2ywzQSpYrXoQ3A=', + }, + { + index: 4, + cipherKey: 'A99HjM4pUugsQ5+2v48FGTGEhZPoW6wzW9MqSc11QQ4=', + iv: 'bE8Ei2Rkaoz4SKRwdG4+tQ==', + macKey: 'TOTdbAf0bCHOzcQ3lBaIm3yqmpEqvvldD0qTuDFmkAI=', + }, + ], + }, + { + senderRatchetKey: 'BTpb20+IlnBkryDC2ecQT96Hd3t9/Qh3ljnA3509kxRa', + chainKey: { + index: 1, + }, + messageKeys: [ + { + index: 0, + cipherKey: 'aAbSz5jOagUTgQKo3aqExcl8hyZANrY+HvrLc/OgoQI=', + iv: 'JcyLzw0fL67Kd4tfGJ2OUQ==', + macKey: 'dt+RXeaeIx+ASrKSk7D4guwTE1IUYl3LiLG9aI4sZm8=', + }, + ], + }, + { + senderRatchetKey: 'Bd5nlMVr6YMBE5eh//tOWMgoOQakkneYri/YuVJpi0pJ', + chainKey: { + index: 11, + }, + messageKeys: [ + { + index: 0, + cipherKey: 'pjcY/7MoRGtGHwNN/E8KqoKCx/5mdKp0VCmrmkBAj+M=', + iv: 'eBpAEoDj94NsI0vsf+4Hrw==', + macKey: 'P7Jz2KkOXC7B0mLkz7JaU/d0vdaYZjAfuKJ86xXB19U=', + }, + { + index: 2, + cipherKey: 'EGDj0sc/1TMtSycYDCrpZdl6UCzCzDuMwlAvVVAs2OQ=', + iv: 'A+1OA9M2Z8gGlARtA231RA==', + macKey: 'oQ/PQxJDD52qrkShSy6hD3fASEfhWnlmY3qsSPuOY/o=', + }, + { + index: 3, + cipherKey: 'WM3UUILGdECXjO8jZbBVYrPAnzRM8RdiU+PSAyHUT5U=', + iv: 'CWuQIuIyGqApA6MQgnDR5Q==', + macKey: 'hg+/xrOKFzn2eK1BnJ5C+ERsFgaWAOaBxQTc4q3b/g8=', + }, + { + index: 9, + cipherKey: 'T0cBaGAseFz+s2njVr4sqbFf1pUH5PoPvdMBoizIT+Y=', + iv: 'hkT2kqgqhlORAjBI7ZDsig==', + macKey: 'uE/Dd4WSQWkYNRgolcQtOd+HpaHP5wGogMzErkZj+AQ=', + }, + ], + }, + { + senderRatchetKey: 'BYSxQO1OIs0ZSFN7JI/vF5Rb0VwaKjs+UAAfDkhOYfkp', + chainKey: { + index: 5, + }, + messageKeys: [ + { + index: 0, + cipherKey: 'ni6XhRCoLFud2Zk1zoel4he8znDG/t+TWVBASO35GlQ=', + iv: 'rKy/sxLmQ4j2DSxbDZTO5A==', + macKey: 'MKxs29AmNOnp6zZOsIbrmSqcVXYJL01kuvIaqwjRNvQ=', + }, + { + index: 2, + cipherKey: 'Pp7GOD72vfjvb3qx7qm1YVoZKPqnyXC2uqCt89ZA/yc=', + iv: 'NuDf5iM0lD/o0YzjHZo4mA==', + macKey: 'JkBZiaxmwFr1xh/zzTQE6mlUIVJmSIrqSIQVlaoTz7M=', + }, + { + index: 3, + cipherKey: 'zORWRvJEUe2F4UnBwe2YRqPS4GzUFE1lWptcqMzWf2U=', + iv: 'Og7jF9JJhiLtPD8W2OgTnw==', + macKey: 'Lxbcl9fL9x5Javtdz7tOV7Bbr8ar3rWxSIsi1Focv9w=', + }, + { + index: 5, + cipherKey: 'T/TZNw04+ZfB0s2ltOT9qbzRPnCFn7VvxqHHAvORFx0=', + iv: 'DpOAK77ErIr2QFTsRnfOew==', + macKey: 'k/fxafepBiA0dQOTpohL+EKm2+1jpFwRigVWt02U/Jg=', + }, + ], + }, + { + senderRatchetKey: 'BbXSFD/IoivRUvfnPzOaRLqDXEAwi4YEristfwiOj3IJ', + chainKey: { + index: 2, + }, + }, + { + senderRatchetKey: 'BRRAnr1NhizgCPPzmYV9qGBpvwCpSQH0Rx+UOtl78wUg', + chainKey: { + index: 0, + }, + }, + { + senderRatchetKey: 'BZvOKPA+kXiCg8TIP/52fu1reCDirC7wb5nyRGce3y4N', + chainKey: { + index: 6, + }, + messageKeys: [ + { + index: 4, + cipherKey: 'PB44plPzHam/o2LZnyjo8HLRuAvp3uE6ixO5+GUCUsA=', + iv: 'JBbgRb10X/dDsn0GKg69dA==', + macKey: 'jKV1Rmlb0HATZHndLDIMONPgOXqT3kwE1QEstxXVe+o=', + }, + ], + }, + { + senderRatchetKey: 'Ba9q9bHjMHfbUNDCU8+0O7cmEcIluq+wk3/d2f7q+ThG', + chainKey: { + index: 3, + }, + messageKeys: [ + { + index: 0, + cipherKey: '4buOJSqRFIpWwo4pXYwQTCTxas4+amBLpZ/CuEWXbPg=', + iv: '9uD8ECO/fxtK28OvlCFXuQ==', + macKey: 'LI0ZSdX7k+cd5bTgs6XEYYIWY+2cxhWI97vAGFpoZIc=', + }, + { + index: 1, + cipherKey: 'oNbFxcy2eebUQhoD+NLf12fgkXzhn4EU0Pgqn1bVKOs=', + iv: 'o1mm4rCN6Q0J1hA7I5jjgA==', + macKey: 'dfHB14sCIdun+RaKnAoyaQPC6qRDMewjqOIDZGmn3Es=', + }, + { + index: 2, + cipherKey: '/aU3zX2IdA91GAcB+7H57yzRe+6CgZ61tlW4M/rkCJI=', + iv: 'v8VJF467QDD1ZCr1JD8pbQ==', + macKey: 'MjK5iYjhZtQTJ4Eu3+qGOdYxn0G23EGRtTcusbzy9OA=', + }, + ], + }, + { + senderRatchetKey: 'BTwX5SmcUeBG7mwyOZ3YgxyXIN0ktzuEdWTfBUmPfGYG', + chainKey: { + index: 1, + }, + }, + { + senderRatchetKey: 'BV7ECvKbwKIAD61BXDYr0xr3JtckuKzR1Hw8cVPWGtlo', + chainKey: { + index: 2, + }, + }, + { + senderRatchetKey: 'BTC7rQqoykGR5Aaix7RkAhI5fSXufc6pVGN9OIC8EW5c', + chainKey: { + index: 0, + }, + }, + ], + remoteRegistrationId: 4243, + localRegistrationId: 3554, + aliceBaseKey: 'BVeHv5MAbMgKeaoO/G1CMBdqhC7bo7Mtc4EWxI0oT19N', + }, + }; + + const recordCopy = getRecordCopy(record); + + const actual = sessionRecordToProtobuf(record, ourData); + + assert.deepEqual(expected, actual.toJSON()); + + // We want to ensure that conversion doesn't modify incoming data + assert.deepEqual(record, recordCopy); + }); + + it('Generates expected protobuf with pending prekey', () => { + const record: any = { + sessions: { + '\u0005W‡¿“\u0000lÈ\nyª\u000eümB0\u0017j„.Û£³-s\u0016č(O_M': { + registrationId: 4243, + currentRatchet: { + rootKey: + 'Ë\u00035/üœÚšg\u0003Xeûú\u0010—\u0000ü\u0002¶»o5\u001cƒ—­¥\u0004Ðÿ«', + lastRemoteEphemeralKey: + '\u0005\n7\u001cmT…b!è\u000eÍ\u0007\u0016m4g³\u0005‘üœIYŒê\b\u0011ÏÎPs', + previousCounter: 2, + ephemeralKeyPair: { + privKey: 'ä—ãÅ«ªŠàøí)ˆá\u0005Á"ŒsJM.¨¡\u0012r(N\f9Ô\b', + pubKey: '\u0005+\u00134–«1\u0000\u0013l *ãKçnºÖó³íTSŸ&Œ{ù ͂>1', + }, + }, + indexInfo: { + remoteIdentityKey: '\u0005¨¨©üÏäúo፩êO¢çúxr»Æ¿rœ²GžùiT@', + closed: -1, + baseKey: '\u0005W‡¿“\u0000lÈ\nyª\u000eümB0\u0017j„.Û£³-s\u0016č(O_M', + baseKeyType: 2, + }, + pendingPreKey: { + baseKey: '\u0005ui©üÏäúo፩êO¢çúxr»Æ¿rœ²GžùiT@', + signedKeyId: 38, + preKeyId: 2, + }, + oldRatchetList: [], + '\u0005\n7\u001cmT…b!è\u000eÍ\u0007\u0016m4g³\u0005‘üœIYŒê\b\u0011ÏÎPs': { + messageKeys: { + '0': 'Îgó¯‘2àvñ‘X_õ\u0014–Ç\u0000öl\u001f4J>ŒÐÏ{`-Ü5¦', + '4': 'c¿<µâ¼Xµƒ!Ù¯µ®[—n<žìîúcoå©n\u0013"l]Ð', + }, + chainKey: { + counter: 5, + key: 'Z{òÙ8سAÝdSZ†k\n×\u001cô¡\u001b[YÒ¶ƒ\u0016a°\u0004<', + }, + chainType: 2, + }, + '\u0005+\u00134–«1\u0000\u0013l *ãKçnºÖó³íTSŸ&Œ{ù ͂>1': { + messageKeys: {}, + chainKey: { + counter: -1, + key: + "èB?7\u000f¯\u001e\u0010¨\u0007}:“?¹\u0010$\\ë~ª\u0000gM0՘'£\u0005", + }, + chainType: 1, + }, + }, + }, + version: 'v1', + }; + + const expected = { + currentSession: { + sessionVersion: 1, + localIdentityPublic: 'Baioqfzc/5JD6b+GNqapPouf6eHK7xr9ynLJHnvl+444', + remoteIdentityPublic: 'BaioqfzP5JD6b+GNqepPouf6eHK7xr9ynLJHnvlpgVRA', + rootKey: 'ywM1L/yc2ppnA1hl+/oQlwD8Ara7bzUcg5etpQTQ/6s=', + previousCounter: 2, + senderChain: { + senderRatchetKey: 'BSsTNJarMQATbCAq40vnbrrW87PtVFOfJox7+SDNgj4x', + senderRatchetKeyPrivate: + '5JfjxauqiuD47SmI4QXBIoxzSk0uqKEScp0oTgw51Ag=', + chainKey: { + index: -1, + key: '6EI/Nw+vHhCoB499OpM/kLkQJFzrfqoAZ00w1ZgnowU=', + }, + }, + receiverChains: [ + { + senderRatchetKey: 'BQo3HG1UhWIh6A7NBxZtNGezBZH8nElZjOqNCBHPzlBz', + chainKey: { + index: 5, + key: 'Wnvy2TjYs0HdZFNahmsKw5cc9KEbW1nSwraDFmGwBDw=', + }, + messageKeys: [ + { + index: 0, + cipherKey: 'xVreEbT7Vtrxs85JyGBj6Y+UWftQz4H72F5kWV4cxqM=', + iv: 'TcRanSxZVWbuIq0xDRGnEw==', + macKey: '5fW9aIKXhtwWp/5alNJUIXInZbztf2ywzQSpYrXoQ3A=', + }, + { + index: 4, + cipherKey: 'A99HjM4pUugsQ5+2v48FGTGEhZPoW6wzW9MqSc11QQ4=', + iv: 'bE8Ei2Rkaoz4SKRwdG4+tQ==', + macKey: 'TOTdbAf0bCHOzcQ3lBaIm3yqmpEqvvldD0qTuDFmkAI=', + }, + ], + }, + ], + pendingPreKey: { + preKeyId: 2, + baseKey: 'BXVpqfzP5JD6b+GNqepPouf6eHK7xr9ynLJHnvlpgVRA', + signedPreKeyId: 38, + }, + remoteRegistrationId: 4243, + localRegistrationId: 3554, + aliceBaseKey: 'BVeHv5MAbMgKeaoO/G1CMBdqhC7bo7Mtc4EWxI0oT19N', + }, + }; + + const recordCopy = getRecordCopy(record); + + const actual = sessionRecordToProtobuf(record, ourData); + + assert.deepEqual(expected, actual.toJSON()); + + // We want to ensure that conversion doesn't modify incoming data + assert.deepEqual(record, recordCopy); + }); + + it('Generates expected protobuf with multiple sessions', () => { + const record: any = { + sessions: { + '\u0005W‡¿“\u0000lÈ\nyª\u000eümB0\u0017j„.Û£³-s\u0016č(O_M': { + registrationId: 4243, + currentRatchet: { + rootKey: + 'Ë\u00035/üœÚšg\u0003Xeûú\u0010—\u0000ü\u0002¶»o5\u001cƒ—­¥\u0004Ðÿ«', + lastRemoteEphemeralKey: + '\u0005\n7\u001cmT…b!è\u000eÍ\u0007\u0016m4g³\u0005‘üœIYŒê\b\u0011ÏÎPs', + previousCounter: 2, + ephemeralKeyPair: { + privKey: 'ä—ãÅ«ªŠàøí)ˆá\u0005Á"ŒsJM.¨¡\u0012r(N\f9Ô\b', + pubKey: '\u0005+\u00134–«1\u0000\u0013l *ãKçnºÖó³íTSŸ&Œ{ù ͂>1', + }, + }, + indexInfo: { + remoteIdentityKey: '\u0005¨¨©üÏäúo፩êO¢çúxr»Æ¿rœ²GžùiT@', + closed: -1, + baseKey: '\u0005W‡¿“\u0000lÈ\nyª\u000eümB0\u0017j„.Û£³-s\u0016č(O_M', + baseKeyType: 2, + }, + oldRatchetList: [], + '\u0005\n7\u001cmT…b!è\u000eÍ\u0007\u0016m4g³\u0005‘üœIYŒê\b\u0011ÏÎPs': { + messageKeys: { + '0': 'Îgó¯‘2àvñ‘X_õ\u0014–Ç\u0000öl\u001f4J>ŒÐÏ{`-Ü5¦', + '4': 'c¿<µâ¼Xµƒ!Ù¯µ®[—n<žìîúcoå©n\u0013"l]Ð', + }, + chainKey: { + counter: 5, + key: 'Z{òÙ8سAÝdSZ†k\n×\u001cô¡\u001b[YÒ¶ƒ\u0016a°\u0004<', + }, + chainType: 2, + }, + '\u0005+\u00134–«1\u0000\u0013l *ãKçnºÖó³íTSŸ&Œ{ù ͂>1': { + messageKeys: {}, + chainKey: { + counter: -1, + key: + "èB?7\u000f¯\u001e\u0010¨\u0007}:“?¹\u0010$\\ë~ª\u0000gM0՘'£\u0005", + }, + chainType: 1, + }, + }, + '\u0005BD¿Z\u0000lÈ\nyª\u000eümB0\u0017j„.Û£³-s\u0016č(O_M': { + registrationId: 3432, + currentRatchet: { + rootKey: + 'Ë\u00035/üœÚšg\u0003Xeûú\u0010—\u0000ü\u0002¶»o5\u001cƒ—­¥\u0004Ðÿ«', + lastRemoteEphemeralKey: + '\u0005\n7\u001cmT…b!è\u000eÍ\u0007\u0016m4g³\u0005‘üœIYŒê\b\u0011ÏÎPs', + previousCounter: 2, + ephemeralKeyPair: { + privKey: 'ä—ãÅ«ªŠàøí)ˆá\u0005Á"ŒsJM.¨¡\u0012r(N\f9Ô\b', + pubKey: '\u0005+\u00134–«1\u0000\u0013l *ãKçnºÖó³íTSŸ&Œ{ù ͂>1', + }, + }, + indexInfo: { + remoteIdentityKey: '\u0005¨¨©üÏäúo፩êO¢çúxr»Æ¿rœ²GžùiT@', + closed: 1605579954962, + baseKey: '\u0005BD¿Z\u0000lÈ\nyª\u000eümB0\u0017j„.Û£³-s\u0016č(O_M', + baseKeyType: 2, + }, + oldRatchetList: [], + '\u0005\n7\u001cmT…b!è\u000eÍ\u0007\u0016m4g³\u0005‘üœIYŒê\b\u0011ÏÎPs': { + messageKeys: { + '2': 'Îgó¯‘2àvñ‘X_õ\u0014–Ç\u0000öl\u001f4J>ŒÐÏ{`-Ü5¦', + '3': 'c¿<µâ¼Xµƒ!Ù¯µ®[—n<žìîúcoå©n\u0013"l]Ð', + }, + chainKey: { + counter: 5, + key: 'Z{òÙ8سAÝdSZ†k\n×\u001cô¡\u001b[YÒ¶ƒ\u0016a°\u0004<', + }, + chainType: 2, + }, + '\u0005+\u00134–«1\u0000\u0013l *ãKçnºÖó³íTSŸ&Œ{ù ͂>1': { + messageKeys: {}, + chainKey: { + counter: -1, + key: + "èB?7\u000f¯\u001e\u0010¨\u0007}:“?¹\u0010$\\ë~ª\u0000gM0՘'£\u0005", + }, + chainType: 1, + }, + }, + '\u0005AN¿C\u0000lÈ\nyª\u000eümB0\u0017j„.Û£³-s\u0016č(O_M': { + registrationId: 2312, + currentRatchet: { + rootKey: + 'Ë\u00035/üœÚšg\u0003Xeûú\u0010—\u0000ü\u0002¶»o5\u001cƒ—­¥\u0004Ðÿ«', + lastRemoteEphemeralKey: + '\u0005\n7\u001cmT…b!è\u000eÍ\u0007\u0016m4g³\u0005‘üœIYŒê\b\u0011ÏÎPs', + previousCounter: 2, + ephemeralKeyPair: { + privKey: 'ä—ãÅ«ªŠàøí)ˆá\u0005Á"ŒsJM.¨¡\u0012r(N\f9Ô\b', + pubKey: '\u0005+\u00134–«1\u0000\u0013l *ãKçnºÖó³íTSŸ&Œ{ù ͂>1', + }, + }, + indexInfo: { + remoteIdentityKey: '\u0005¨¨©üÏäúo፩êO¢çúxr»Æ¿rœ²GžùiT@', + closed: 1605580407000, + baseKey: '\u0005AN¿C\u0000lÈ\nyª\u000eümB0\u0017j„.Û£³-s\u0016č(O_M', + baseKeyType: 2, + }, + oldRatchetList: [], + '\u0005\n7\u001cmT…b!è\u000eÍ\u0007\u0016m4g³\u0005‘üœIYŒê\b\u0011ÏÎPs': { + messageKeys: { + '1': 'Îgó¯‘2àvñ‘X_õ\u0014–Ç\u0000öl\u001f4J>ŒÐÏ{`-Ü5¦', + '5': 'c¿<µâ¼Xµƒ!Ù¯µ®[—n<žìîúcoå©n\u0013"l]Ð', + }, + chainKey: { + counter: 5, + key: 'Z{òÙ8سAÝdSZ†k\n×\u001cô¡\u001b[YÒ¶ƒ\u0016a°\u0004<', + }, + chainType: 2, + }, + '\u0005+\u00134–«1\u0000\u0013l *ãKçnºÖó³íTSŸ&Œ{ù ͂>1': { + messageKeys: {}, + chainKey: { + counter: -1, + key: + "èB?7\u000f¯\u001e\u0010¨\u0007}:“?¹\u0010$\\ë~ª\u0000gM0՘'£\u0005", + }, + chainType: 1, + }, + }, + }, + version: 'v1', + }; + + const expected = { + currentSession: { + sessionVersion: 1, + localIdentityPublic: 'Baioqfzc/5JD6b+GNqapPouf6eHK7xr9ynLJHnvl+444', + remoteIdentityPublic: 'BaioqfzP5JD6b+GNqepPouf6eHK7xr9ynLJHnvlpgVRA', + rootKey: 'ywM1L/yc2ppnA1hl+/oQlwD8Ara7bzUcg5etpQTQ/6s=', + previousCounter: 2, + senderChain: { + senderRatchetKey: 'BSsTNJarMQATbCAq40vnbrrW87PtVFOfJox7+SDNgj4x', + senderRatchetKeyPrivate: + '5JfjxauqiuD47SmI4QXBIoxzSk0uqKEScp0oTgw51Ag=', + chainKey: { + index: -1, + key: '6EI/Nw+vHhCoB499OpM/kLkQJFzrfqoAZ00w1ZgnowU=', + }, + }, + receiverChains: [ + { + senderRatchetKey: 'BQo3HG1UhWIh6A7NBxZtNGezBZH8nElZjOqNCBHPzlBz', + chainKey: { + index: 5, + key: 'Wnvy2TjYs0HdZFNahmsKw5cc9KEbW1nSwraDFmGwBDw=', + }, + messageKeys: [ + { + index: 0, + cipherKey: 'xVreEbT7Vtrxs85JyGBj6Y+UWftQz4H72F5kWV4cxqM=', + iv: 'TcRanSxZVWbuIq0xDRGnEw==', + macKey: '5fW9aIKXhtwWp/5alNJUIXInZbztf2ywzQSpYrXoQ3A=', + }, + { + index: 4, + cipherKey: 'A99HjM4pUugsQ5+2v48FGTGEhZPoW6wzW9MqSc11QQ4=', + iv: 'bE8Ei2Rkaoz4SKRwdG4+tQ==', + macKey: 'TOTdbAf0bCHOzcQ3lBaIm3yqmpEqvvldD0qTuDFmkAI=', + }, + ], + }, + ], + remoteRegistrationId: 4243, + localRegistrationId: 3554, + aliceBaseKey: 'BVeHv5MAbMgKeaoO/G1CMBdqhC7bo7Mtc4EWxI0oT19N', + }, + previousSessions: [ + { + sessionVersion: 1, + localIdentityPublic: 'Baioqfzc/5JD6b+GNqapPouf6eHK7xr9ynLJHnvl+444', + remoteIdentityPublic: 'BaioqfzP5JD6b+GNqepPouf6eHK7xr9ynLJHnvlpgVRA', + rootKey: 'ywM1L/yc2ppnA1hl+/oQlwD8Ara7bzUcg5etpQTQ/6s=', + previousCounter: 2, + senderChain: { + senderRatchetKey: 'BSsTNJarMQATbCAq40vnbrrW87PtVFOfJox7+SDNgj4x', + senderRatchetKeyPrivate: + '5JfjxauqiuD47SmI4QXBIoxzSk0uqKEScp0oTgw51Ag=', + chainKey: { + index: -1, + key: '6EI/Nw+vHhCoB499OpM/kLkQJFzrfqoAZ00w1ZgnowU=', + }, + }, + receiverChains: [ + { + senderRatchetKey: 'BQo3HG1UhWIh6A7NBxZtNGezBZH8nElZjOqNCBHPzlBz', + chainKey: { + index: 5, + key: 'Wnvy2TjYs0HdZFNahmsKw5cc9KEbW1nSwraDFmGwBDw=', + }, + messageKeys: [ + { + index: 1, + cipherKey: 'xVreEbT7Vtrxs85JyGBj6Y+UWftQz4H72F5kWV4cxqM=', + iv: 'TcRanSxZVWbuIq0xDRGnEw==', + macKey: '5fW9aIKXhtwWp/5alNJUIXInZbztf2ywzQSpYrXoQ3A=', + }, + { + index: 5, + cipherKey: 'A99HjM4pUugsQ5+2v48FGTGEhZPoW6wzW9MqSc11QQ4=', + iv: 'bE8Ei2Rkaoz4SKRwdG4+tQ==', + macKey: 'TOTdbAf0bCHOzcQ3lBaIm3yqmpEqvvldD0qTuDFmkAI=', + }, + ], + }, + ], + remoteRegistrationId: 2312, + localRegistrationId: 3554, + aliceBaseKey: 'BUFOv0MAbMgKeaoO/G1CMBdqhC7bo7Mtc4EWxI0oT19N', + }, + { + sessionVersion: 1, + localIdentityPublic: 'Baioqfzc/5JD6b+GNqapPouf6eHK7xr9ynLJHnvl+444', + remoteIdentityPublic: 'BaioqfzP5JD6b+GNqepPouf6eHK7xr9ynLJHnvlpgVRA', + rootKey: 'ywM1L/yc2ppnA1hl+/oQlwD8Ara7bzUcg5etpQTQ/6s=', + previousCounter: 2, + senderChain: { + senderRatchetKey: 'BSsTNJarMQATbCAq40vnbrrW87PtVFOfJox7+SDNgj4x', + senderRatchetKeyPrivate: + '5JfjxauqiuD47SmI4QXBIoxzSk0uqKEScp0oTgw51Ag=', + chainKey: { + index: -1, + key: '6EI/Nw+vHhCoB499OpM/kLkQJFzrfqoAZ00w1ZgnowU=', + }, + }, + receiverChains: [ + { + senderRatchetKey: 'BQo3HG1UhWIh6A7NBxZtNGezBZH8nElZjOqNCBHPzlBz', + chainKey: { + index: 5, + key: 'Wnvy2TjYs0HdZFNahmsKw5cc9KEbW1nSwraDFmGwBDw=', + }, + messageKeys: [ + { + index: 2, + cipherKey: 'xVreEbT7Vtrxs85JyGBj6Y+UWftQz4H72F5kWV4cxqM=', + iv: 'TcRanSxZVWbuIq0xDRGnEw==', + macKey: '5fW9aIKXhtwWp/5alNJUIXInZbztf2ywzQSpYrXoQ3A=', + }, + { + index: 3, + cipherKey: 'A99HjM4pUugsQ5+2v48FGTGEhZPoW6wzW9MqSc11QQ4=', + iv: 'bE8Ei2Rkaoz4SKRwdG4+tQ==', + macKey: 'TOTdbAf0bCHOzcQ3lBaIm3yqmpEqvvldD0qTuDFmkAI=', + }, + ], + }, + ], + remoteRegistrationId: 3432, + localRegistrationId: 3554, + aliceBaseKey: 'BUJEv1oAbMgKeaoO/G1CMBdqhC7bo7Mtc4EWxI0oT19N', + }, + ], + }; + + const recordCopy = getRecordCopy(record); + + const actual = sessionRecordToProtobuf(record, ourData); + + assert.deepEqual(expected, actual.toJSON()); + + // We want to ensure that conversion doesn't modify incoming data + assert.deepEqual(record, recordCopy); + }); + + it('Generates expected protobuf with just-initialized session', () => { + const record: any = { + sessions: { + '\u00055>=eV¹\u0019ۉn¾¯—#߶_=\u0013.Nî\u001a¥%…-]ù_\n': { + registrationId: 3188, + currentRatchet: { + rootKey: '\u001b1Ÿ6ŒÊæðʨ¾>}Ú©ˆÄH¸sNÓ:ˆÈF¹³QÖi', + lastRemoteEphemeralKey: + '\u0005KÆ\\û«\u0003Ñ\u0005ÚûU±iú\u0012iˆÃ\u0011]¼åUà\u001f¯òÉ~&\u0003', + previousCounter: 0, + ephemeralKeyPair: { + privKey: + " -&\t]$\u0015P\u001fù\u000e\u001c\u001e'y…\u001eïËîEÑ+éa†ª± :wM", + pubKey: '\u0005\u0014¦çœ\u0002ò\u001aÆå\u001a{—Ø1´èn‚žn•Ç(ÛK©8PË"h', + }, + }, + indexInfo: { + remoteIdentityKey: '\u0005\u0019Ú䍧\u0006×d˜â°ˆu§õ`EËTe%H¢!&Ù8cˆz*', + closed: -1, + baseKey: '\u00055>=eV¹\u0019ۉn¾¯—#߶_=\u0013.Nî\u001a¥%…-]ù_\n', + baseKeyType: 1, + }, + oldRatchetList: [], + '\u0005\u0014¦çœ\u0002ò\u001aÆå\u001a{—Ø1´èn‚žn•Ç(ÛK©8PË"h': { + messageKeys: {}, + chainKey: { + counter: 0, + key: '¶^Do/jî\u000fU諈ª\u0011Œxnõ\u0011Æò}Ðó*äÇÊÂ\u0000', + }, + chainType: 1, + }, + pendingPreKey: { + signedKeyId: 2995, + baseKey: '\u00055>=eV¹\u0019ۉn¾¯—#߶_=\u0013.Nî\u001a¥%…-]ù_\n', + preKeyId: 386, + }, + }, + }, + version: 'v1', + }; + + const expected = { + currentSession: { + aliceBaseKey: 'BTU+PWVWuRnbiW6+ja+XI9+2Xz0TLk7uGqUlhS1d+V8K', + localIdentityPublic: 'Baioqfzc/5JD6b+GNqapPouf6eHK7xr9ynLJHnvl+444', + localRegistrationId: 3554, + pendingPreKey: { + baseKey: 'BTU+PWVWuRnbiW6+ja+XI9+2Xz0TLk7uGqUlhS1d+V8K', + preKeyId: 386, + signedPreKeyId: 2995, + }, + previousCounter: 0, + remoteIdentityPublic: 'BRmB2uSNpwbXZJjisIh1p/VgRctUZSVIoiEm2ThjiHoq', + remoteRegistrationId: 3188, + rootKey: 'GzGfNozK5vDKqL4+fdqpiMRIuHNOndM6iMhGubNR1mk=', + senderChain: { + chainKey: { + index: 0, + key: 'tl5Eby9q7n8PVeiriKoRjHhu9Y0RxvJ90PMq5MfKwgA=', + }, + senderRatchetKey: 'BRSm55wC8hrG5Rp7l9gxtOhugp5ulcco20upOFCPyyJo', + senderRatchetKeyPrivate: + 'IC0mCV0kFVAf+Q4cHid5hR7vy+5F0SvpYYaqsSA6d00=', + }, + sessionVersion: 1, + }, + }; + + const recordCopy = getRecordCopy(record); + + const actual = sessionRecordToProtobuf(record, ourData); + + assert.deepEqual(expected, actual.toJSON()); + + // We want to ensure that conversion doesn't modify incoming data + assert.deepEqual(record, recordCopy); + }); +}); diff --git a/ts/util/index.ts b/ts/util/index.ts index eab75b1ae..c550e7e21 100644 --- a/ts/util/index.ts +++ b/ts/util/index.ts @@ -22,6 +22,10 @@ import { sleep } from './sleep'; import { longRunningTaskWrapper } from './longRunningTaskWrapper'; import { toWebSafeBase64, fromWebSafeBase64 } from './webSafeBase64'; import { mapToSupportLocale } from './mapToSupportLocale'; +import { + sessionRecordToProtobuf, + sessionStructureToArrayBuffer, +} from './sessionTranslation'; import * as zkgroup from './zkgroup'; export { @@ -45,6 +49,8 @@ export { missingCaseError, parseRemoteClientExpiration, Registration, + sessionRecordToProtobuf, + sessionStructureToArrayBuffer, sleep, toWebSafeBase64, zkgroup, diff --git a/ts/util/lint/exceptions.json b/ts/util/lint/exceptions.json index fd5779a12..64ae6ce97 100644 --- a/ts/util/lint/exceptions.json +++ b/ts/util/lint/exceptions.json @@ -15241,78 +15241,6 @@ "reasonCategory": "falseMatch", "updated": "2020-02-07T19:52:28.522Z" }, - { - "rule": "jQuery-before(", - "path": "ts/test-electron/models/messages_test.js", - "line": " before(async () => {", - "lineNumber": 47, - "reasonCategory": "testCode", - "updated": "2020-10-21T00:45:53.649Z", - "reasonDetail": "Test code and a false positive." - }, - { - "rule": "jQuery-load(", - "path": "ts/test-electron/models/messages_test.js", - "line": " await window.ConversationController.load();", - "lineNumber": 49, - "reasonCategory": "testCode", - "updated": "2020-10-21T00:45:53.649Z", - "reasonDetail": "Test code and a false positive." - }, - { - "rule": "jQuery-after(", - "path": "ts/test-electron/models/messages_test.js", - "line": " after(async () => {", - "lineNumber": 53, - "reasonCategory": "testCode", - "updated": "2020-10-21T00:45:53.649Z", - "reasonDetail": "Test code and a false positive." - }, - { - "rule": "jQuery-before(", - "path": "ts/test-electron/models/messages_test.ts", - "line": " before(async () => {", - "lineNumber": 29, - "reasonCategory": "testCode", - "updated": "2020-10-21T00:45:53.649Z", - "reasonDetail": "Test code and a false positive." - }, - { - "rule": "jQuery-load(", - "path": "ts/test-electron/models/messages_test.ts", - "line": " await window.ConversationController.load();", - "lineNumber": 31, - "reasonCategory": "testCode", - "updated": "2020-10-21T00:45:53.649Z", - "reasonDetail": "Test code and a false positive." - }, - { - "rule": "jQuery-after(", - "path": "ts/test-electron/models/messages_test.ts", - "line": " after(async () => {", - "lineNumber": 36, - "reasonCategory": "testCode", - "updated": "2020-10-21T00:45:53.649Z", - "reasonDetail": "Test code and a false positive." - }, - { - "rule": "jQuery-before(", - "path": "ts/test-node/util/windowsZoneIdentifier_test.js", - "line": " before(function thisNeeded() {", - "lineNumber": 33, - "reasonCategory": "testCode", - "updated": "2020-09-02T18:59:59.432Z", - "reasonDetail": "This is test code (and isn't jQuery code)." - }, - { - "rule": "jQuery-before(", - "path": "ts/test-node/util/windowsZoneIdentifier_test.ts", - "line": " before(function thisNeeded() {", - "lineNumber": 15, - "reasonCategory": "testCode", - "updated": "2020-09-02T18:59:59.432Z", - "reasonDetail": "This is test code (and isn't jQuery code)." - }, { "rule": "jQuery-append(", "path": "ts/textsecure/ContactsParser.js", @@ -15466,4 +15394,4 @@ "updated": "2021-01-08T15:46:32.143Z", "reasonDetail": "Doesn't manipulate the DOM. This is just a function." } -] \ No newline at end of file +] diff --git a/ts/util/lint/linter.ts b/ts/util/lint/linter.ts index c80847a2a..217b0c89a 100644 --- a/ts/util/lint/linter.ts +++ b/ts/util/lint/linter.ts @@ -72,6 +72,7 @@ const excludedFilesRegexps = [ '^libtextsecure/test/test.js', '^sticker-creator/dist/bundle.js', '^test/test.js', + '^ts/test[^/]*/.+', // From libsignal-protocol-javascript project '^libtextsecure/libsignal-protocol.js', diff --git a/ts/util/sessionTranslation.ts b/ts/util/sessionTranslation.ts new file mode 100644 index 000000000..c5a4ac970 --- /dev/null +++ b/ts/util/sessionTranslation.ts @@ -0,0 +1,409 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import { get, isFinite, isInteger, isString } from 'lodash'; +import { HKDF } from 'libsignal-client'; + +import { signal } from '../protobuf/compiled'; +import { + bytesFromString, + fromEncodedBinaryToArrayBuffer, + typedArrayToArrayBuffer, +} from '../Crypto'; + +const { RecordStructure, SessionStructure } = signal.proto.storage; +const { Chain } = SessionStructure; + +type KeyPairType = { + privKey?: string; + pubKey?: string; +}; + +type OldRatchetType = { + added?: number; + ephemeralKey?: string; +}; + +type SessionType = { + registrationId?: number; + currentRatchet?: { + rootKey?: string; + lastRemoteEphemeralKey?: string; + previousCounter?: number; + ephemeralKeyPair?: KeyPairType; + }; + indexInfo?: { + remoteIdentityKey?: string; + closed?: number; + baseKey?: string; + baseKeyType?: number; + }; + pendingPreKey?: { + baseKey?: string; + signedPreKeyId?: number; + // The first two are required; this one is optional + preKeyId?: number; + }; + oldRatchetList?: Array; + + // Note: ChainTypes are stored here, keyed by their baseKey. Typescript + /// doesn't allow that kind of combination definition (known keys and + // indexer), so we force session to `any` below whenever we access it like + // `session[baseKey]`. +}; + +type MessageKeyGroup = { + [key: string]: string; +}; + +type ChainType = { + messageKeys?: MessageKeyGroup; + chainKey?: { + counter?: number; + key?: string; + }; + chainType: number; +}; + +type SessionListType = { + [key: string]: SessionType; +}; + +type SessionRecordType = { + sessions?: SessionListType; + version?: 'v1'; +}; + +export type LocalUserDataType = { + identityKeyPublic: ArrayBuffer; + registrationId: number; +}; + +export function sessionStructureToArrayBuffer( + recordStructure: signal.proto.storage.RecordStructure +): ArrayBuffer { + return typedArrayToArrayBuffer( + signal.proto.storage.RecordStructure.encode(recordStructure).finish() + ); +} + +export function sessionRecordToProtobuf( + record: SessionRecordType, + ourData: LocalUserDataType +): signal.proto.storage.RecordStructure { + const proto = new RecordStructure(); + + proto.previousSessions = []; + + const sessionGroup = record.sessions || {}; + const sessions = Object.values(sessionGroup); + + const first = sessions.find(session => { + return session?.indexInfo?.closed === -1; + }); + + if (first) { + proto.currentSession = toProtobufSession(first, ourData); + } + + sessions.sort((left, right) => { + // Descending - we want recently-closed sessions to be first + return (right?.indexInfo?.closed || 0) - (left?.indexInfo?.closed || 0); + }); + const onlyClosed = sessions.filter( + session => session?.indexInfo?.closed !== -1 + ); + + if (onlyClosed.length < sessions.length - 1) { + throw new Error('toProtobuf: More than one open session!'); + } + + proto.previousSessions = []; + onlyClosed.forEach(session => { + proto.previousSessions.push(toProtobufSession(session, ourData)); + }); + + if (!proto.currentSession && proto.previousSessions.length === 0) { + throw new Error('toProtobuf: Record had no sessions!'); + } + + return proto; +} + +function toProtobufSession( + session: SessionType, + ourData: LocalUserDataType +): signal.proto.storage.SessionStructure { + const proto = new SessionStructure(); + + // Core Fields + + proto.aliceBaseKey = binaryToUint8Array(session, 'indexInfo.baseKey', 33); + proto.localIdentityPublic = new Uint8Array(ourData.identityKeyPublic); + proto.localRegistrationId = ourData.registrationId; + + proto.previousCounter = getInteger(session, 'currentRatchet.previousCounter'); + proto.remoteIdentityPublic = binaryToUint8Array( + session, + 'indexInfo.remoteIdentityKey', + 33 + ); + proto.remoteRegistrationId = getInteger(session, 'registrationId'); + proto.rootKey = binaryToUint8Array(session, 'currentRatchet.rootKey', 32); + proto.sessionVersion = 1; + + // Note: currently unused + // proto.needsRefresh = null; + + // Pending PreKey + + if (session.pendingPreKey) { + proto.pendingPreKey = new signal.proto.storage.SessionStructure.PendingPreKey(); + proto.pendingPreKey.baseKey = binaryToUint8Array( + session, + 'pendingPreKey.baseKey', + 33 + ); + proto.pendingPreKey.signedPreKeyId = getInteger( + session, + 'pendingPreKey.signedKeyId' + ); + + if (session.pendingPreKey.preKeyId !== undefined) { + proto.pendingPreKey.preKeyId = getInteger( + session, + 'pendingPreKey.preKeyId' + ); + } + } + + // Sender Chain + + const senderBaseKey = session.currentRatchet?.ephemeralKeyPair?.pubKey; + if (!senderBaseKey) { + throw new Error('toProtobufSession: No sender base key!'); + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const senderChain = (session as any)[senderBaseKey] as ChainType | undefined; + if (!senderChain) { + throw new Error( + 'toProtobufSession: No matching chain found with senderBaseKey!' + ); + } + + if (senderChain.chainType !== 1) { + throw new Error( + `toProtobufSession: Expected sender chain type for senderChain, got ${senderChain.chainType}` + ); + } + + const protoSenderChain = toProtobufChain(senderChain); + + protoSenderChain.senderRatchetKey = binaryToUint8Array( + session, + 'currentRatchet.ephemeralKeyPair.pubKey', + 33 + ); + protoSenderChain.senderRatchetKeyPrivate = binaryToUint8Array( + session, + 'currentRatchet.ephemeralKeyPair.privKey', + 32 + ); + + proto.senderChain = protoSenderChain; + + // First Receiver Chain + + proto.receiverChains = []; + + const firstReceiverChainBaseKey = + session.currentRatchet?.lastRemoteEphemeralKey; + if (!firstReceiverChainBaseKey) { + throw new Error('toProtobufSession: No receiver base key!'); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const firstReceiverChain = (session as any)[firstReceiverChainBaseKey] as + | ChainType + | undefined; + + // If the session was just initialized, then there will be no receiver chain + if (firstReceiverChain) { + const protoFirstReceiverChain = toProtobufChain(firstReceiverChain); + + if (firstReceiverChain.chainType !== 2) { + throw new Error( + `toProtobufSession: Expected receiver chain type for firstReceiverChain, got ${firstReceiverChain.chainType}` + ); + } + + protoFirstReceiverChain.senderRatchetKey = binaryToUint8Array( + session, + 'currentRatchet.lastRemoteEphemeralKey', + 33 + ); + + proto.receiverChains.push(protoFirstReceiverChain); + } + + // Old Receiver Chains + + const oldChains = (session.oldRatchetList || []) + .slice(0) + .sort((left, right) => (right.added || 0) - (left.added || 0)); + oldChains.forEach(oldRatchet => { + const baseKey = oldRatchet.ephemeralKey; + if (!baseKey) { + throw new Error('toProtobufSession: No base key for old receiver chain!'); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const chain = (session as any)[baseKey] as ChainType | undefined; + if (!chain) { + throw new Error( + 'toProtobufSession: No chain for old receiver chain base key!' + ); + } + + if (chain.chainType !== 2) { + throw new Error( + `toProtobufSession: Expected receiver chain type, got ${chain.chainType}` + ); + } + + const protoChain = toProtobufChain(chain); + + protoChain.senderRatchetKey = binaryToUint8Array( + oldRatchet, + 'ephemeralKey', + 33 + ); + + proto.receiverChains.push(protoChain); + }); + + return proto; +} + +function toProtobufChain( + chain: ChainType +): signal.proto.storage.SessionStructure.Chain { + const proto = new Chain(); + + const protoChainKey = new Chain.ChainKey(); + protoChainKey.index = getInteger(chain, 'chainKey.counter'); + if (chain.chainKey?.key !== undefined) { + protoChainKey.key = binaryToUint8Array(chain, 'chainKey.key', 32); + } + proto.chainKey = protoChainKey; + + const messageKeys = Object.entries(chain.messageKeys || {}); + proto.messageKeys = messageKeys.map(entry => { + const protoMessageKey = new SessionStructure.Chain.MessageKey(); + protoMessageKey.index = getInteger(entry, '0'); + const key = binaryToUint8Array(entry, '1', 32); + + const { cipherKey, macKey, iv } = translateMessageKey(key); + + protoMessageKey.cipherKey = new Uint8Array(cipherKey); + protoMessageKey.macKey = new Uint8Array(macKey); + protoMessageKey.iv = new Uint8Array(iv); + + return protoMessageKey; + }); + + return proto; +} + +// Utility functions + +const WHISPER_MESSAGE_KEYS = 'WhisperMessageKeys'; + +function deriveSecrets( + input: ArrayBuffer, + salt: ArrayBuffer, + info: ArrayBuffer +): Array { + const hkdf = HKDF.new(3); + const output = hkdf.deriveSecrets( + 3 * 32, + Buffer.from(input), + Buffer.from(info), + Buffer.from(salt) + ); + return [ + typedArrayToArrayBuffer(output.slice(0, 32)), + typedArrayToArrayBuffer(output.slice(32, 64)), + typedArrayToArrayBuffer(output.slice(64, 96)), + ]; +} + +function translateMessageKey(key: Uint8Array) { + const input = key.buffer; + const salt = new ArrayBuffer(32); + const info = bytesFromString(WHISPER_MESSAGE_KEYS); + + const [cipherKey, macKey, ivContainer] = deriveSecrets(input, salt, info); + + return { + cipherKey, + macKey, + iv: ivContainer.slice(0, 16), + }; +} + +function binaryToUint8Array( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + object: any, + path: string, + length: number +): Uint8Array { + const target = get(object, path); + if (target === null || target === undefined) { + throw new Error(`binaryToUint8Array: Falsey path ${path}`); + } + + if (!isString(target)) { + throw new Error(`binaryToUint8Array: String not found at path ${path}`); + } + + const buffer = fromEncodedBinaryToArrayBuffer(target); + if (length && buffer.byteLength !== length) { + throw new Error( + `binaryToUint8Array: Got unexpected length ${buffer.byteLength} instead of ${length} at path ${path}` + ); + } + + return new Uint8Array(buffer); +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function getInteger(object: any, path: string): number { + const target = get(object, path); + if (target === null || target === undefined) { + throw new Error(`getInteger: Falsey path ${path}`); + } + + if (isString(target)) { + const result = parseInt(target, 10); + if (!isFinite(result)) { + throw new Error( + `getInteger: Value could not be parsed as number at ${path}: {target}` + ); + } + + if (!isInteger(result)) { + throw new Error( + `getInteger: Parsed value not an integer at ${path}: {target}` + ); + } + + return result; + } + + if (!isInteger(target)) { + throw new Error(`getInteger: Value not an integer at ${path}: {target}`); + } + + return target; +}