init
This commit is contained in:
@@ -0,0 +1,518 @@
|
||||
var KeyHelper = libsignal.KeyHelper;
|
||||
|
||||
var ChatOmemo = {
|
||||
refreshed: false,
|
||||
|
||||
initiateBundle: async function (devicesIds) {
|
||||
var store = new ChatOmemoStorage();
|
||||
store.removeAllSessions();
|
||||
|
||||
const identityKeyPair = await KeyHelper.generateIdentityKeyPair();
|
||||
const bundle = {};
|
||||
|
||||
const localDeviceId = await store.getLocalRegistrationId();
|
||||
const deviceId = localDeviceId ?? KeyHelper.generateRegistrationId();
|
||||
|
||||
bundle['identityKey'] = MovimUtils.arrayBufferToBase64(identityKeyPair.pubKey);
|
||||
bundle['deviceId'] = deviceId;
|
||||
|
||||
store.setLocalRegistrationId(deviceId);
|
||||
store.setIdentityKeyPair(identityKeyPair);
|
||||
|
||||
const signedPreKey = await KeyHelper.generateSignedPreKey(identityKeyPair, SIGNED_PREKEY_ID);
|
||||
|
||||
store.storeSignedPreKey(signedPreKey.keyId, signedPreKey);
|
||||
bundle['signedPreKey'] = {
|
||||
'id': signedPreKey.keyId,
|
||||
'publicKey': signedPreKey.keyPair.pubKey,
|
||||
'signature': signedPreKey.signature
|
||||
}
|
||||
const keys = await Promise.all(MovimUtils.range(0, NUM_PREKEYS).map(id => KeyHelper.generatePreKey(id)));
|
||||
keys.forEach(k => store.storePreKey(k.keyId, k.keyPair));
|
||||
|
||||
const preKeys = keys.map(k => ({ 'id': k.keyId, 'key': k.keyPair.pubKey }));
|
||||
bundle['preKeys'] = preKeys;
|
||||
|
||||
ChatOmemo_ajaxNotifyGeneratedBundle();
|
||||
ChatOmemo_ajaxAnnounceBundle(bundle, devicesIds);
|
||||
ChatOmemo_ajaxGetDevicesList(USER_JID);
|
||||
},
|
||||
|
||||
refreshBundle: async function (devicesIds, publishOnly) {
|
||||
var store = new ChatOmemoStorage();
|
||||
|
||||
const bundle = {};
|
||||
const localDeviceId = await store.getLocalRegistrationId();
|
||||
|
||||
if (!localDeviceId) ChatOmemo.initiateBundle(devicesIds);
|
||||
|
||||
// We get the base of the bundle from the store
|
||||
let keyPair = await store.getIdentityKeyPair();
|
||||
const deviceId = localDeviceId;
|
||||
|
||||
bundle['identityKey'] = MovimUtils.arrayBufferToBase64(keyPair.pubKey);
|
||||
bundle['deviceId'] = deviceId;
|
||||
|
||||
let signedPreKey = store.loadCompleteSignedPreKey(SIGNED_PREKEY_ID);
|
||||
bundle['signedPreKey'] = {
|
||||
'id': signedPreKey.keyId,
|
||||
'publicKey': MovimUtils.arrayBufferToBase64(signedPreKey.keyPair.pubKey),
|
||||
'signature': MovimUtils.arrayBufferToBase64(signedPreKey.signature)
|
||||
}
|
||||
|
||||
// We refresh or load all the preKeys
|
||||
let keys = [];
|
||||
let preKeys = [];
|
||||
|
||||
if (publishOnly) {
|
||||
keys = await Promise.all(MovimUtils.range(0, NUM_PREKEYS).map(id => store.loadPreKey(id)));
|
||||
|
||||
let counter = -1;
|
||||
preKeys = keys.map(k => {
|
||||
counter++;
|
||||
return { 'id': counter, 'key': MovimUtils.arrayBufferToBase64(k.pubKey) };
|
||||
});
|
||||
} else {
|
||||
keys = await Promise.all(MovimUtils.range(0, NUM_PREKEYS).map(id => KeyHelper.generatePreKey(id)));
|
||||
keys.forEach(k => store.storePreKey(k.keyId, k.keyPair));
|
||||
preKeys = keys.map(k => ({ 'id': k.keyId, 'key': k.keyPair.pubKey }));
|
||||
}
|
||||
|
||||
bundle['preKeys'] = preKeys;
|
||||
|
||||
ChatOmemo_ajaxAnnounceBundle(bundle, devicesIds);
|
||||
},
|
||||
|
||||
ownDevicesReceived: async function (from, devicesIds) {
|
||||
var store = new ChatOmemoStorage();
|
||||
const localDeviceId = await store.getLocalRegistrationId();
|
||||
devicesIds = Object.values(devicesIds);
|
||||
|
||||
if (localDeviceId == undefined) {
|
||||
ChatOmemo.initiateBundle(devicesIds);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!devicesIds.includes(localDeviceId.toString()) && ChatOmemo.refreshed == false) {
|
||||
ChatOmemo.refreshBundle(devicesIds, true);
|
||||
ChatOmemo.refreshed = true;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* A contact send us a new device list, we check if we can already build a new session
|
||||
*/
|
||||
devicesReceived: async function (from, devicesIds) {
|
||||
var store = new ChatOmemoStorage();
|
||||
let localIds = store.getSessionsIds(from);
|
||||
|
||||
if (await store.getContactState(from)) {
|
||||
for (deviceId of devicesIds.filter(key => !localIds.includes(key))) {
|
||||
ChatOmemo_ajaxGetBundle(from, deviceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* If we have open sessions from devices that doesn't exists anymore
|
||||
* we close them
|
||||
*/
|
||||
for (deviceId of localIds.filter(key => !devicesIds.includes(key))) {
|
||||
var address = new libsignal.SignalProtocolAddress(from, deviceId);
|
||||
store.removeSession(address);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
bundlesRefreshError(jid) {
|
||||
var store = new ChatOmemoStorage();
|
||||
store.setContactTombstone(jid);
|
||||
ChatOmemo.disableContactState(jid);
|
||||
},
|
||||
|
||||
bundlesRefreshed(jid) {
|
||||
var store = new ChatOmemoStorage();
|
||||
|
||||
if (typeof Chat == 'undefined') return;
|
||||
|
||||
let textarea = Chat && Chat.getTextarea();
|
||||
|
||||
if (textarea) {
|
||||
// First time we handle a session, we enforce the OMEMO state to true by default
|
||||
if (!store.hasContactState(jid)) {
|
||||
store.setContactState(jid, true);
|
||||
}
|
||||
|
||||
if (textarea.dataset.jid == jid) {
|
||||
Chat.setOmemoState('yes');
|
||||
Chat.disableSending();
|
||||
|
||||
Chat_ajaxGet(jid);
|
||||
|
||||
if (textarea.value.length > 0) {
|
||||
Chat.sendMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
handlePreKey: async function (jid, deviceId, preKey) {
|
||||
var store = new ChatOmemoStorage();
|
||||
|
||||
const localDeviceId = await store.getLocalRegistrationId();
|
||||
if (localDeviceId == undefined) return;
|
||||
|
||||
var address = new libsignal.SignalProtocolAddress(jid, deviceId);
|
||||
|
||||
const session = await store.loadSession(address.toString());
|
||||
|
||||
// If we already have a session we don't have to build it
|
||||
if (session) {
|
||||
var promise = Promise.resolve();
|
||||
} else {
|
||||
var sessionBuilder = new libsignal.SessionBuilder(store, address);
|
||||
var promise = sessionBuilder.processPreKey({
|
||||
registrationId: parseInt(deviceId, 10),
|
||||
identityKey: MovimUtils.base64ToArrayBuffer(preKey.identitykey),
|
||||
signedPreKey: {
|
||||
keyId: parseInt(preKey.signedprekeyid, 10),
|
||||
publicKey: MovimUtils.base64ToArrayBuffer(preKey.signedprekeypublic),
|
||||
signature: MovimUtils.base64ToArrayBuffer(preKey.signedprekeysignature)
|
||||
},
|
||||
preKey: {
|
||||
keyId: preKey.prekey.id,
|
||||
publicKey: MovimUtils.base64ToArrayBuffer(preKey.prekey.value)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
promise.then(function onsuccess() {
|
||||
console.log('success ' + jid + ':' + deviceId);
|
||||
|
||||
if (typeof Chat == 'undefined') return;
|
||||
|
||||
let textarea = Chat.getTextarea();
|
||||
if (textarea && textarea.dataset.jid == jid) {
|
||||
Chat.setOmemoState('yes');
|
||||
}
|
||||
});
|
||||
|
||||
promise.catch(function onerror(error) {
|
||||
console.log(error);
|
||||
});
|
||||
|
||||
return promise;
|
||||
},
|
||||
|
||||
encrypt: async function (to, plaintext, muc) {
|
||||
var store = new ChatOmemoStorage();
|
||||
|
||||
// https://xmpp.org/extensions/attic/xep-0384-0.3.0.html#usecases-messagesend
|
||||
|
||||
let iv = crypto.getRandomValues(new Uint8Array(12));
|
||||
let key = await crypto.subtle.generateKey(KEY_ALGO, true, ['encrypt', 'decrypt']);
|
||||
|
||||
let algo = {
|
||||
'name': 'AES-GCM',
|
||||
'iv': iv,
|
||||
'tagLength': 128
|
||||
};
|
||||
|
||||
let encrypted = await crypto.subtle.encrypt(algo, key, MovimUtils.stringToArrayBuffer(plaintext));
|
||||
let length = encrypted.byteLength - ((128 + 7) >> 3);
|
||||
let ciphertext = encrypted.slice(0, length);
|
||||
let tag = encrypted.slice(length);
|
||||
let exportedKey = await crypto.subtle.exportKey('raw', key);
|
||||
|
||||
// obj
|
||||
let keyAndTag = MovimUtils.appendArrayBuffer(exportedKey, tag);
|
||||
let biv = MovimUtils.arrayBufferToBase64(iv);
|
||||
let payload = MovimUtils.arrayBufferToBase64(ciphertext);
|
||||
let deviceId = await store.getLocalRegistrationId();
|
||||
let ownKeys = await this.encryptJid(keyAndTag, store.jid);
|
||||
|
||||
let remoteKeys = [];
|
||||
|
||||
if (muc) {
|
||||
for (member of Chat.groupChatMembers) {
|
||||
remoteKeys = remoteKeys.concat(await this.encryptJid(keyAndTag, member));
|
||||
}
|
||||
} else {
|
||||
remoteKeys = await this.encryptJid(keyAndTag, to);
|
||||
}
|
||||
|
||||
ownKeys = ownKeys.concat(remoteKeys);
|
||||
|
||||
let messageKeys = {};
|
||||
ownKeys.map(result => {
|
||||
messageKeys[result.device] = {
|
||||
payload: btoa(result.payload.body),
|
||||
prekey: 3 == parseInt(result.payload.type, 10)
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
'sid': deviceId,
|
||||
'keys': messageKeys,
|
||||
'iv': biv,
|
||||
'payload': payload
|
||||
};
|
||||
},
|
||||
decrypt: async function (message) {
|
||||
if (message.omemoheader == undefined) return;
|
||||
|
||||
var store = new ChatOmemoStorage();
|
||||
|
||||
let resolvedId = message.mine
|
||||
? message.messageid
|
||||
: message.id;
|
||||
|
||||
let maybeDecrypted = await ChatOmemoDB.getMessage(resolvedId);
|
||||
|
||||
if (maybeDecrypted !== undefined) {
|
||||
return maybeDecrypted;
|
||||
}
|
||||
|
||||
// Resolved jid from a muc message
|
||||
var jid = message.mucjid ?? message.jidfrom;
|
||||
|
||||
// If there was an encryption tombstone on this contact we can lift it
|
||||
store.removeContactTombstone(jid);
|
||||
|
||||
// No keys
|
||||
if (message.omemoheader.keys == undefined) return;
|
||||
|
||||
let deviceId = await store.getLocalRegistrationId();
|
||||
|
||||
if (!deviceId) return;
|
||||
|
||||
/**
|
||||
* Check if we need to build more sessions
|
||||
*/
|
||||
if (Chat) {
|
||||
textarea = Chat.getTextarea();
|
||||
|
||||
if (!Boolean(textarea.dataset.muc)) {
|
||||
for (bundleId of Object.keys(message.omemoheader.keys).filter(
|
||||
key => !store.getSessionsIds(jid)
|
||||
.concat(store.getOwnSessionsIds(), [deviceId.toString()]).includes(key)
|
||||
)
|
||||
) {
|
||||
ChatOmemo_ajaxGetBundle(jid, bundleId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (message.omemoheader.keys[deviceId] == undefined) {
|
||||
console.log('Message not encrypted for this device');
|
||||
ChatOmemoDB.putMessage(resolvedId, false);
|
||||
return;
|
||||
}
|
||||
|
||||
let key = message.omemoheader.keys[deviceId];
|
||||
let plainKey;
|
||||
|
||||
try {
|
||||
plainKey = await this.decryptDevice(MovimUtils.base64ToArrayBuffer(key.payload), key.prekey, jid, message.omemoheader.sid);
|
||||
} catch (err) {
|
||||
ChatOmemoDB.putMessage(resolvedId, false);
|
||||
console.log('Error during decryption: ' + err);
|
||||
|
||||
// Let the session heal, a new prekey will be send next time
|
||||
//var address = new libsignal.SignalProtocolAddress(jid, message.omemoheader.sid);
|
||||
//store.removeSession(address);
|
||||
return;
|
||||
}
|
||||
|
||||
let exportedAESKey = plainKey.slice(0, 16);
|
||||
let authenticationTag = plainKey.slice(16);
|
||||
|
||||
if (authenticationTag.byteLength < 16) {
|
||||
if (authenticationTag.byteLength > 0) {
|
||||
throw new Error('Authentication tag too short');
|
||||
}
|
||||
|
||||
console.log(`Authentication tag is only ${authenticationTag.byteLength} byte long`);
|
||||
}
|
||||
|
||||
if (!message.omemoheader.payload) {
|
||||
console.log('No payload to decrypt');
|
||||
}
|
||||
|
||||
// One of our key was used, lets refresh the bundle
|
||||
if (key.prekey) {
|
||||
ChatOmemo.refreshBundle(store.getOwnSessionsIds());
|
||||
}
|
||||
|
||||
let iv = MovimUtils.base64ToArrayBuffer(message.omemoheader.iv);
|
||||
let ciphertextAndAuthenticationTag = MovimUtils.appendArrayBuffer(
|
||||
MovimUtils.base64ToArrayBuffer(message.omemoheader.payload),
|
||||
authenticationTag
|
||||
);
|
||||
|
||||
let importedKey = await crypto.subtle.importKey('raw', exportedAESKey, 'AES-GCM', false, ['decrypt']);
|
||||
let decryptedBuffer = await crypto.subtle.decrypt({
|
||||
name: 'AES-GCM',
|
||||
iv,
|
||||
tagLength: 128
|
||||
}, importedKey, ciphertextAndAuthenticationTag);
|
||||
|
||||
let plaintext = MovimUtils.arrayBufferToString(decryptedBuffer);
|
||||
|
||||
ChatOmemoDB.putMessage(resolvedId, plaintext);
|
||||
return plaintext;
|
||||
},
|
||||
enableContactState: function (jid, muc) {
|
||||
var store = new ChatOmemoStorage();
|
||||
store.setContactState(jid, true);
|
||||
|
||||
if (muc) {
|
||||
Chat_ajaxGetRoom(jid);
|
||||
ChatOmemo_ajaxEnableRoomState();
|
||||
|
||||
Chat.groupChatMembers.forEach(member => {
|
||||
if (!store.isJidResolved(member)) {
|
||||
ChatOmemo_ajaxGetDevicesList(member);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Chat_ajaxGet(jid);
|
||||
ChatOmemo_ajaxEnableContactState();
|
||||
ChatOmemo_ajaxGetDevicesList(jid);
|
||||
}
|
||||
},
|
||||
disableContactState: function (jid, muc) {
|
||||
var store = new ChatOmemoStorage();
|
||||
store.setContactState(jid, false);
|
||||
Chat.setOmemoState("disabled");
|
||||
|
||||
if (muc) {
|
||||
ChatOmemo_ajaxDisableRoomState();
|
||||
} else {
|
||||
ChatOmemo_ajaxDisableContactState();
|
||||
}
|
||||
},
|
||||
getContactState: async function (jid) {
|
||||
var store = new ChatOmemoStorage();
|
||||
return store.getContactState(jid);
|
||||
},
|
||||
encryptJid: function (plaintext, jid) {
|
||||
var store = new ChatOmemoStorage();
|
||||
let promises = store.filter('.session' + jid)
|
||||
.map(key => key.split(/[\s.]+/).pop())
|
||||
.map(deviceId => store.getSessionState(jid + '.' + deviceId).then(state => {
|
||||
if (state) {
|
||||
return this.encryptDevice(plaintext, jid, deviceId);
|
||||
}
|
||||
|
||||
return Promise.resolve(false);
|
||||
}));
|
||||
|
||||
return Promise.all(promises).then(result => {
|
||||
return result.filter(encrypted => encrypted !== false);
|
||||
});
|
||||
},
|
||||
encryptDevice: function (plaintext, jid, deviceId) {
|
||||
var address = new libsignal.SignalProtocolAddress(jid, parseInt(deviceId, 10));
|
||||
var store = new ChatOmemoStorage();
|
||||
var sessionCipher = new libsignal.SessionCipher(store, address);
|
||||
|
||||
return sessionCipher.encrypt(plaintext)
|
||||
.then(payload => ({ 'payload': payload, 'device': deviceId }));
|
||||
},
|
||||
decryptDevice: async function (ciphertext, preKey, jid, deviceId) {
|
||||
var address = new libsignal.SignalProtocolAddress(jid, parseInt(deviceId, 10));
|
||||
var store = new ChatOmemoStorage();
|
||||
var sessionCipher = new libsignal.SessionCipher(store, address);
|
||||
|
||||
return (preKey)
|
||||
? await sessionCipher.decryptPreKeyWhisperMessage(ciphertext, 'binary')
|
||||
: await sessionCipher.decryptWhisperMessage(ciphertext, 'binary');
|
||||
},
|
||||
searchEncryptedFile: function (plaintext) {
|
||||
let lines = plaintext.split('\n');
|
||||
let matches = lines[0].match(AESGCM_REGEX);
|
||||
|
||||
if (!matches) {
|
||||
return plaintext;
|
||||
}
|
||||
let [match, , filename, extension, hash] = matches;
|
||||
return '<i class="material-symbols">file_download</i> <a href="#" class="encrypted_file" onclick="ChatOmemo.getEncryptedFile(\''
|
||||
+ lines[0]
|
||||
+ '\')">'
|
||||
+ filename
|
||||
+ '</a>';
|
||||
},
|
||||
getEncryptedFile: async function (encryptedUrl) {
|
||||
Chat.enableSending();
|
||||
|
||||
let [, url, filename, , hash] = encryptedUrl.match(AESGCM_REGEX);
|
||||
url = 'https://' + url;
|
||||
let response;
|
||||
|
||||
try {
|
||||
response = await fetch(url)
|
||||
} catch (e) {
|
||||
console.log('Cannot get the following file: ' + url)
|
||||
return null;
|
||||
}
|
||||
|
||||
if (response.status >= 200 && response.status < 400) {
|
||||
let cipher = await response.arrayBuffer();
|
||||
const iv = hash.slice(0, 24);
|
||||
const key = hash.slice(24);
|
||||
|
||||
const keyObj = await crypto.subtle.importKey('raw', MovimUtils.hexToArrayBuffer(key), 'AES-GCM', false, ['decrypt']);
|
||||
const algo = {
|
||||
'name': 'AES-GCM',
|
||||
'iv': MovimUtils.hexToArrayBuffer(iv),
|
||||
};
|
||||
let plainFile = await crypto.subtle.decrypt(algo, keyObj, cipher);
|
||||
|
||||
const file = new File([plainFile], filename);
|
||||
const link = document.createElement('a');
|
||||
link.href = URL.createObjectURL(file);
|
||||
link.download = filename;
|
||||
link.click();
|
||||
|
||||
Chat.disableSending();
|
||||
|
||||
URL.revokeObjectURL(link.href)
|
||||
}
|
||||
},
|
||||
resolveContactFingerprints: function (jid) {
|
||||
let omemoStorage = new ChatOmemoStorage;
|
||||
|
||||
let sessionLoaded = [];
|
||||
omemoStorage.getSessionsIds(jid).forEach(deviceId => {
|
||||
var address = new libsignal.SignalProtocolAddress(jid, deviceId);
|
||||
sessionLoaded.push(omemoStorage.loadSession(address.toString()));
|
||||
});
|
||||
|
||||
const promise = new Promise((resolve) => {
|
||||
Promise.all(sessionLoaded).then(sessions => {
|
||||
let remoteKeys = [];
|
||||
|
||||
sessions.forEach(session => {
|
||||
let parsed = Object.values(JSON.parse(session).sessions)[0];
|
||||
remoteKeys.push({
|
||||
jid: jid,
|
||||
self: false,
|
||||
bundleid: parsed.registrationId.toString(),
|
||||
fingerprint: btoa(parsed.indexInfo.remoteIdentityKey)
|
||||
});
|
||||
});
|
||||
|
||||
resolve(remoteKeys);
|
||||
});
|
||||
});
|
||||
|
||||
return promise;
|
||||
}
|
||||
}
|
||||
|
||||
MovimWebsocket.attach(async () => {
|
||||
var store = new ChatOmemoStorage();
|
||||
const localDeviceId = await store.getLocalRegistrationId();
|
||||
if (!localDeviceId) {
|
||||
ChatOmemo_ajaxGetDevicesList(USER_JID);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,91 @@
|
||||
var ChatOmemoDB = {
|
||||
dbName: 'movim',
|
||||
storeName: 'decryptedMessages',
|
||||
version: 3,
|
||||
|
||||
cachedMessages: {},
|
||||
|
||||
db: null,
|
||||
|
||||
setup: async function() {
|
||||
var db = window.indexedDB.open(this.dbName, this.version);
|
||||
db.onupgradeneeded = function (event) {
|
||||
var db = event.target.result;
|
||||
db.createObjectStore(ChatOmemoDB.storeName, { keyPath: "id" });
|
||||
}
|
||||
|
||||
db.onsuccess = function() {
|
||||
ChatOmemoDB.db = db;
|
||||
}
|
||||
|
||||
db.onerror = function() {
|
||||
console.log('Movim Decrypted Messages database cannot be opened properly');
|
||||
}
|
||||
},
|
||||
|
||||
loadMessagesByIds: async function(ids) {
|
||||
ChatOmemoDB.cachedMessages = {};
|
||||
|
||||
var tx = ChatOmemoDB.db.result.transaction(ChatOmemoDB.storeName, 'readonly');
|
||||
var os = tx.objectStore(ChatOmemoDB.storeName);
|
||||
|
||||
var promises = [];
|
||||
|
||||
ids.forEach(id => {
|
||||
var request = os.get(id);
|
||||
|
||||
promises.push(new Promise((resolve, reject) => {
|
||||
request.onsuccess = function (event) {
|
||||
if (event.target.result) {
|
||||
ChatOmemoDB.cachedMessages[id] = event.target.result.body;
|
||||
}
|
||||
resolve();
|
||||
}
|
||||
|
||||
request.onerror = function() {
|
||||
resolve();
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
return Promise.all(promises);
|
||||
},
|
||||
|
||||
putMessage: async function(id, body) {
|
||||
let promise = new Promise(resolve => {
|
||||
var tx = ChatOmemoDB.db.result.transaction(ChatOmemoDB.storeName, 'readwrite');
|
||||
var os = tx.objectStore(ChatOmemoDB.storeName);
|
||||
|
||||
var request = os.put({"id" : id, "body" : body});
|
||||
request.onsuccess = function(event) {
|
||||
resolve(body);
|
||||
}
|
||||
});
|
||||
|
||||
return promise;
|
||||
},
|
||||
|
||||
getMessage: async function(id) {
|
||||
let promise = new Promise(resolve => {
|
||||
if (id in ChatOmemoDB.cachedMessages) {
|
||||
resolve(ChatOmemoDB.cachedMessages[String(id)]);
|
||||
} else {
|
||||
var tx = ChatOmemoDB.db.result.transaction(ChatOmemoDB.storeName, 'readonly');
|
||||
var os = tx.objectStore(ChatOmemoDB.storeName);
|
||||
|
||||
var request = os.get(id);
|
||||
request.onsuccess = function(event) {
|
||||
if (event.target.result) {
|
||||
resolve(event.target.result.body);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return promise;
|
||||
}
|
||||
}
|
||||
|
||||
ChatOmemoDB.setup();
|
||||
@@ -0,0 +1,250 @@
|
||||
function ChatOmemoStorage() {
|
||||
this.jid = USER_JID;
|
||||
}
|
||||
|
||||
ChatOmemoStorage.prototype = {
|
||||
Direction: {
|
||||
SENDING: 1,
|
||||
RECEIVING: 2,
|
||||
},
|
||||
|
||||
// Storage is versionned to ensure a proper reset when changes are made
|
||||
storageVersion: '2',
|
||||
|
||||
// Generic methods
|
||||
put: function (key, value) {
|
||||
if (key === undefined || value === undefined || key === null || value === null)
|
||||
throw new Error("Tried to store undefined/null");
|
||||
|
||||
return localStorage.setObject(this.storageVersion + '.' + key, value);
|
||||
},
|
||||
get: function (key, defaultValue) {
|
||||
if (key === null || key === undefined)
|
||||
throw new Error("Tried to get value for undefined/null key");
|
||||
if (this.storageVersion + '.' + key in localStorage) {
|
||||
return localStorage.getObject(this.storageVersion + '.' + key);
|
||||
} else {
|
||||
return defaultValue;
|
||||
}
|
||||
},
|
||||
remove: function (key) {
|
||||
if (key === null || key === undefined)
|
||||
throw new Error("Tried to remove value for undefined/null key");
|
||||
|
||||
localStorage.removeItem(this.storageVersion + '.' + key);
|
||||
},
|
||||
filter: function (search) {
|
||||
return Object.keys(localStorage).filter(key => key.startsWith(this.storageVersion + '.' + this.jid + search));
|
||||
},
|
||||
|
||||
// OMEMO specific methods
|
||||
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');
|
||||
|
||||
if (!identityKeyPair) return Promise.reject();
|
||||
|
||||
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");
|
||||
}
|
||||
if (!(identityKey instanceof ArrayBuffer)) {
|
||||
throw new Error("Expected identityKey to be an ArrayBuffer");
|
||||
}
|
||||
var trusted = this.get(this.jid + '.identityKey' + identifier);
|
||||
if (trusted === undefined) {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
// movim-rs: identities are stored per JID (not per device) so a contact with several
|
||||
// devices would always be rejected. Blind Trust Before Verification, like Conversations:
|
||||
// the fingerprints can still be checked and disabled in the contact drawer.
|
||||
return Promise.resolve(true);
|
||||
},
|
||||
loadIdentityKey: function (identifier) {
|
||||
if (identifier === null || identifier === undefined)
|
||||
throw new Error("Tried to get identity key for undefined/null key");
|
||||
return Promise.resolve(MovimUtils.base64ToArrayBuffer(this.get(this.jid + '.identityKey' + identifier)));
|
||||
},
|
||||
saveIdentity: function (identifier, identityKey) {
|
||||
if (identifier === null || identifier === undefined)
|
||||
throw new Error("Tried to put identity key for undefined/null key");
|
||||
|
||||
var address = new libsignal.SignalProtocolAddress.fromString(identifier);
|
||||
|
||||
var existing = this.get(this.jid + '.identityKey' + address.getName());
|
||||
this.put(this.jid + '.identityKey' + address.getName(), MovimUtils.arrayBufferToBase64(identityKey))
|
||||
|
||||
if (existing && toString(identityKey) !== toString(existing)) {
|
||||
return Promise.resolve(true);
|
||||
} else {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
loadPreKey: function (keyId) {
|
||||
var res = this.get(this.jid + '.25519KeypreKey' + keyId);
|
||||
if (res !== undefined) {
|
||||
res = {
|
||||
pubKey: MovimUtils.base64ToArrayBuffer(res.pubKey),
|
||||
privKey: MovimUtils.base64ToArrayBuffer(res.privKey)
|
||||
};
|
||||
}
|
||||
return Promise.resolve(res);
|
||||
},
|
||||
storePreKey: function (keyId, keyPair) {
|
||||
keyPair.pubKey = MovimUtils.arrayBufferToBase64(keyPair.pubKey);
|
||||
keyPair.privKey = MovimUtils.arrayBufferToBase64(keyPair.privKey);
|
||||
return Promise.resolve(this.put(this.jid + '.25519KeypreKey' + keyId, keyPair));
|
||||
},
|
||||
removePreKey: function (keyId) {
|
||||
return Promise.resolve(this.remove(this.jid + '.25519KeypreKey' + keyId));
|
||||
},
|
||||
|
||||
loadCompleteSignedPreKey: function (keyId) {
|
||||
var res = this.get(this.jid + '.25519KeysignedKey' + keyId);
|
||||
if (res !== undefined) {
|
||||
res = {
|
||||
keyPair: {
|
||||
pubKey: MovimUtils.base64ToArrayBuffer(res.keyPair.pubKey),
|
||||
privKey: MovimUtils.base64ToArrayBuffer(res.keyPair.privKey),
|
||||
},
|
||||
signature: MovimUtils.base64ToArrayBuffer(res.signature),
|
||||
keyId: keyId
|
||||
};
|
||||
}
|
||||
return res;
|
||||
},
|
||||
loadSignedPreKey: function (keyId) {
|
||||
return Promise.resolve(this.loadCompleteSignedPreKey(keyId).keyPair);
|
||||
},
|
||||
storeSignedPreKey: function (keyId, key) {
|
||||
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(this.jid + '.25519KeysignedKey' + keyId, key));
|
||||
},
|
||||
removeSignedPreKey: function (keyId) {
|
||||
return Promise.resolve(this.remove(this.jid + '.25519KeysignedKey' + keyId));
|
||||
},
|
||||
|
||||
loadSession: function (identifier) {
|
||||
return Promise.resolve(this.get(this.jid + '.session' + identifier));
|
||||
},
|
||||
storeSession: function (identifier, record) {
|
||||
return Promise.resolve(this.put(this.jid + '.session' + identifier, record));
|
||||
},
|
||||
removeSession: function (identifier) {
|
||||
return Promise.resolve(this.remove(this.jid + '.session' + identifier));
|
||||
},
|
||||
removeAllSessions: function () {
|
||||
let filtered = this.filter('.session');
|
||||
|
||||
for (key in filtered) {
|
||||
localStorage.removeItem(filtered[key]);
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
},
|
||||
getOwnSessions: function () {
|
||||
return this.filter('.session' + this.jid);
|
||||
},
|
||||
getSessions: function (identifier) {
|
||||
return this.filter('.session' + identifier);
|
||||
},
|
||||
getOwnSessionsIds: function () {
|
||||
return this.filter('.session' + this.jid).map(sessionId => sessionId.split('.').pop());
|
||||
},
|
||||
getSessionsIds: function (identifier) {
|
||||
return this.filter('.session' + identifier).map(sessionId => sessionId.split('.').pop());
|
||||
},
|
||||
|
||||
isJidResolved: function (identifier) {
|
||||
return this.hasContactTombstone(identifier) || this.filter('.session' + identifier).length > 0;
|
||||
},
|
||||
|
||||
removeAllSessionsOfJid: function (identifier) {
|
||||
let filtered = this.filter('.session' + identifier);
|
||||
|
||||
for (id in filtered) {
|
||||
localStorage.removeItem(filtered[id]);
|
||||
}
|
||||
|
||||
this.remove(this.jid + '.contact' + identifier);
|
||||
|
||||
return Promise.resolve();
|
||||
},
|
||||
|
||||
setContactState: function (jid, state) {
|
||||
return Promise.resolve(this.put(this.jid + '.contact' + jid, state));
|
||||
},
|
||||
removeContactState: function (jid) {
|
||||
return Promise.resolve(this.remove(this.jid + '.contact' + jid));
|
||||
},
|
||||
hasContactState: function(jid) {
|
||||
return (this.get(this.jid + '.contact' + jid) !== undefined);
|
||||
},
|
||||
getContactState: function (jid) {
|
||||
let state = Boolean(this.get(this.jid + '.contact' + jid));
|
||||
return Promise.resolve(state);
|
||||
},
|
||||
|
||||
/**
|
||||
* Tombstone to know if a contact doesn't have OMEMO at the moment
|
||||
*/
|
||||
setContactTombstone: function (jid) {
|
||||
return Promise.resolve(this.put(this.jid + '.contact.tombstone' + jid, true));
|
||||
},
|
||||
removeContactTombstone: function (jid) {
|
||||
return Promise.resolve(this.remove(this.jid + '.contact.tombstone' + jid));
|
||||
},
|
||||
hasContactTombstone: function(jid) {
|
||||
return (this.get(this.jid + '.contact.tombstone' + jid) == true);
|
||||
},
|
||||
|
||||
getSessionState: function (identifier) {
|
||||
return this.loadSession(identifier).then(json => {
|
||||
if (json !== undefined) {
|
||||
let session = JSON.parse(json);
|
||||
return (session.state === true || session.state === undefined);
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
},
|
||||
setSessionState: function (identifier, enabled) {
|
||||
this.loadSession(identifier).then(json => {
|
||||
if (json !== undefined) {
|
||||
let session = JSON.parse(json);
|
||||
session.state = Boolean(enabled);
|
||||
this.storeSession(identifier, JSON.stringify(session));
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
toString: function(thing) {
|
||||
if (typeof thing == 'string') {
|
||||
return thing;
|
||||
}
|
||||
return new dcodeIO.ByteBuffer.wrap(thing).toString('binary');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
[omemo]
|
||||
fingerprints_title = Fingerprints
|
||||
fingerprints = OMEMO Fingerprints
|
||||
generating_bundle = "Generating the encryption keys, please wait…"
|
||||
generated_bundle = Encryption keys generated
|
||||
resolving_devices = Resolving OMEMO devices
|
||||
building_sessions = Building encrypted sessions
|
||||
building_own_sessions = Building own encryption sessions
|
||||
encrypted = Encrypted message
|
||||
encrypted_disabled = Encryption disabled
|
||||
encrypted_loading = Send a message to build the encrypted session
|
||||
enable_contact = Enabling encryption for the contact
|
||||
enable_room = Enabling encryption for the chatroom
|
||||
disable_contact = Disabling encryption for the contact
|
||||
disable_room = Disabling encryption for the chatroom
|
||||
last_activity = Last activity
|
||||
sessions_built = %s sessions built
|
||||
Reference in New Issue
Block a user