forked from mystiq/hydrogen-web
key backup: add disable button,and enabling add dehydrated device option
This commit is contained in:
parent
3b3751c827
commit
44a26fd340
11 changed files with 201 additions and 67 deletions
|
@ -15,6 +15,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {ViewModel} from "./ViewModel.js";
|
import {ViewModel} from "./ViewModel.js";
|
||||||
|
import {KeyType} from "../matrix/ssss/index.js";
|
||||||
|
|
||||||
export class AccountSetupViewModel extends ViewModel {
|
export class AccountSetupViewModel extends ViewModel {
|
||||||
constructor(accountSetup) {
|
constructor(accountSetup) {
|
||||||
|
@ -35,10 +36,10 @@ export class AccountSetupViewModel extends ViewModel {
|
||||||
return this._accountSetup.encryptedDehydratedDevice.deviceId;
|
return this._accountSetup.encryptedDehydratedDevice.deviceId;
|
||||||
}
|
}
|
||||||
|
|
||||||
tryDecryptDehydratedDevice(password) {
|
async tryDecryptDehydratedDevice(password) {
|
||||||
const {encryptedDehydratedDevice} = this._accountSetup;
|
const {encryptedDehydratedDevice} = this._accountSetup;
|
||||||
if (encryptedDehydratedDevice) {
|
if (encryptedDehydratedDevice) {
|
||||||
this._dehydratedDevice = encryptedDehydratedDevice.decrypt(password);
|
this._dehydratedDevice = await encryptedDehydratedDevice.decrypt(KeyType.RecoveryKey, password);
|
||||||
this.emitChange("deviceDecrypted");
|
this.emitChange("deviceDecrypted");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,19 +15,57 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {ViewModel} from "../../ViewModel.js";
|
import {ViewModel} from "../../ViewModel.js";
|
||||||
|
import {KeyType} from "../../../matrix/ssss/index.js";
|
||||||
|
import {createEnum} from "../../../utils/enum.js";
|
||||||
|
|
||||||
|
const Status = createEnum("Enabled", "SetupKey", "SetupPhrase", "Pending");
|
||||||
|
|
||||||
export class SessionBackupViewModel extends ViewModel {
|
export class SessionBackupViewModel extends ViewModel {
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
super(options);
|
super(options);
|
||||||
this._session = options.session;
|
this._session = options.session;
|
||||||
this._showKeySetup = true;
|
|
||||||
this._error = null;
|
this._error = null;
|
||||||
this._isBusy = false;
|
this._isBusy = false;
|
||||||
|
this._dehydratedDeviceId = undefined;
|
||||||
|
this._status = undefined;
|
||||||
|
this._reevaluateStatus();
|
||||||
this.track(this._session.hasSecretStorageKey.subscribe(() => {
|
this.track(this._session.hasSecretStorageKey.subscribe(() => {
|
||||||
|
if (this._reevaluateStatus()) {
|
||||||
this.emitChange("status");
|
this.emitChange("status");
|
||||||
|
}
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_reevaluateStatus() {
|
||||||
|
if (this._isBusy) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let status;
|
||||||
|
const hasSecretStorageKey = this._session.hasSecretStorageKey.get();
|
||||||
|
if (hasSecretStorageKey === true) {
|
||||||
|
status = this._session.sessionBackup ? Status.Enabled : Status.SetupKey;
|
||||||
|
} else if (hasSecretStorageKey === false) {
|
||||||
|
status = Status.SetupKey;
|
||||||
|
} else {
|
||||||
|
status = Status.Pending;
|
||||||
|
}
|
||||||
|
const changed = status !== this._status;
|
||||||
|
this._status = status;
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
get decryptAction() {
|
||||||
|
return this.i18n`Set up`;
|
||||||
|
}
|
||||||
|
|
||||||
|
offerDehydratedDeviceSetup() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
get dehydratedDeviceId() {
|
||||||
|
return this._dehydratedDeviceId;
|
||||||
|
}
|
||||||
|
|
||||||
get isBusy() {
|
get isBusy() {
|
||||||
return this._isBusy;
|
return this._isBusy;
|
||||||
}
|
}
|
||||||
|
@ -37,15 +75,7 @@ export class SessionBackupViewModel extends ViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
get status() {
|
get status() {
|
||||||
if (this._session.sessionBackup) {
|
return this._status;
|
||||||
return "enabled";
|
|
||||||
} else {
|
|
||||||
if (this._session.hasSecretStorageKey.get() === false) {
|
|
||||||
return this._showKeySetup ? "setupKey" : "setupPhrase";
|
|
||||||
} else {
|
|
||||||
return "pending";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get error() {
|
get error() {
|
||||||
|
@ -53,46 +83,61 @@ export class SessionBackupViewModel extends ViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
showPhraseSetup() {
|
showPhraseSetup() {
|
||||||
this._showKeySetup = false;
|
if (this._status === Status.SetupKey) {
|
||||||
|
this._status = Status.SetupPhrase;
|
||||||
this.emitChange("status");
|
this.emitChange("status");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
showKeySetup() {
|
showKeySetup() {
|
||||||
this._showKeySetup = true;
|
if (this._status === Status.SetupPhrase) {
|
||||||
|
this._status = Status.SetupKey;
|
||||||
this.emitChange("status");
|
this.emitChange("status");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async enterSecurityPhrase(passphrase) {
|
async _enterCredentials(keyType, credential, setupDehydratedDevice) {
|
||||||
if (passphrase) {
|
if (credential) {
|
||||||
try {
|
try {
|
||||||
this._isBusy = true;
|
this._isBusy = true;
|
||||||
this.emitChange("isBusy");
|
this.emitChange("isBusy");
|
||||||
await this._session.enableSecretStorage("phrase", passphrase);
|
const key = await this._session.enableSecretStorage(keyType, credential);
|
||||||
|
if (setupDehydratedDevice) {
|
||||||
|
this._dehydratedDeviceId = await this._session.setupDehydratedDevice(key);
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
this._error = err;
|
this._error = err;
|
||||||
this.emitChange("error");
|
this.emitChange("error");
|
||||||
} finally {
|
} finally {
|
||||||
this._isBusy = false;
|
this._isBusy = false;
|
||||||
|
this._reevaluateStatus();
|
||||||
this.emitChange("");
|
this.emitChange("");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async enterSecurityKey(securityKey) {
|
enterSecurityPhrase(passphrase, setupDehydratedDevice) {
|
||||||
if (securityKey) {
|
this._enterCredentials(KeyType.Passphrase, passphrase, setupDehydratedDevice);
|
||||||
|
}
|
||||||
|
|
||||||
|
enterSecurityKey(securityKey, setupDehydratedDevice) {
|
||||||
|
this._enterCredentials(KeyType.RecoveryKey, securityKey, setupDehydratedDevice);
|
||||||
|
}
|
||||||
|
|
||||||
|
async disable() {
|
||||||
try {
|
try {
|
||||||
this._isBusy = true;
|
this._isBusy = true;
|
||||||
this.emitChange("isBusy");
|
this.emitChange("isBusy");
|
||||||
await this._session.enableSecretStorage("key", securityKey);
|
await this._session.disableSecretStorage();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
this._error = err;
|
this._error = err;
|
||||||
this.emitChange("error");
|
this.emitChange("error");
|
||||||
} finally {
|
} finally {
|
||||||
this._isBusy = false;
|
this._isBusy = false;
|
||||||
|
this._reevaluateStatus();
|
||||||
this.emitChange("");
|
this.emitChange("");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
|
@ -40,6 +40,7 @@ import {
|
||||||
keyFromCredential as ssssKeyFromCredential,
|
keyFromCredential as ssssKeyFromCredential,
|
||||||
readKey as ssssReadKey,
|
readKey as ssssReadKey,
|
||||||
writeKey as ssssWriteKey,
|
writeKey as ssssWriteKey,
|
||||||
|
removeKey as ssssRemoveKey
|
||||||
} from "./ssss/index.js";
|
} from "./ssss/index.js";
|
||||||
import {SecretStorage} from "./ssss/SecretStorage.js";
|
import {SecretStorage} from "./ssss/SecretStorage.js";
|
||||||
import {ObservableValue, RetainedObservableValue} from "../observable/ObservableValue";
|
import {ObservableValue, RetainedObservableValue} from "../observable/ObservableValue";
|
||||||
|
@ -217,6 +218,30 @@ export class Session {
|
||||||
}
|
}
|
||||||
await writeTxn.complete();
|
await writeTxn.complete();
|
||||||
this._hasSecretStorageKey.set(true);
|
this._hasSecretStorageKey.set(true);
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
async disableSecretStorage() {
|
||||||
|
const writeTxn = await this._storage.readWriteTxn([
|
||||||
|
this._storage.storeNames.session,
|
||||||
|
]);
|
||||||
|
try {
|
||||||
|
ssssRemoveKey(writeTxn);
|
||||||
|
} catch (err) {
|
||||||
|
writeTxn.abort();
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
await writeTxn.complete();
|
||||||
|
if (this._sessionBackup) {
|
||||||
|
for (const room of this._rooms.values()) {
|
||||||
|
if (room.isEncrypted) {
|
||||||
|
room.enableSessionBackup(undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this._sessionBackup?.dispose();
|
||||||
|
this._sessionBackup = undefined;
|
||||||
|
}
|
||||||
|
this._hasSecretStorageKey.set(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
async _createSessionBackup(ssssKey, txn) {
|
async _createSessionBackup(ssssKey, txn) {
|
||||||
|
@ -311,6 +336,7 @@ export class Session {
|
||||||
try {
|
try {
|
||||||
const deviceId = await uploadAccountAsDehydratedDevice(
|
const deviceId = await uploadAccountAsDehydratedDevice(
|
||||||
dehydrationAccount, this._hsApi, key, "Dehydrated device", log);
|
dehydrationAccount, this._hsApi, key, "Dehydrated device", log);
|
||||||
|
log.set("deviceId", deviceId);
|
||||||
return deviceId;
|
return deviceId;
|
||||||
} finally {
|
} finally {
|
||||||
dehydrationAccount.dispose();
|
dehydrationAccount.dispose();
|
||||||
|
|
|
@ -321,7 +321,7 @@ export class SessionContainer {
|
||||||
request: this._platform.request,
|
request: this._platform.request,
|
||||||
});
|
});
|
||||||
const olm = await this._olmPromise;
|
const olm = await this._olmPromise;
|
||||||
const encryptedDehydratedDevice = await getDehydratedDevice(hsApi, olm, log);
|
const encryptedDehydratedDevice = await getDehydratedDevice(hsApi, olm, this._platform, log);
|
||||||
if (encryptedDehydratedDevice) {
|
if (encryptedDehydratedDevice) {
|
||||||
let resolveStageFinish;
|
let resolveStageFinish;
|
||||||
const promiseStageFinish = new Promise(r => resolveStageFinish = r);
|
const promiseStageFinish = new Promise(r => resolveStageFinish = r);
|
||||||
|
|
|
@ -15,12 +15,14 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const DEHYDRATION_LIBOLM_PICKLE_ALGORITHM = "org.matrix.msc2697.v1.olm.libolm_pickle";
|
const DEHYDRATION_LIBOLM_PICKLE_ALGORITHM = "org.matrix.msc2697.v1.olm.libolm_pickle";
|
||||||
|
import {KeyDescription} from "../ssss/common.js";
|
||||||
|
import {keyFromCredentialAndDescription} from "../ssss/index.js";
|
||||||
|
|
||||||
export async function getDehydratedDevice(hsApi, olm, log) {
|
export async function getDehydratedDevice(hsApi, olm, platform, log) {
|
||||||
try {
|
try {
|
||||||
const response = await hsApi.getDehydratedDevice({log}).response();
|
const response = await hsApi.getDehydratedDevice({log}).response();
|
||||||
if (response.device_data.algorithm === DEHYDRATION_LIBOLM_PICKLE_ALGORITHM) {
|
if (response.device_data.algorithm === DEHYDRATION_LIBOLM_PICKLE_ALGORITHM) {
|
||||||
return new EncryptedDehydratedDevice(response, olm);
|
return new EncryptedDehydratedDevice(response, olm, platform);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err.name !== "HomeServerError") {
|
if (err.name !== "HomeServerError") {
|
||||||
|
@ -34,8 +36,8 @@ export async function uploadAccountAsDehydratedDevice(account, hsApi, key, devic
|
||||||
const response = await hsApi.createDehydratedDevice({
|
const response = await hsApi.createDehydratedDevice({
|
||||||
device_data: {
|
device_data: {
|
||||||
algorithm: DEHYDRATION_LIBOLM_PICKLE_ALGORITHM,
|
algorithm: DEHYDRATION_LIBOLM_PICKLE_ALGORITHM,
|
||||||
account: account.pickleWithKey(new Uint8Array(key)),
|
account: account.pickleWithKey(key.binaryKey),
|
||||||
passphrase: {}
|
passphrase: key.description?.passphraseParams || {},
|
||||||
},
|
},
|
||||||
initial_device_display_name: deviceDisplayName
|
initial_device_display_name: deviceDisplayName
|
||||||
}).response();
|
}).response();
|
||||||
|
@ -46,17 +48,20 @@ export async function uploadAccountAsDehydratedDevice(account, hsApi, key, devic
|
||||||
}
|
}
|
||||||
|
|
||||||
class EncryptedDehydratedDevice {
|
class EncryptedDehydratedDevice {
|
||||||
constructor(dehydratedDevice, olm) {
|
constructor(dehydratedDevice, olm, platform) {
|
||||||
this._dehydratedDevice = dehydratedDevice;
|
this._dehydratedDevice = dehydratedDevice;
|
||||||
this._olm = olm;
|
this._olm = olm;
|
||||||
|
this._platform = platform;
|
||||||
}
|
}
|
||||||
|
|
||||||
decrypt(key) {
|
async decrypt(keyType, credential) {
|
||||||
|
const keyDescription = new KeyDescription("dehydrated_device", this._dehydratedDevice.device_data.passphrase);
|
||||||
|
const key = await keyFromCredentialAndDescription(keyType, credential, keyDescription, this._platform, this._olm);
|
||||||
const account = new this._olm.Account();
|
const account = new this._olm.Account();
|
||||||
try {
|
try {
|
||||||
const pickledAccount = this._dehydratedDevice.device_data.account;
|
const pickledAccount = this._dehydratedDevice.device_data.account;
|
||||||
account.unpickle(new Uint8Array(key), pickledAccount);
|
account.unpickle(key.binaryKey, pickledAccount);
|
||||||
return new DehydratedDevice(this._dehydratedDevice, account);
|
return new DehydratedDevice(this._dehydratedDevice, account, keyType, key);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
account.free();
|
account.free();
|
||||||
if (err.message === "OLM.BAD_ACCOUNT_KEY") {
|
if (err.message === "OLM.BAD_ACCOUNT_KEY") {
|
||||||
|
@ -73,9 +78,11 @@ class EncryptedDehydratedDevice {
|
||||||
}
|
}
|
||||||
|
|
||||||
class DehydratedDevice {
|
class DehydratedDevice {
|
||||||
constructor(dehydratedDevice, account) {
|
constructor(dehydratedDevice, account, keyType, key) {
|
||||||
this._dehydratedDevice = dehydratedDevice;
|
this._dehydratedDevice = dehydratedDevice;
|
||||||
this._account = account;
|
this._account = account;
|
||||||
|
this._keyType = keyType;
|
||||||
|
this._key = key;
|
||||||
}
|
}
|
||||||
|
|
||||||
async claim(hsApi, log) {
|
async claim(hsApi, log) {
|
||||||
|
@ -98,6 +105,14 @@ class DehydratedDevice {
|
||||||
return this._dehydratedDevice.device_id;
|
return this._dehydratedDevice.device_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get key() {
|
||||||
|
return this._key;
|
||||||
|
}
|
||||||
|
|
||||||
|
get keyType() {
|
||||||
|
return this._keyType;
|
||||||
|
}
|
||||||
|
|
||||||
dispose() {
|
dispose() {
|
||||||
this._account?.free();
|
this._account?.free();
|
||||||
this._account = undefined;
|
this._account = undefined;
|
||||||
|
|
|
@ -49,7 +49,7 @@ export class RoomEncryption {
|
||||||
}
|
}
|
||||||
|
|
||||||
enableSessionBackup(sessionBackup) {
|
enableSessionBackup(sessionBackup) {
|
||||||
if (this._sessionBackup) {
|
if (this._sessionBackup && !!sessionBackup) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this._sessionBackup = sessionBackup;
|
this._sessionBackup = sessionBackup;
|
||||||
|
|
|
@ -463,7 +463,7 @@ export class BaseRoom extends EventEmitter {
|
||||||
enableSessionBackup(sessionBackup) {
|
enableSessionBackup(sessionBackup) {
|
||||||
this._roomEncryption?.enableSessionBackup(sessionBackup);
|
this._roomEncryption?.enableSessionBackup(sessionBackup);
|
||||||
// TODO: do we really want to do this every time you open the app?
|
// TODO: do we really want to do this every time you open the app?
|
||||||
if (this._timeline) {
|
if (this._timeline && sessionBackup) {
|
||||||
this._platform.logger.run("enableSessionBackup", log => {
|
this._platform.logger.run("enableSessionBackup", log => {
|
||||||
return this._roomEncryption.restoreMissingSessionsFromBackup(this._timeline.remoteEntries, log);
|
return this._roomEncryption.restoreMissingSessionsFromBackup(this._timeline.remoteEntries, log);
|
||||||
});
|
});
|
||||||
|
|
|
@ -15,9 +15,9 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export class KeyDescription {
|
export class KeyDescription {
|
||||||
constructor(id, keyAccountData) {
|
constructor(id, keyDescription) {
|
||||||
this._id = id;
|
this._id = id;
|
||||||
this._keyAccountData = keyAccountData;
|
this._keyDescription = keyDescription;
|
||||||
}
|
}
|
||||||
|
|
||||||
get id() {
|
get id() {
|
||||||
|
@ -25,11 +25,11 @@ export class KeyDescription {
|
||||||
}
|
}
|
||||||
|
|
||||||
get passphraseParams() {
|
get passphraseParams() {
|
||||||
return this._keyAccountData?.content?.passphrase;
|
return this._keyDescription?.passphrase;
|
||||||
}
|
}
|
||||||
|
|
||||||
get algorithm() {
|
get algorithm() {
|
||||||
return this._keyAccountData?.content?.algorithm;
|
return this._keyDescription?.algorithm;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,6 +39,10 @@ export class Key {
|
||||||
this._binaryKey = binaryKey;
|
this._binaryKey = binaryKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get description() {
|
||||||
|
return this._keyDescription;
|
||||||
|
}
|
||||||
|
|
||||||
get id() {
|
get id() {
|
||||||
return this._keyDescription.id;
|
return this._keyDescription.id;
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,9 +18,12 @@ import {KeyDescription, Key} from "./common.js";
|
||||||
import {keyFromPassphrase} from "./passphrase.js";
|
import {keyFromPassphrase} from "./passphrase.js";
|
||||||
import {keyFromRecoveryKey} from "./recoveryKey.js";
|
import {keyFromRecoveryKey} from "./recoveryKey.js";
|
||||||
import {SESSION_E2EE_KEY_PREFIX} from "../e2ee/common.js";
|
import {SESSION_E2EE_KEY_PREFIX} from "../e2ee/common.js";
|
||||||
|
import {createEnum} from "../../utils/enum.js";
|
||||||
|
|
||||||
const SSSS_KEY = `${SESSION_E2EE_KEY_PREFIX}ssssKey`;
|
const SSSS_KEY = `${SESSION_E2EE_KEY_PREFIX}ssssKey`;
|
||||||
|
|
||||||
|
export const KeyType = createEnum("RecoveryKey", "Passphrase");
|
||||||
|
|
||||||
async function readDefaultKeyDescription(storage) {
|
async function readDefaultKeyDescription(storage) {
|
||||||
const txn = await storage.readTxn([
|
const txn = await storage.readTxn([
|
||||||
storage.storeNames.accountData
|
storage.storeNames.accountData
|
||||||
|
@ -34,7 +37,7 @@ async function readDefaultKeyDescription(storage) {
|
||||||
if (!keyAccountData) {
|
if (!keyAccountData) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
return new KeyDescription(id, keyAccountData);
|
return new KeyDescription(id, keyAccountData.content);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function writeKey(key, txn) {
|
export async function writeKey(key, txn) {
|
||||||
|
@ -47,7 +50,12 @@ export async function readKey(txn) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const keyAccountData = await txn.accountData.get(`m.secret_storage.key.${keyData.id}`);
|
const keyAccountData = await txn.accountData.get(`m.secret_storage.key.${keyData.id}`);
|
||||||
return new Key(new KeyDescription(keyData.id, keyAccountData), keyData.binaryKey);
|
return new Key(new KeyDescription(keyData.id, keyAccountData.content), keyData.binaryKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export async function removeKey(txn) {
|
||||||
|
await txn.session.remove(SSSS_KEY);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function keyFromCredential(type, credential, storage, platform, olm) {
|
export async function keyFromCredential(type, credential, storage, platform, olm) {
|
||||||
|
@ -55,10 +63,14 @@ export async function keyFromCredential(type, credential, storage, platform, olm
|
||||||
if (!keyDescription) {
|
if (!keyDescription) {
|
||||||
throw new Error("Could not find a default secret storage key in account data");
|
throw new Error("Could not find a default secret storage key in account data");
|
||||||
}
|
}
|
||||||
|
return await keyFromCredentialAndDescription(type, credential, keyDescription, platform, olm);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function keyFromCredentialAndDescription(type, credential, keyDescription, platform, olm) {
|
||||||
let key;
|
let key;
|
||||||
if (type === "phrase") {
|
if (type === KeyType.Passphrase) {
|
||||||
key = await keyFromPassphrase(keyDescription, credential, platform);
|
key = await keyFromPassphrase(keyDescription, credential, platform);
|
||||||
} else if (type === "key") {
|
} else if (type === KeyType.RecoveryKey) {
|
||||||
key = keyFromRecoveryKey(keyDescription, credential, olm, platform);
|
key = keyFromRecoveryKey(keyDescription, credential, olm, platform);
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Invalid type: ${type}`);
|
throw new Error(`Invalid type: ${type}`);
|
||||||
|
|
|
@ -629,6 +629,19 @@ a {
|
||||||
|
|
||||||
.Settings .row .label {
|
.Settings .row .label {
|
||||||
flex: 0 0 200px;
|
flex: 0 0 200px;
|
||||||
|
align-self: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Settings .row .content p {
|
||||||
|
margin: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Settings .row .content p:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Settings .row .content p:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.error {
|
.error {
|
||||||
|
|
|
@ -21,17 +21,23 @@ export class SessionBackupSettingsView extends TemplateView {
|
||||||
render(t, vm) {
|
render(t, vm) {
|
||||||
return t.mapView(vm => vm.status, status => {
|
return t.mapView(vm => vm.status, status => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case "enabled": return new TemplateView(vm, renderEnabled)
|
case "Enabled": return new TemplateView(vm, renderEnabled)
|
||||||
case "setupKey": return new TemplateView(vm, renderEnableFromKey)
|
case "SetupKey": return new TemplateView(vm, renderEnableFromKey)
|
||||||
case "setupPhrase": return new TemplateView(vm, renderEnableFromPhrase)
|
case "SetupPhrase": return new TemplateView(vm, renderEnableFromPhrase)
|
||||||
case "pending": return new StaticView(vm, t => t.p(vm.i18n`Waiting to go online…`))
|
case "Pending": return new StaticView(vm, t => t.p(vm.i18n`Waiting to go online…`))
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderEnabled(t, vm) {
|
function renderEnabled(t, vm) {
|
||||||
return t.p(vm.i18n`Session backup is enabled, using backup version ${vm.backupVersion}.`);
|
const items = [
|
||||||
|
t.p([vm.i18n`Session backup is enabled, using backup version ${vm.backupVersion}. `, t.button({onClick: () => vm.disable()}, vm.i18n`Disable`)])
|
||||||
|
];
|
||||||
|
if (vm.dehydratedDeviceId) {
|
||||||
|
items.push(t.p(vm.i18n`A dehydrated device id was set up with id ${vm.dehydratedDeviceId} which you can use during your next login with your secret storage key.`));
|
||||||
|
}
|
||||||
|
return t.div(items);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderEnableFromKey(t, vm) {
|
function renderEnableFromKey(t, vm) {
|
||||||
|
@ -39,7 +45,7 @@ function renderEnableFromKey(t, vm) {
|
||||||
return t.div([
|
return t.div([
|
||||||
t.p(vm.i18n`Enter your secret storage security key below to set up session backup, which will enable you to decrypt messages received before you logged into this session. The security key is a code of 12 groups of 4 characters separated by a space that Element created for you when setting up security.`),
|
t.p(vm.i18n`Enter your secret storage security key below to set up session backup, which will enable you to decrypt messages received before you logged into this session. The security key is a code of 12 groups of 4 characters separated by a space that Element created for you when setting up security.`),
|
||||||
renderError(t),
|
renderError(t),
|
||||||
renderEnableFieldRow(t, vm, vm.i18n`Security key`, key => vm.enterSecurityKey(key)),
|
renderEnableFieldRow(t, vm, vm.i18n`Security key`, (key, setupDehydratedDevice) => vm.enterSecurityKey(key, setupDehydratedDevice)),
|
||||||
t.p([vm.i18n`Alternatively, you can `, useASecurityPhrase, vm.i18n` if you have one.`]),
|
t.p([vm.i18n`Alternatively, you can `, useASecurityPhrase, vm.i18n` if you have one.`]),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
@ -49,20 +55,32 @@ function renderEnableFromPhrase(t, vm) {
|
||||||
return t.div([
|
return t.div([
|
||||||
t.p(vm.i18n`Enter your secret storage security phrase below to set up session backup, which will enable you to decrypt messages received before you logged into this session. The security phrase is a freeform secret phrase you optionally chose when setting up security in Element. It is different from your password to login, unless you chose to set them to the same value.`),
|
t.p(vm.i18n`Enter your secret storage security phrase below to set up session backup, which will enable you to decrypt messages received before you logged into this session. The security phrase is a freeform secret phrase you optionally chose when setting up security in Element. It is different from your password to login, unless you chose to set them to the same value.`),
|
||||||
renderError(t),
|
renderError(t),
|
||||||
renderEnableFieldRow(t, vm, vm.i18n`Security phrase`, phrase => vm.enterSecurityPhrase(phrase)),
|
renderEnableFieldRow(t, vm, vm.i18n`Security phrase`, (phrase, setupDehydratedDevice) => vm.enterSecurityPhrase(phrase, setupDehydratedDevice)),
|
||||||
t.p([vm.i18n`You can also `, useASecurityKey, vm.i18n`.`]),
|
t.p([vm.i18n`You can also `, useASecurityKey, vm.i18n`.`]),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderEnableFieldRow(t, vm, label, callback) {
|
function renderEnableFieldRow(t, vm, label, callback) {
|
||||||
const eventHandler = () => callback(input.value);
|
let setupDehydrationCheck;
|
||||||
const input = t.input({type: "password", disabled: vm => vm.isBusy, placeholder: label, onChange: eventHandler});
|
const eventHandler = () => callback(input.value, setupDehydrationCheck?.checked || false);
|
||||||
|
const input = t.input({type: "password", disabled: vm => vm.isBusy, placeholder: label});
|
||||||
|
const children = [
|
||||||
|
t.p([
|
||||||
|
input,
|
||||||
|
t.button({disabled: vm => vm.isBusy, onClick: eventHandler}, vm.decryptAction),
|
||||||
|
]),
|
||||||
|
];
|
||||||
|
if (vm.offerDehydratedDeviceSetup) {
|
||||||
|
setupDehydrationCheck = t.input({type: "checkbox", id:"enable-dehydrated-device"});
|
||||||
|
const moreInfo = t.a({href: "https://github.com/uhoreg/matrix-doc/blob/dehydration/proposals/2697-device-dehydration.md", target: "_blank", rel: "noopener"}, "more info");
|
||||||
|
children.push(t.p([
|
||||||
|
setupDehydrationCheck,
|
||||||
|
t.label({for: setupDehydrationCheck.id}, [vm.i18n`Back up my device as well (`, moreInfo, ")"])
|
||||||
|
]));
|
||||||
|
}
|
||||||
return t.div({className: `row`}, [
|
return t.div({className: `row`}, [
|
||||||
t.div({className: "label"}, label),
|
t.div({className: "label"}, label),
|
||||||
t.div({className: "content"}, [
|
t.div({className: "content"}, children),
|
||||||
input,
|
|
||||||
t.button({disabled: vm => vm.isBusy, onClick: eventHandler}, vm.i18n`Set up`),
|
|
||||||
]),
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue