Browse Source

Separate localStorage per account (to allow several Movim accounts per browser)

Basic bundle refresh when opening a chat
Fix some issues
Add a small icon in the textarea when a session is available
pull/983/head
Timothée Jaussoin 5 years ago
parent
commit
3f04c20b62
  1. 3
      app/widgets/Chat/_chat.tpl
  2. 13
      app/widgets/Chat/chat.css
  3. 13
      app/widgets/Chat/chat.js
  4. 47
      app/widgets/ChatOmemo/ChatOmemo.php
  5. 26
      app/widgets/ChatOmemo/chatomemo.js
  6. 79
      app/widgets/ChatOmemo/chatomemo_storage.js
  7. 4
      app/widgets/ChatOmemo/locales.ini
  8. 3
      app/widgets/ContactActions/locales.ini
  9. 1
      app/widgets/System/system.tpl

3
app/widgets/Chat/_chat.tpl

@ -87,6 +87,9 @@
placeholder="{$c->__('chat.placeholder')}"
{/if}
></textarea>
<span class="encrypted">
<i class="material-icons">lock</i>
</span>
</div>
</form>
</li>

13
app/widgets/Chat/chat.css

@ -117,6 +117,19 @@ main:not(.enabled) #chat_widget {
overflow-y: auto;
}
#chat_widget .chat_box form textarea + span.encrypted {
display: none;
}
#chat_widget .chat_box form textarea[data-encrypted=true] + span.encrypted {
display: inline-block;
font-size: 2rem;
position: absolute;
right: 1rem;
top: 0;
opacity: 0.25;
}
#chat_widget .chat_box span.primary,
#chat_widget .chat_box span.control:not(.button):not(.color) {
font-size: 3.5rem;

13
app/widgets/Chat/chat.js

@ -223,10 +223,21 @@ var Chat = {
}
},
checkOmemoSession: function(jid)
{
// Try to get the missing bundles, todo: refresh more recent ones
if (!Chat.setOmemoState(jid)) {
ChatOmemo_ajaxGetBundles(jid, ChatOmemo.getSessionBundlesIds(jid));
}
},
setOmemoState: function(jid)
{
let enabled = ChatOmemo.hasSessionOpened(jid);
let textarea = Chat.getTextarea();
textarea.dataset.encrypted = enabled;
if (textarea) {
textarea.dataset.encrypted = enabled;
}
return enabled;
},
enableSending: function()
{

47
app/widgets/ChatOmemo/ChatOmemo.php

@ -1,5 +1,6 @@
<?php
use App\Bundle;
use Moxl\Xec\Action\OMEMO\AnnounceBundle;
use Moxl\Xec\Action\OMEMO\GetDeviceList;
use Moxl\Xec\Action\OMEMO\SetDeviceList;
@ -19,16 +20,35 @@ class ChatOmemo extends \Movim\Widget\Base
public function onBundle($packet)
{
$bundle = $packet->content;
$prekey = $this->extractPreKey($bundle);
$this->rpc('ChatOmemo.handlePreKey', $bundle->jid, $bundle->bundle_id, $prekey);
}
$pickedKey = array_rand($bundle->prekeys);
$prekey = [
'identitykey' => $bundle->identitykey,
'prekeypublic' => $bundle->prekeypublic,
'prekeysignature' => $bundle->prekeysignature,
'prekey' => ['id' => $pickedKey, 'value' => $bundle->prekeys[$pickedKey]]
];
public function ajaxNotifyGeneratingBundle()
{
Toast::send($this->__('omemo.generating_bundle'));
$this->rpc('ChatOmemo.doGenerateBundle');
}
$this->rpc('ChatOmemo.handlePreKey', $bundle->jid, $bundle->bundle_id, $prekey);
public function ajaxNotifyGeneratedBundle()
{
Toast::send($this->__('omemo.generated_bundle'));
}
public function ajaxGetBundles(string $jid, array $exceptBundleIds = [])
{
$bundles = $this->user->bundles()->where('jid', $jid);
if (!empty($exceptBundleIds)) {
$bundles = $bundles->whereNotIn('bundle_id', $exceptBundleIds);
}
$bundles = $bundles->get();
foreach ($bundles as $bundle) {
$prekey = $this->extractPreKey($bundle);
$this->rpc('ChatOmemo.handlePreKey', $bundle->jid, $bundle->bundle_id, $prekey);
}
}
public function ajaxGetDevicesList($to)
@ -69,4 +89,15 @@ class ChatOmemo extends \Movim\Widget\Base
$sdl->setList($devicesList)
->request();
}
private function extractPreKey(Bundle $bundle): array
{
$pickedKey = array_rand($bundle->prekeys);
return [
'identitykey' => $bundle->identitykey,
'prekeypublic' => $bundle->prekeypublic,
'prekeysignature' => $bundle->prekeysignature,
'prekey' => ['id' => $pickedKey, 'value' => $bundle->prekeys[$pickedKey]]
];
}
}

