mirror of
https://github.com/vector-im/hydrogen-web.git
synced 2024-12-23 19:45:05 +01:00
WIP
This commit is contained in:
parent
abb802b881
commit
faf4ea6434
@ -29,6 +29,7 @@ export class SessionLoadViewModel extends ViewModel {
|
|||||||
this._loading = false;
|
this._loading = false;
|
||||||
this._error = null;
|
this._error = null;
|
||||||
this.backUrl = this.urlCreator.urlForSegment("session", true);
|
this.backUrl = this.urlCreator.urlForSegment("session", true);
|
||||||
|
this._dehydratedDevice = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
async start() {
|
async start() {
|
||||||
@ -110,6 +111,10 @@ export class SessionLoadViewModel extends ViewModel {
|
|||||||
// Statuses related to login are handled by respective login view models
|
// Statuses related to login are handled by respective login view models
|
||||||
if (sc) {
|
if (sc) {
|
||||||
switch (sc.loadStatus.get()) {
|
switch (sc.loadStatus.get()) {
|
||||||
|
case LoadStatus.QueryAccount:
|
||||||
|
return `Querying account encryption setup…`;
|
||||||
|
case LoadStatus.SetupAccount:
|
||||||
|
return `Please enter your password to restore your encryption setup`;
|
||||||
case LoadStatus.SessionSetup:
|
case LoadStatus.SessionSetup:
|
||||||
return `Setting up your encryption keys…`;
|
return `Setting up your encryption keys…`;
|
||||||
case LoadStatus.Loading:
|
case LoadStatus.Loading:
|
||||||
@ -136,4 +141,27 @@ export class SessionLoadViewModel extends ViewModel {
|
|||||||
const logExport = await this.logger.export();
|
const logExport = await this.logger.export();
|
||||||
this.platform.saveFileAs(logExport.asBlob(), `hydrogen-logs-${this.platform.clock.now()}.json`);
|
this.platform.saveFileAs(logExport.asBlob(), `hydrogen-logs-${this.platform.clock.now()}.json`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get canSetupAccount() {
|
||||||
|
return this._sessionContainer.loadStatus === LoadStatus.SetupAccount;
|
||||||
|
}
|
||||||
|
|
||||||
|
get canDehydrateDevice() {
|
||||||
|
return this.canSetupAccount && !!this._sessionContainer.accountSetup.encryptedDehydratedDevice;
|
||||||
|
}
|
||||||
|
|
||||||
|
tryDecryptDehydratedDevice(password) {
|
||||||
|
const {encryptedDehydratedDevice} = this._sessionContainer.accountSetup;
|
||||||
|
if (encryptedDehydratedDevice) {
|
||||||
|
this._dehydratedDevice = encryptedDehydratedDevice.decrypt(password);
|
||||||
|
return !!this._dehydratedDevice;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
finishAccountSetup() {
|
||||||
|
const dehydratedDevice = this._dehydratedDevice;
|
||||||
|
this._dehydratedDevice = undefined;
|
||||||
|
this._sessionContainer.accountSetup.finish(dehydratedDevice);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -248,23 +248,77 @@ export class Session {
|
|||||||
async createIdentity(log) {
|
async createIdentity(log) {
|
||||||
if (this._olm) {
|
if (this._olm) {
|
||||||
if (!this._e2eeAccount) {
|
if (!this._e2eeAccount) {
|
||||||
this._e2eeAccount = await E2EEAccount.create({
|
this._e2eeAccount = this._createNewAccount(this._sessionInfo.deviceId, this._storage);
|
||||||
hsApi: this._hsApi,
|
|
||||||
olm: this._olm,
|
|
||||||
pickleKey: PICKLE_KEY,
|
|
||||||
userId: this._sessionInfo.userId,
|
|
||||||
deviceId: this._sessionInfo.deviceId,
|
|
||||||
olmWorker: this._olmWorker,
|
|
||||||
storage: this._storage,
|
|
||||||
});
|
|
||||||
log.set("keys", this._e2eeAccount.identityKeys);
|
log.set("keys", this._e2eeAccount.identityKeys);
|
||||||
this._setupEncryption();
|
this._setupEncryption();
|
||||||
}
|
}
|
||||||
await this._e2eeAccount.generateOTKsIfNeeded(this._storage, log);
|
await this._e2eeAccount.generateOTKsIfNeeded(this._storage, log);
|
||||||
await log.wrap("uploadKeys", log => this._e2eeAccount.uploadKeys(this._storage, log));
|
await log.wrap("uploadKeys", log => this._e2eeAccount.uploadKeys(this._storage, undefined, log));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
|
dehydrateIdentity(dehydratedDevice, log = null) {
|
||||||
|
this._platform.logger.wrapOrRun(log, "dehydrateIdentity", async log => {
|
||||||
|
log.set("deviceId", dehydratedDevice.deviceId);
|
||||||
|
if (!this._olm) {
|
||||||
|
log.set("no_olm", true);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (dehydratedDevice.deviceId !== this.deviceId) {
|
||||||
|
log.set("wrong_device", true);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (this._e2eeAccount) {
|
||||||
|
log.set("account_already_setup", true);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!await dehydratedDevice.claim(this._hsApi, log)) {
|
||||||
|
log.set("already_claimed", true);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
this._e2eeAccount = await E2EEAccount.adoptDehydratedDevice({
|
||||||
|
dehydratedDevice,
|
||||||
|
hsApi: this._hsApi,
|
||||||
|
olm: this._olm,
|
||||||
|
pickleKey: PICKLE_KEY,
|
||||||
|
userId: this._sessionInfo.userId,
|
||||||
|
olmWorker: this._olmWorker,
|
||||||
|
deviceId,
|
||||||
|
storage,
|
||||||
|
});
|
||||||
|
log.set("keys", this._e2eeAccount.identityKeys);
|
||||||
|
this._setupEncryption();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_createNewAccount(deviceId, storage = undefined) {
|
||||||
|
// storage is optional and if omitted the account won't be persisted (useful for dehydrating devices)
|
||||||
|
return E2EEAccount.create({
|
||||||
|
hsApi: this._hsApi,
|
||||||
|
olm: this._olm,
|
||||||
|
pickleKey: PICKLE_KEY,
|
||||||
|
userId: this._sessionInfo.userId,
|
||||||
|
olmWorker: this._olmWorker,
|
||||||
|
deviceId,
|
||||||
|
storage,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setupDehydratedDevice(key, log = null) {
|
||||||
|
return this._platform.logger.wrapOrRun(log, "setupDehydratedDevice", async log => {
|
||||||
|
const dehydrationAccount = await this._createNewAccount("temp-device-id");
|
||||||
|
try {
|
||||||
|
const deviceId = await uploadAccountAsDehydratedDevice(
|
||||||
|
dehydrationAccount, this._hsApi, key, "Dehydrated device", log);
|
||||||
|
return deviceId;
|
||||||
|
} finally {
|
||||||
|
dehydrationAccount.dispose();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
async load(log) {
|
async load(log) {
|
||||||
const txn = await this._storage.readTxn([
|
const txn = await this._storage.readTxn([
|
||||||
@ -323,6 +377,7 @@ export class Session {
|
|||||||
this._olmWorker?.dispose();
|
this._olmWorker?.dispose();
|
||||||
this._sessionBackup?.dispose();
|
this._sessionBackup?.dispose();
|
||||||
this._megolmDecryption.dispose();
|
this._megolmDecryption.dispose();
|
||||||
|
this._e2eeAccount?.dispose();
|
||||||
for (const room of this._rooms.values()) {
|
for (const room of this._rooms.values()) {
|
||||||
room.dispose();
|
room.dispose();
|
||||||
}
|
}
|
||||||
@ -517,7 +572,7 @@ export class Session {
|
|||||||
if (!isCatchupSync) {
|
if (!isCatchupSync) {
|
||||||
const needsToUploadOTKs = await this._e2eeAccount.generateOTKsIfNeeded(this._storage, log);
|
const needsToUploadOTKs = await this._e2eeAccount.generateOTKsIfNeeded(this._storage, log);
|
||||||
if (needsToUploadOTKs) {
|
if (needsToUploadOTKs) {
|
||||||
await log.wrap("uploadKeys", log => this._e2eeAccount.uploadKeys(this._storage, log));
|
await log.wrap("uploadKeys", log => this._e2eeAccount.uploadKeys(this._storage, undefined, log));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -34,6 +34,8 @@ export const LoadStatus = createEnum(
|
|||||||
"NotLoading",
|
"NotLoading",
|
||||||
"Login",
|
"Login",
|
||||||
"LoginFailed",
|
"LoginFailed",
|
||||||
|
"QueryAccount", // check for dehydrated device after login
|
||||||
|
"SetupAccount", // asked to restore from dehydrated device if present, call sc.accountSetup.finish() to progress to the next stage
|
||||||
"Loading",
|
"Loading",
|
||||||
"SessionSetup", // upload e2ee keys, ...
|
"SessionSetup", // upload e2ee keys, ...
|
||||||
"Migrating", //not used atm, but would fit here
|
"Migrating", //not used atm, but would fit here
|
||||||
@ -85,7 +87,7 @@ export class SessionContainer {
|
|||||||
if (!sessionInfo) {
|
if (!sessionInfo) {
|
||||||
throw new Error("Invalid session id: " + sessionId);
|
throw new Error("Invalid session id: " + sessionId);
|
||||||
}
|
}
|
||||||
await this._loadSessionInfo(sessionInfo, false, log);
|
await this._loadSessionInfo(sessionInfo, null, log);
|
||||||
log.set("status", this._status.get());
|
log.set("status", this._status.get());
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.catch(err);
|
log.catch(err);
|
||||||
@ -154,7 +156,6 @@ export class SessionContainer {
|
|||||||
lastUsed: clock.now()
|
lastUsed: clock.now()
|
||||||
};
|
};
|
||||||
log.set("id", sessionId);
|
log.set("id", sessionId);
|
||||||
await this._platform.sessionInfoStorage.add(sessionInfo);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this._error = err;
|
this._error = err;
|
||||||
if (err.name === "HomeServerError") {
|
if (err.name === "HomeServerError") {
|
||||||
@ -173,11 +174,16 @@ export class SessionContainer {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const dehydratedDevice = await this._inspectAccountAfterLogin(sessionInfo);
|
||||||
|
if (dehydratedDevice) {
|
||||||
|
sessionInfo.deviceId = dehydratedDevice.deviceId;
|
||||||
|
}
|
||||||
|
await this._platform.sessionInfoStorage.add(sessionInfo);
|
||||||
// loading the session can only lead to
|
// loading the session can only lead to
|
||||||
// LoadStatus.Error in case of an error,
|
// LoadStatus.Error in case of an error,
|
||||||
// so separate try/catch
|
// so separate try/catch
|
||||||
try {
|
try {
|
||||||
await this._loadSessionInfo(sessionInfo, true, log);
|
await this._loadSessionInfo(sessionInfo, dehydratedDevice, log);
|
||||||
log.set("status", this._status.get());
|
log.set("status", this._status.get());
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.catch(err);
|
log.catch(err);
|
||||||
@ -187,7 +193,7 @@ export class SessionContainer {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async _loadSessionInfo(sessionInfo, isNewLogin, log) {
|
async _loadSessionInfo(sessionInfo, dehydratedDevice, log) {
|
||||||
log.set("appVersion", this._platform.version);
|
log.set("appVersion", this._platform.version);
|
||||||
const clock = this._platform.clock;
|
const clock = this._platform.clock;
|
||||||
this._sessionStartedByReconnector = false;
|
this._sessionStartedByReconnector = false;
|
||||||
@ -233,7 +239,9 @@ export class SessionContainer {
|
|||||||
platform: this._platform,
|
platform: this._platform,
|
||||||
});
|
});
|
||||||
await this._session.load(log);
|
await this._session.load(log);
|
||||||
if (!this._session.hasIdentity) {
|
if (dehydratedDevice) {
|
||||||
|
await log.wrap("dehydrateIdentity", log => await this._session.dehydrateIdentity(dehydratedDevice, log));
|
||||||
|
} else if (!this._session.hasIdentity) {
|
||||||
this._status.set(LoadStatus.SessionSetup);
|
this._status.set(LoadStatus.SessionSetup);
|
||||||
await log.wrap("createIdentity", log => this._session.createIdentity(log));
|
await log.wrap("createIdentity", log => this._session.createIdentity(log));
|
||||||
}
|
}
|
||||||
@ -300,6 +308,30 @@ export class SessionContainer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async _inspectAccountAfterLogin(sessionInfo) {
|
||||||
|
this._status.set(LoadStatus.QueryAccount);
|
||||||
|
const hsApi = new HomeServerApi({
|
||||||
|
homeserver: sessionInfo.homeServer,
|
||||||
|
accessToken: sessionInfo.accessToken,
|
||||||
|
request: this._platform.request,
|
||||||
|
});
|
||||||
|
const olm = await this._olmPromise;
|
||||||
|
const encryptedDehydratedDevice = await getDehydratedDevice(hsApi, olm);
|
||||||
|
if (encryptedDehydratedDevice) {
|
||||||
|
let resolveStageFinish;
|
||||||
|
const promiseStageFinish = new Promise(r => resolveStageFinish = r);
|
||||||
|
this._accountSetup = new AccountSetup(encryptedDehydratedDevice, resolveStageFinish);
|
||||||
|
this._status.set(LoadStatus.SetupAccount);
|
||||||
|
await promiseStageFinish;
|
||||||
|
const dehydratedDevice = this._accountSetup?._dehydratedDevice;
|
||||||
|
this._accountSetup = null;
|
||||||
|
return dehydratedDevice;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get accountSetup() {
|
||||||
|
return this._accountSetup;
|
||||||
|
}
|
||||||
|
|
||||||
get loadStatus() {
|
get loadStatus() {
|
||||||
return this._status;
|
return this._status;
|
||||||
@ -378,3 +410,20 @@ export class SessionContainer {
|
|||||||
this._loginFailure = null;
|
this._loginFailure = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class AccountSetup {
|
||||||
|
constructor(encryptedDehydratedDevice, finishStage) {
|
||||||
|
this._encryptedDehydratedDevice = encryptedDehydratedDevice;
|
||||||
|
this._dehydratedDevice = undefined;
|
||||||
|
this._finishStage = finishStage;
|
||||||
|
}
|
||||||
|
|
||||||
|
get encryptedDehydratedDevice() {
|
||||||
|
return this._encryptedDehydratedDevice;
|
||||||
|
}
|
||||||
|
|
||||||
|
finish(dehydratedDevice) {
|
||||||
|
this._dehydratedDevice = dehydratedDevice;
|
||||||
|
this._finishStage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -22,6 +22,24 @@ const ACCOUNT_SESSION_KEY = SESSION_E2EE_KEY_PREFIX + "olmAccount";
|
|||||||
const DEVICE_KEY_FLAG_SESSION_KEY = SESSION_E2EE_KEY_PREFIX + "areDeviceKeysUploaded";
|
const DEVICE_KEY_FLAG_SESSION_KEY = SESSION_E2EE_KEY_PREFIX + "areDeviceKeysUploaded";
|
||||||
const SERVER_OTK_COUNT_SESSION_KEY = SESSION_E2EE_KEY_PREFIX + "serverOTKCount";
|
const SERVER_OTK_COUNT_SESSION_KEY = SESSION_E2EE_KEY_PREFIX + "serverOTKCount";
|
||||||
|
|
||||||
|
async function initiallyStoreAccount(account, pickleKey, areDeviceKeysUploaded, serverOTKCount, storage) {
|
||||||
|
const pickledAccount = account.pickle(pickleKey);
|
||||||
|
const txn = await storage.readWriteTxn([
|
||||||
|
storage.storeNames.session
|
||||||
|
]);
|
||||||
|
try {
|
||||||
|
// add will throw if the key already exists
|
||||||
|
// we would not want to overwrite olmAccount here
|
||||||
|
txn.session.add(ACCOUNT_SESSION_KEY, pickledAccount);
|
||||||
|
txn.session.add(DEVICE_KEY_FLAG_SESSION_KEY, areDeviceKeysUploaded);
|
||||||
|
txn.session.add(SERVER_OTK_COUNT_SESSION_KEY, serverOTKCount);
|
||||||
|
} catch (err) {
|
||||||
|
txn.abort();
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
await txn.complete();
|
||||||
|
}
|
||||||
|
|
||||||
export class Account {
|
export class Account {
|
||||||
static async load({olm, pickleKey, hsApi, userId, deviceId, olmWorker, txn}) {
|
static async load({olm, pickleKey, hsApi, userId, deviceId, olmWorker, txn}) {
|
||||||
const pickledAccount = await txn.session.get(ACCOUNT_SESSION_KEY);
|
const pickledAccount = await txn.session.get(ACCOUNT_SESSION_KEY);
|
||||||
@ -35,6 +53,21 @@ export class Account {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async adoptDehydratedDevice({olm, dehydratedDevice, pickleKey, hsApi, userId, olmWorker, storage}) {
|
||||||
|
const account = dehydratedDevice.adoptUnpickledOlmAccount();
|
||||||
|
const areDeviceKeysUploaded = true;
|
||||||
|
const oneTimeKeys = JSON.parse(this._account.one_time_keys());
|
||||||
|
// only one algorithm supported by olm atm, so hardcode its name
|
||||||
|
const oneTimeKeysEntries = Object.entries(oneTimeKeys.curve25519);
|
||||||
|
const serverOTKCount = oneTimeKeysEntries.length;
|
||||||
|
await initiallyStoreAccount(account, pickleKey, areDeviceKeysUploaded, serverOTKCount, storage);
|
||||||
|
return new Account({
|
||||||
|
pickleKey, hsApi, account, userId,
|
||||||
|
deviceId: dehydratedDevice.deviceId,
|
||||||
|
areDeviceKeysUploaded, serverOTKCount, olm, olmWorker
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
static async create({olm, pickleKey, hsApi, userId, deviceId, olmWorker, storage}) {
|
static async create({olm, pickleKey, hsApi, userId, deviceId, olmWorker, storage}) {
|
||||||
const account = new olm.Account();
|
const account = new olm.Account();
|
||||||
if (olmWorker) {
|
if (olmWorker) {
|
||||||
@ -43,22 +76,9 @@ export class Account {
|
|||||||
account.create();
|
account.create();
|
||||||
account.generate_one_time_keys(account.max_number_of_one_time_keys());
|
account.generate_one_time_keys(account.max_number_of_one_time_keys());
|
||||||
}
|
}
|
||||||
const pickledAccount = account.pickle(pickleKey);
|
if (storage) {
|
||||||
const areDeviceKeysUploaded = false;
|
await initiallyStoreAccount(account, pickleKey, false, 0, storage);
|
||||||
const txn = await storage.readWriteTxn([
|
|
||||||
storage.storeNames.session
|
|
||||||
]);
|
|
||||||
try {
|
|
||||||
// add will throw if the key already exists
|
|
||||||
// we would not want to overwrite olmAccount here
|
|
||||||
txn.session.add(ACCOUNT_SESSION_KEY, pickledAccount);
|
|
||||||
txn.session.add(DEVICE_KEY_FLAG_SESSION_KEY, areDeviceKeysUploaded);
|
|
||||||
txn.session.add(SERVER_OTK_COUNT_SESSION_KEY, 0);
|
|
||||||
} catch (err) {
|
|
||||||
txn.abort();
|
|
||||||
throw err;
|
|
||||||
}
|
}
|
||||||
await txn.complete();
|
|
||||||
return new Account({pickleKey, hsApi, account, userId,
|
return new Account({pickleKey, hsApi, account, userId,
|
||||||
deviceId, areDeviceKeysUploaded, serverOTKCount: 0, olm, olmWorker});
|
deviceId, areDeviceKeysUploaded, serverOTKCount: 0, olm, olmWorker});
|
||||||
}
|
}
|
||||||
@ -80,7 +100,7 @@ export class Account {
|
|||||||
return this._identityKeys;
|
return this._identityKeys;
|
||||||
}
|
}
|
||||||
|
|
||||||
async uploadKeys(storage, log) {
|
async uploadKeys(storage, dehydratedDeviceId, log) {
|
||||||
const oneTimeKeys = JSON.parse(this._account.one_time_keys());
|
const oneTimeKeys = JSON.parse(this._account.one_time_keys());
|
||||||
// only one algorithm supported by olm atm, so hardcode its name
|
// only one algorithm supported by olm atm, so hardcode its name
|
||||||
const oneTimeKeysEntries = Object.entries(oneTimeKeys.curve25519);
|
const oneTimeKeysEntries = Object.entries(oneTimeKeys.curve25519);
|
||||||
@ -95,7 +115,7 @@ export class Account {
|
|||||||
log.set("otks", true);
|
log.set("otks", true);
|
||||||
payload.one_time_keys = this._oneTimeKeysPayload(oneTimeKeysEntries);
|
payload.one_time_keys = this._oneTimeKeysPayload(oneTimeKeysEntries);
|
||||||
}
|
}
|
||||||
const response = await this._hsApi.uploadKeys(payload, {log}).response();
|
const response = await this._hsApi.uploadKeys(dehydratedDeviceId, payload, {log}).response();
|
||||||
this._serverOTKCount = response?.one_time_key_counts?.signed_curve25519;
|
this._serverOTKCount = response?.one_time_key_counts?.signed_curve25519;
|
||||||
log.set("serverOTKCount", this._serverOTKCount);
|
log.set("serverOTKCount", this._serverOTKCount);
|
||||||
// TODO: should we not modify this in the txn like we do elsewhere?
|
// TODO: should we not modify this in the txn like we do elsewhere?
|
||||||
@ -105,12 +125,12 @@ export class Account {
|
|||||||
await this._updateSessionStorage(storage, sessionStore => {
|
await this._updateSessionStorage(storage, sessionStore => {
|
||||||
if (oneTimeKeysEntries.length) {
|
if (oneTimeKeysEntries.length) {
|
||||||
this._account.mark_keys_as_published();
|
this._account.mark_keys_as_published();
|
||||||
sessionStore.set(ACCOUNT_SESSION_KEY, this._account.pickle(this._pickleKey));
|
sessionStore?.set(ACCOUNT_SESSION_KEY, this._account.pickle(this._pickleKey));
|
||||||
sessionStore.set(SERVER_OTK_COUNT_SESSION_KEY, this._serverOTKCount);
|
sessionStore?.set(SERVER_OTK_COUNT_SESSION_KEY, this._serverOTKCount);
|
||||||
}
|
}
|
||||||
if (!this._areDeviceKeysUploaded) {
|
if (!this._areDeviceKeysUploaded) {
|
||||||
this._areDeviceKeysUploaded = true;
|
this._areDeviceKeysUploaded = true;
|
||||||
sessionStore.set(DEVICE_KEY_FLAG_SESSION_KEY, this._areDeviceKeysUploaded);
|
sessionStore?.set(DEVICE_KEY_FLAG_SESSION_KEY, this._areDeviceKeysUploaded);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -246,6 +266,7 @@ export class Account {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async _updateSessionStorage(storage, callback) {
|
async _updateSessionStorage(storage, callback) {
|
||||||
|
if (storage) {
|
||||||
const txn = await storage.readWriteTxn([
|
const txn = await storage.readWriteTxn([
|
||||||
storage.storeNames.session
|
storage.storeNames.session
|
||||||
]);
|
]);
|
||||||
@ -256,6 +277,9 @@ export class Account {
|
|||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
await txn.complete();
|
await txn.complete();
|
||||||
|
} else {
|
||||||
|
await callback(undefined);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
signObject(obj) {
|
signObject(obj) {
|
||||||
@ -273,4 +297,12 @@ export class Account {
|
|||||||
obj.unsigned = unsigned;
|
obj.unsigned = unsigned;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pickleWithKey(key) {
|
||||||
|
return this._account.pickle(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
dispose() {
|
||||||
|
this._account.free();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
88
src/matrix/e2ee/Dehydration.js
Normal file
88
src/matrix/e2ee/Dehydration.js
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const DEHYDRATION_LIBOLM_PICKLE_ALGORITHM = "org.matrix.msc2697.v1.olm.libolm_pickle";
|
||||||
|
|
||||||
|
async function getDehydratedDevice(hsApi, olm) {
|
||||||
|
const response = await hsApi.getDehydratedDevice().response();
|
||||||
|
if (response.device_data.algorithm === DEHYDRATION_LIBOLM_PICKLE_ALGORITHM) {
|
||||||
|
return new DehydratedDevice(response, olm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function hasRemainingDevice(ownUserId, ownDeviceId, storage) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadAccountAsDehydratedDevice(account, hsApi, key, deviceDisplayName, log) {
|
||||||
|
const response = await hsApi.createDehydratedDevice({
|
||||||
|
device_data: {
|
||||||
|
algorithm: DEHYDRATION_LIBOLM_PICKLE_ALGORITHM,
|
||||||
|
account: account.pickleWithKey(new Uint8Array(key)),
|
||||||
|
passphrase: {}
|
||||||
|
},
|
||||||
|
initial_device_display_name: deviceDisplayName
|
||||||
|
}).response();
|
||||||
|
const deviceId = response.device_id;
|
||||||
|
await account.uploadKeys(undefined, deviceId, log);
|
||||||
|
return deviceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
class EncryptedDehydratedDevice {
|
||||||
|
constructor(dehydratedDevice, olm) {
|
||||||
|
this._dehydratedDevice = dehydratedDevice;
|
||||||
|
this._olm = olm;
|
||||||
|
}
|
||||||
|
|
||||||
|
decrypt(key) {
|
||||||
|
const account = new this._olm.Account();
|
||||||
|
try {
|
||||||
|
const pickledAccount = this._dehydratedDevice.device_data.account;
|
||||||
|
account.unpickle(key, pickledAccount);
|
||||||
|
return new DehydratedDevice(this._dehydratedDevice, account);
|
||||||
|
} catch (err) {
|
||||||
|
account.free();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DehydratedDevice {
|
||||||
|
constructor(dehydratedDevice, account) {
|
||||||
|
this._dehydratedDevice = dehydratedDevice;
|
||||||
|
this._account = account;
|
||||||
|
}
|
||||||
|
|
||||||
|
claim(hsApi, log) {
|
||||||
|
try {
|
||||||
|
const response = await hsApi.claimDehydratedDevice(this.deviceId, {log}).response();
|
||||||
|
return response.success;
|
||||||
|
} catch (err) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// make it clear that ownership is transfered upon calling this
|
||||||
|
adoptUnpickledOlmAccount() {
|
||||||
|
const account = this._account;
|
||||||
|
this._account = null;
|
||||||
|
return account;
|
||||||
|
}
|
||||||
|
|
||||||
|
get deviceId() {
|
||||||
|
this._dehydratedDevice.device_id;
|
||||||
|
}
|
||||||
|
}
|
@ -18,6 +18,9 @@ limitations under the License.
|
|||||||
import {encodeQueryParams, encodeBody} from "./common.js";
|
import {encodeQueryParams, encodeBody} from "./common.js";
|
||||||
import {HomeServerRequest} from "./HomeServerRequest.js";
|
import {HomeServerRequest} from "./HomeServerRequest.js";
|
||||||
|
|
||||||
|
const CS_R0_PREFIX = "/_matrix/client/r0";
|
||||||
|
const DEHYDRATION_PREFIX = "/unstable/org.matrix.msc2697.v2";
|
||||||
|
|
||||||
export class HomeServerApi {
|
export class HomeServerApi {
|
||||||
constructor({homeserver, accessToken, request, reconnector}) {
|
constructor({homeserver, accessToken, request, reconnector}) {
|
||||||
// store these both in a closure somehow so it's harder to get at in case of XSS?
|
// store these both in a closure somehow so it's harder to get at in case of XSS?
|
||||||
@ -28,8 +31,8 @@ export class HomeServerApi {
|
|||||||
this._reconnector = reconnector;
|
this._reconnector = reconnector;
|
||||||
}
|
}
|
||||||
|
|
||||||
_url(csPath) {
|
_url(csPath, prefix) {
|
||||||
return `${this._homeserver}/_matrix/client/r0${csPath}`;
|
return this._homeserver + prefix + csPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
_baseRequest(method, url, queryParams, body, options, accessToken) {
|
_baseRequest(method, url, queryParams, body, options, accessToken) {
|
||||||
@ -92,15 +95,15 @@ export class HomeServerApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_post(csPath, queryParams, body, options) {
|
_post(csPath, queryParams, body, options) {
|
||||||
return this._authedRequest("POST", this._url(csPath), queryParams, body, options);
|
return this._authedRequest("POST", this._url(csPath, options?.prefix || CS_R0_PREFIX), queryParams, body, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
_put(csPath, queryParams, body, options) {
|
_put(csPath, queryParams, body, options) {
|
||||||
return this._authedRequest("PUT", this._url(csPath), queryParams, body, options);
|
return this._authedRequest("PUT", this._url(csPath, options?.prefix || CS_R0_PREFIX), queryParams, body, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
_get(csPath, queryParams, body, options) {
|
_get(csPath, queryParams, body, options) {
|
||||||
return this._authedRequest("GET", this._url(csPath), queryParams, body, options);
|
return this._authedRequest("GET", this._url(csPath, options?.prefix || CS_R0_PREFIX), queryParams, body, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
sync(since, filter, timeout, options = null) {
|
sync(since, filter, timeout, options = null) {
|
||||||
@ -170,8 +173,12 @@ export class HomeServerApi {
|
|||||||
return this._unauthedRequest("GET", `${this._homeserver}/_matrix/client/versions`, null, null, options);
|
return this._unauthedRequest("GET", `${this._homeserver}/_matrix/client/versions`, null, null, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
uploadKeys(payload, options = null) {
|
uploadKeys(dehydratedDeviceId, payload, options = null) {
|
||||||
return this._post("/keys/upload", null, payload, options);
|
let path = "/keys/upload";
|
||||||
|
if (dehydratedDeviceId) {
|
||||||
|
path = path + `/${encodeURIComponent(dehydratedDeviceId)}`;
|
||||||
|
}
|
||||||
|
return this._post(path, null, payload, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
queryKeys(queryRequest, options = null) {
|
queryKeys(queryRequest, options = null) {
|
||||||
@ -229,6 +236,21 @@ export class HomeServerApi {
|
|||||||
logout(options = null) {
|
logout(options = null) {
|
||||||
return this._post(`/logout`, null, null, options);
|
return this._post(`/logout`, null, null, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getDehydratedDevice(options = {}) {
|
||||||
|
options.prefix = DEHYDRATION_PREFIX;
|
||||||
|
return this._get(`/dehydrated_device`, null, null, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
createDehydratedDevice(payload, options = null) {
|
||||||
|
options.prefix = DEHYDRATION_PREFIX;
|
||||||
|
return this._put(`/dehydrated_device`, null, payload, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
claimDehydratedDevice(deviceId) {
|
||||||
|
options.prefix = DEHYDRATION_PREFIX;
|
||||||
|
return this._post(`/dehydrated_device/claim`, null, {device_id: deviceId}, options);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
import {Request as MockRequest} from "../../mocks/Request.js";
|
import {Request as MockRequest} from "../../mocks/Request.js";
|
||||||
|
Loading…
Reference in New Issue
Block a user