@jsxc/jsxc
Version:
Real-time XMPP chat application with video calls, file transfer and encrypted communication
287 lines (218 loc) • 8.89 kB
text/typescript
import IStorage from '../../../Storage.interface';
import { IContact as Contact } from '../../../Contact.interface';
import { IMessage as Message } from '../../../Message.interface';
import { IConnection } from '../../../connection/Connection.interface';
import Store from './Store';
import Peer from './Peer';
import Bootstrap from './Bootstrap';
import JID from '../../../JID';
import { IJID } from '../../../JID.interface';
import Stanza from '../util/Stanza';
import { NS_BASE } from '../util/Const';
import ArrayBufferUtils from '../util/ArrayBuffer';
import * as AES from '../util/AES';
import Device, { Trust } from './Device';
import { Strophe } from '../../../vendor/Strophe';
import BundleManager from './BundleManager';
import IdentityManager from './IdentityManager';
import Translation from '@util/Translation';
import Log from '@util/Log';
export default class Omemo {
private store: Store;
private peers: any = {};
private bootstrap: Bootstrap;
private bundleManager: BundleManager;
private identityManager: IdentityManager;
private deviceName: string;
private localPeer: Peer;
constructor(storage: IStorage, private connection: IConnection) {
this.deviceName = connection.getJID().bare;
this.store = new Store(storage);
this.bundleManager = new BundleManager(connection.getPEPService(), this.store);
this.localPeer = new Peer(this.deviceName, this);
}
public getIdentityManager(): IdentityManager {
if (!this.identityManager) {
this.identityManager = new IdentityManager(this.store, this.bundleManager);
}
return this.identityManager;
}
public getStore(): Store {
return this.store;
}
public getBundleManager(): BundleManager {
return this.bundleManager;
}
public async cleanUpDeviceList() {
let localIdentifier = this.store.getLocalDeviceName();
let localDeviceId = this.store.getLocalDeviceId();
this.store.setDeviceList(localIdentifier, [localDeviceId]);
await this.bundleManager.deleteDeviceList();
await this.bundleManager.publishDeviceId(localDeviceId);
return localDeviceId;
}
public storeDeviceList(identifier: string, deviceList: number[]) {
let ownJid = this.connection.getJID();
this.store.setDeviceList(identifier, deviceList);
if (ownJid.bare === identifier) {
this.makeSureOwnDeviceIdIsInList(deviceList);
}
}
private makeSureOwnDeviceIdIsInList(deviceList: number[]) {
let ownDeviceId = this.store.getLocalDeviceId();
if (
this.store.isPublished() &&
typeof ownDeviceId === 'number' &&
!isNaN(ownDeviceId) &&
deviceList.indexOf(ownDeviceId) < 0
) {
this.bundleManager.publishDeviceId(ownDeviceId);
}
}
public prepare(): Promise<void> {
return this.getBootstrap().prepare();
}
public isSupported(contact: Contact): boolean {
let devices = this.getDevices(contact);
return devices.length > 0;
}
public getTrust(contact: Contact): Trust {
let peer = this.getPeer(contact.getJid());
let peerTrust = peer.getTrust();
let localPeerTrust = this.localPeer.getTrust();
return Math.min(peerTrust, localPeerTrust);
}
public isTrusted(contact: Contact): boolean {
let peer = this.getPeer(contact.getJid());
return peer.getTrust() !== Trust.unknown && this.localPeer.getTrust() !== Trust.unknown;
}
public async trustOnFirstUse(contact: Contact): Promise<boolean> {
let peer = this.getPeer(contact.getJid());
let [peerTrustedOnFirstUse, localPeerTrustedOnFirstUse] = await Promise.all([
peer.trustOnFirstUse(),
this.localPeer.trustOnFirstUse(),
]);
if (peerTrustedOnFirstUse) {
contact.addSystemMessage(Translation.t('Blindly_trusted_peer_on_first_use'));
}
if (localPeerTrustedOnFirstUse) {
contact.addSystemMessage(Translation.t('Blindly_trusted_all_your_own_devices_on_first_use'));
}
return peerTrustedOnFirstUse && localPeerTrustedOnFirstUse;
}
public getDevices(contact?: Contact): Device[] {
let peer: Peer;
if (contact) {
peer = this.getPeer(contact.getJid());
} else {
peer = this.localPeer;
}
return peer.getDevices();
}
public isTrustUnknown(contact: Contact): boolean {
let peer = this.getPeer(contact.getJid());
let peerNewDevices = peer.getTrust() === Trust.unknown;
let localPeerNewDevices = this.localPeer.getTrust() === Trust.unknown;
return peerNewDevices || localPeerNewDevices;
}
public encrypt(contact: Contact, message: Message, xmlElement: Strophe.Builder) {
let peer = this.getPeer(contact.getJid());
let plaintextMessage = message.getPlaintextMessage();
return peer
.encrypt(this.localPeer, plaintextMessage)
.then(encryptedMessages => {
let stanza = Stanza.buildEncryptedStanza(encryptedMessages, this.store.getLocalDeviceId());
$(xmlElement.tree()).find(`html[xmlns="${Strophe.NS.XHTML_IM}"]`).remove();
$(xmlElement.tree()).find('>body').remove();
$(xmlElement.tree()).find('>data[xmlns="urn:xmpp:bob"]').remove();
xmlElement.cnode(stanza.tree());
xmlElement
.up()
.c('store', {
xmlns: 'urn:xmpp:hints',
})
.up();
xmlElement
.c('body')
.t('***' + Translation.t('You_received_an_OMEMO_encrypted_message') + '***')
.up();
message.setEncrypted(true);
return [message, xmlElement];
})
.catch(msg => {
message.setErrorMessage(Translation.t('Message_was_not_sent'));
message.setEncrypted(false);
contact.addSystemMessage(typeof msg === 'string' ? msg : msg.toString());
throw msg;
});
}
public async decrypt(stanza): Promise<{ plaintext: string; trust: Trust } | void> {
let messageElement = $(stanza);
if (messageElement.prop('tagName') !== 'message') {
throw new Error('Root element is no message element');
}
let encryptedElement = $(stanza).find(`>encrypted[xmlns="${NS_BASE}"]`);
if (encryptedElement.length === 0) {
throw new Error('No encrypted stanza found');
}
let from = new JID(messageElement.attr('from'));
let encryptedData = Stanza.parseEncryptedStanza(encryptedElement);
if (!encryptedData) {
throw new Error('Could not parse encrypted stanza');
}
let ownDeviceId = this.store.getLocalDeviceId();
let ownPreKeyFiltered = encryptedData.keys.filter(function (preKey) {
return ownDeviceId === preKey.deviceId;
});
if (ownPreKeyFiltered.length !== 1) {
return Promise.reject(`Found ${ownPreKeyFiltered.length} PreKeys which match my device id (${ownDeviceId}).`);
}
let ownPreKey = ownPreKeyFiltered[0];
let peer = this.getPeer(from);
let deviceDecryptionResult;
try {
deviceDecryptionResult = await peer.decrypt(
encryptedData.sourceDeviceId,
ownPreKey.ciphertext,
ownPreKey.preKey
);
} catch (err) {
throw new Error('Error during decryption: ' + err);
}
let exportedKey = deviceDecryptionResult.plaintextKey;
let exportedAESKey = exportedKey.slice(0, 16);
let authenticationTag = exportedKey.slice(16);
if (authenticationTag.byteLength < 16) {
if (authenticationTag.byteLength > 0) {
throw new Error('Authentication tag too short');
}
Log.info(`Authentication tag is only ${authenticationTag.byteLength} byte long`);
}
if (!encryptedData.payload) {
throw new Error('We received a KeyTransportElement');
}
if (ownPreKey.preKey) {
this.bundleManager.refreshBundle().then(bundle => {
this.bundleManager.publishBundle(bundle);
});
}
let iv = (<any>encryptedData).iv;
let ciphertextAndAuthenticationTag = ArrayBufferUtils.concat(encryptedData.payload, authenticationTag);
return {
plaintext: await AES.decrypt(exportedAESKey, iv, ciphertextAndAuthenticationTag),
trust: deviceDecryptionResult.deviceTrust,
};
}
private getPeer(jid: IJID): Peer {
if (!this.peers[jid.bare]) {
this.peers[jid.bare] = new Peer(jid.bare, this);
}
return this.peers[jid.bare];
}
private getBootstrap(): Bootstrap {
if (!this.bootstrap) {
this.bootstrap = new Bootstrap(this.deviceName, this.store, this.bundleManager);
}
return this.bootstrap;
}
}