26
app/widgets/ChatOmemo/chatomemo.js

@ -9,13 +9,17 @@ const SIGNED_PREKEY_ID = 1234;
var ChatOmemo = {
generateBundle: async function () {
ChatOmemo_ajaxNotifyGeneratingBundle();
},
doGenerateBundle: async function () {
var store = new ChatOmemoStorage();
const identityKeyPair = await KeyHelper.generateIdentityKeyPair();
const bundle = {};
const identityKey = MovimUtils.arrayBufferToBase64(identityKeyPair.pubKey);
const localDeviceId = store.getLocalRegistrationId();
const localDeviceId = await store.getLocalRegistrationId();
const deviceId = localDeviceId ?? KeyHelper.generateRegistrationId();
bundle['identityKey'] = identityKey;
@ -38,7 +42,7 @@ var ChatOmemo = {
const preKeys = keys.map(k => ({ 'id': k.keyId, 'key': k.keyPair.pubKey }));
bundle['preKeys'] = preKeys;
console.log(bundle);
ChatOmemo_ajaxNotifyGeneratedBundle();
ChatOmemo_ajaxAnnounceBundle(bundle);
},
@ -69,7 +73,6 @@ var ChatOmemo = {
const preKeys = keys.map(k => ({ 'id': k.keyId, 'key': k.keyPair.pubKey }));
bundle['preKeys'] = preKeys;
console.log(bundle);
ChatOmemo_ajaxAnnounceBundle(bundle);
},
@ -79,7 +82,7 @@ var ChatOmemo = {
var sessionBuilder = new libsignal.SessionBuilder(store, address);
var promise = sessionBuilder.processPreKey({
sessionBuilder.processPreKey({
registrationId: 0,
identityKey: MovimUtils.base64ToArrayBuffer(preKey.identitykey),
signedPreKey: {
@ -91,14 +94,12 @@ var ChatOmemo = {
keyId: preKey.prekey.id,
publicKey: MovimUtils.base64ToArrayBuffer(preKey.prekey.value)
}
});
promise.then(function onsuccess() {
}).then(function onsuccess() {
console.log('success');
});
promise.catch(function onerror(error) {
Chat.setOmemoState(jid);
}).catch(function onerror(error) {
console.log(error);
Chat.setOmemoState(jid);
});
},
encrypt: async function (to, plaintext) {
@ -215,6 +216,11 @@ var ChatOmemo = {
.filter(key => key.startsWith('session' + jid))
.length > 0;
},
getSessionBundlesIds(jid) {
return Object.keys(localStorage)
.filter(key => key.startsWith('session' + jid))
.map(key => key.substring(key.lastIndexOf('.') + 1));
},
encryptJid: function (plaintext, jid) {
let promises = Object.keys(localStorage)
.filter(key => key.startsWith('session' + jid))

79
app/widgets/ChatOmemo/chatomemo_storage.js

@ -1,5 +1,5 @@
function ChatOmemoStorage() {
this.store = {};
this.jid = USER_JID;
}
ChatOmemoStorage.prototype = {
@ -8,26 +8,6 @@ ChatOmemoStorage.prototype = {
RECEIVING: 2,
},
setIdentityKeyPair: function(identityKeyPair) {
return Promise.resolve(this.put('identityKey', {
'privKey': MovimUtils.arrayBufferToBase64(identityKeyPair.privKey),
'pubKey': MovimUtils.arrayBufferToBase64(identityKeyPair.pubKey)
}));
},
getIdentityKeyPair: function () {
identityKeyPair = this.get('identityKey');
return Promise.resolve({
'privKey': MovimUtils.base64ToArrayBuffer(identityKeyPair.privKey),
'pubKey': MovimUtils.base64ToArrayBuffer(identityKeyPair.pubKey)
});
},
setLocalRegistrationId: function (registrationId) {
return Promise.resolve(this.put('registrationId', registrationId));
},
getLocalRegistrationId: function () {
return Promise.resolve(this.get('registrationId'));
},
put: function (key, value) {
if (key === undefined || value === undefined || key === null || value === null)
throw new Error("Tried to store undefined/null");
@ -50,6 +30,27 @@ ChatOmemoStorage.prototype = {
localStorage.removeItem(key);
},
setIdentityKeyPair: function(identityKeyPair) {
return Promise.resolve(this.put(this.jid + '.identityKey', {
'privKey': MovimUtils.arrayBufferToBase64(identityKeyPair.privKey),
'pubKey': MovimUtils.arrayBufferToBase64(identityKeyPair.pubKey)
}));
},
getIdentityKeyPair: function () {
identityKeyPair = this.get(this.jid + '.identityKey');
return Promise.resolve({
'privKey': MovimUtils.base64ToArrayBuffer(identityKeyPair.privKey),
'pubKey': MovimUtils.base64ToArrayBuffer(identityKeyPair.pubKey)
});
},
setLocalRegistrationId: function (registrationId) {
return Promise.resolve(this.put(this.jid + '.registrationId', registrationId));
},
getLocalRegistrationId: function () {
return Promise.resolve(this.get(this.jid + '.registrationId'));
},
isTrustedIdentity: function (identifier, identityKey, direction) {
if (identifier === null || identifier === undefined) {
throw new Error("tried to check identity key for undefined/null key");
@ -57,7 +58,7 @@ ChatOmemoStorage.prototype = {
if (!(identityKey instanceof ArrayBuffer)) {
throw new Error("Expected identityKey to be an ArrayBuffer");
}
var trusted = this.get('identityKey' + identifier);
var trusted = this.get(this.jid + '.identityKey' + identifier);
if (trusted === undefined) {
return Promise.resolve(true);
}
@ -67,7 +68,7 @@ ChatOmemoStorage.prototype = {
loadIdentityKey: function (identifier) {
if (identifier === null || identifier === undefined)
throw new Error("Tried to get identity key for undefined/null key");
return Promise.resolve(this.get('identityKey' + identifier));
return Promise.resolve(this.get(this.jid + '.identityKey' + identifier));
},
saveIdentity: function (identifier, identityKey) {
if (identifier === null || identifier === undefined)
@ -75,8 +76,8 @@ ChatOmemoStorage.prototype = {
var address = new libsignal.SignalProtocolAddress.fromString(identifier);
var existing = this.get('identityKey' + address.getName());
this.put('identityKey' + address.getName(), identityKey)
var existing = this.get(this.jid + '.identityKey' + address.getName());
this.put(this.jid + '.identityKey' + address.getName(), identityKey)
if (existing && toString(identityKey) !== toString(existing)) {
return Promise.resolve(true);
@ -86,9 +87,8 @@ ChatOmemoStorage.prototype = {
},
/* Returns a prekeypair object or undefined */
loadPreKey: function (keyId) {
var res = this.get('25519KeypreKey' + keyId);
var res = this.get(this.jid + '.25519KeypreKey' + keyId);
if (res !== undefined) {
res = {
pubKey: MovimUtils.base64ToArrayBuffer(res.pubKey),
@ -100,15 +100,14 @@ ChatOmemoStorage.prototype = {
storePreKey: function (keyId, keyPair) {
keyPair.pubKey = MovimUtils.arrayBufferToBase64(keyPair.pubKey);
keyPair.privKey = MovimUtils.arrayBufferToBase64(keyPair.privKey);
return Promise.resolve(this.put('25519KeypreKey' + keyId, keyPair));
return Promise.resolve(this.put(this.jid + '.25519KeypreKey' + keyId, keyPair));
},
removePreKey: function (keyId) {
return Promise.resolve(this.remove('25519KeypreKey' + keyId));
return Promise.resolve(this.remove(this.jid + '.25519KeypreKey' + keyId));
},
/* Returns a signed keypair object or undefined */
loadSignedPreKey: function (keyId) {
var res = this.get('25519KeysignedKey' + keyId);
var res = this.get(this.jid + '.25519KeysignedKey' + keyId);
if (res !== undefined) {
res = {
keyPair: {
@ -125,27 +124,27 @@ ChatOmemoStorage.prototype = {
key.keyPair.pubKey = MovimUtils.arrayBufferToBase64(key.keyPair.pubKey);
key.keyPair.privKey = MovimUtils.arrayBufferToBase64(key.keyPair.privKey);
key.signature = MovimUtils.arrayBufferToBase64(key.signature);
return Promise.resolve(this.put('25519KeysignedKey' + keyId, key));
return Promise.resolve(this.put(this.jid + '.25519KeysignedKey' + keyId, key));
},
removeSignedPreKey: function (keyId) {
return Promise.resolve(this.remove('25519KeysignedKey' + keyId));
return Promise.resolve(this.remove(this.jid + '.25519KeysignedKey' + keyId));
},
loadSession: function (identifier) {
return Promise.resolve(this.get('session' + identifier));
return Promise.resolve(this.get(this.jid + '.session' + identifier));
},
storeSession: function (identifier, record) {
return Promise.resolve(this.put('session' + identifier, record));
return Promise.resolve(this.put(this.jid + '.session' + identifier, record));
},
removeSession: function (identifier) {
return Promise.resolve(this.remove('session' + identifier));
return Promise.resolve(this.remove(this.jid + '.session' + identifier));
},
removeAllSessions: function (identifier) {
for (var id in this.store) {
if (id.startsWith('session' + identifier)) {
delete this.store[id];
}
for (key in Object.keys(localStorage)
.filter(key => key.startsWith(this.jid + '.session' + identifier))) {
this.remove(key);
}
return Promise.resolve();
},
toString: function(thing) {

4
app/widgets/ChatOmemo/locales.ini

@ -0,0 +1,4 @@
[omemo]
fingerprints = OMEMO Fingerprints
generating_bundle = "Generating the encryption keys, please wait…"
generated_bundle = Encryption keys generated

3
app/widgets/ContactActions/locales.ini

@ -5,6 +5,3 @@ add_contact_info2 = Press enter to validate.
added = Contact added
updated = Contact updated
deleted = Contact deleted
[omemo]
fingerprints = OMEMO Fingerprints

1
app/widgets/System/system.tpl

@ -4,3 +4,4 @@ var ERROR_URI = '{$error_uri}';
var SMALL_PICTURE_LIMIT = {$small_picture_limit};
var NOTIFICATION_CHAT = {if="$user && $user->notificationchat === true"}true{else}false{/if};
var NOTIFICATION_CALL = {if="$user && $user->notificationcall === true"}true{else}false{/if};
var USER_JID = {if="$user"}'{$user->id}'{else}false{/if};
Loading…
Cancel
Save