var Account = { resetPassword: function () { var form = document.querySelector('form[name=password]'); form.reset(); document.querySelector('#password_save').className = 'button color flat'; }, resolveSessionsStates: function () { var store = new ChatOmemoStorage(); store.getSessionsIds(store.jid).map(id => { store.getSessionState(store.jid + '.' + id).then(state => { if (state) { let checkbox = document.querySelector('input[name=accountsessionstate_' + id + ']'); if (checkbox) { checkbox.checked = true; } } }) }); }, toggleFingerprintState: function (checkbox) { var store = new ChatOmemoStorage(); store.setSessionState(checkbox.dataset.identifier, checkbox.checked); }, deleteBundle: async function (bundleid) { ChatOmemo.resolveContactFingerprints(USER_JID).then(remoteKeys => { remoteKeys = remoteKeys.filter(remoteKey => remoteKey.bundleid == bundleid ); Account_ajaxDeleteBundle(remoteKeys[0]) }); }, deleteBundleConfirm: function (bundleid) { var store = new ChatOmemoStorage(); var address = new libsignal.SignalProtocolAddress(USER_JID, bundleid); store.removeSession(address); Account_ajaxDeleteBundleConfirm(bundleid, store.getOwnSessionsIds()); }, /** * OMEMO key backup of this device: identity, prekeys and sessions, * always encrypted (PBKDF2 + AES-256-GCM), even with an empty password. * Everything happens in the browser, the server never sees the keys. */ backupFormat: 'movim-omemo-backup', backupIterations: 600000, backupCipherKey: async function (password, salt, iterations) { const base = await crypto.subtle.importKey('raw', new TextEncoder().encode(password), 'PBKDF2', false, ['deriveKey']); return crypto.subtle.deriveKey( { name: 'PBKDF2', salt: salt, iterations: iterations, hash: 'SHA-256' }, base, { name: 'AES-GCM', length: 256 }, false, ['encrypt', 'decrypt'] ); }, backupToBase64: function (buffer) { let bytes = new Uint8Array(buffer); let binary = ''; for (let i = 0; i < bytes.length; i += 0x8000) { binary += String.fromCharCode.apply(null, bytes.subarray(i, i + 0x8000)); } return btoa(binary); }, backupError: function (text) { document.querySelector('#omemo_backup_error').textContent = text; }, omemoBackup: async function () { let form = document.querySelector('form[name=omemo_backup]'); let password = form.password.value; if (password != form.password_confirmation.value) return Account.backupError(form.dataset.notsame); let store = new ChatOmemoStorage(); let prefix = store.storageVersion + '.' + store.jid + '.'; let keys = {}; Object.keys(localStorage).filter(k => k.startsWith(prefix)).forEach(k => keys[k] = localStorage.getItem(k)); let deviceId = await store.getLocalRegistrationId(); let payload = JSON.stringify({ jid: store.jid, deviceId: deviceId, keys: keys }); const salt = crypto.getRandomValues(new Uint8Array(16)); const iv = crypto.getRandomValues(new Uint8Array(12)); const key = await Account.backupCipherKey(password, salt, Account.backupIterations); const cipher = await crypto.subtle.encrypt({ name: 'AES-GCM', iv: iv }, key, new TextEncoder().encode(payload)); let backup = JSON.stringify({ format: Account.backupFormat, version: 1, jid: store.jid, deviceId: deviceId, created: new Date().toISOString(), kdf: { name: 'PBKDF2-SHA256', iterations: Account.backupIterations, salt: Account.backupToBase64(salt) }, iv: Account.backupToBase64(iv), data: Account.backupToBase64(cipher) }); let link = document.createElement('a'); link.href = URL.createObjectURL(new Blob([backup], { type: 'application/json' })); link.download = 'movim-omemo-' + store.jid + '-' + deviceId + '-' + new Date().toISOString().slice(0, 10) + '.json'; link.click(); URL.revokeObjectURL(link.href); Dialog_ajaxClear(); }, omemoRestore: async function () { let form = document.querySelector('form[name=omemo_restore]'); let file = form.file.files[0]; if (!file) return Account.backupError(form.dataset.nofile); let backup, payload; try { backup = JSON.parse(await file.text()); if (backup.format != Account.backupFormat || backup.version != 1) throw new Error(); } catch (e) { return Account.backupError(form.dataset.invalid); } if (backup.jid != USER_JID) return Account.backupError(form.dataset.otheraccount + ' ' + backup.jid); try { const key = await Account.backupCipherKey(form.password.value, MovimUtils.base64ToArrayBuffer(backup.kdf.salt), backup.kdf.iterations); const plain = await crypto.subtle.decrypt( { name: 'AES-GCM', iv: MovimUtils.base64ToArrayBuffer(backup.iv) }, key, MovimUtils.base64ToArrayBuffer(backup.data)); payload = JSON.parse(new TextDecoder().decode(plain)); } catch (e) { return Account.backupError(form.dataset.wrongpassword); } let store = new ChatOmemoStorage(); let prefix = store.storageVersion + '.' + store.jid + '.'; if (Object.keys(payload.keys).some(k => !k.startsWith(prefix))) { return Account.backupError(form.dataset.invalid); } // Replace the keys of this device with the backup ones let replaced = await store.getLocalRegistrationId(); Object.keys(localStorage).filter(k => k.startsWith(prefix)).forEach(k => localStorage.removeItem(k)); Object.entries(payload.keys).forEach(([k, v]) => localStorage.setItem(k, v)); localStorage.setObject(OMEMO_RESTORED_KEY, { replaced: replaced != payload.deviceId ? replaced ?? null : null }); // The page reload publishes the restored key (ChatOmemo.ownDevicesReceived) location.reload(); }, refreshFingerprints: async function () { ChatOmemo.resolveContactFingerprints(USER_JID).then(remoteKeys => { let omemoStorage = new ChatOmemoStorage; omemoStorage.getLocalRegistrationId().then(localBundleId => { omemoStorage.getIdentityKeyPair().then(identityKey => { // Remove it if its already there remoteKeys = remoteKeys.filter(remoteKey => remoteKey.bundleid != localBundleId.toString() ); remoteKeys.unshift({ jid: USER_JID, self: true, bundleid: localBundleId.toString(), fingerprint: MovimUtils.arrayBufferToBase64(identityKey.pubKey) }); Account_ajaxHttpGetFingerprints(remoteKeys); }); }); }); } } MovimWebsocket.attach(function () { if (OMEMO_ENABLED) Account.refreshFingerprints(); Account_ajaxHttpGetPresences(); Account_ajaxGetGateways(); });