vietnamese
This commit is contained in:
@@ -39,6 +39,115 @@ var Account = {
|
||||
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user