Merge branch 'master' into bwindels/decrypt-images

This commit is contained in:
Bruno Windels 2020-10-26 17:08:29 +01:00
commit 3ed5ea8b0b
139 changed files with 372 additions and 372 deletions

View file

@ -1,7 +1,7 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no"> <meta name="viewport" content="width=device-width, user-scalable=no">
<meta name="application-name" content="Hydrogen Chat"/> <meta name="application-name" content="Hydrogen Chat"/>
<meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-capable" content="yes">
@ -9,25 +9,26 @@
<meta name="apple-mobile-web-app-title" content="Hydrogen Chat"> <meta name="apple-mobile-web-app-title" content="Hydrogen Chat">
<meta name="description" content="A matrix chat application"> <meta name="description" content="A matrix chat application">
<link rel="apple-touch-icon" href="assets/icon-maskable.png"> <link rel="apple-touch-icon" href="assets/icon-maskable.png">
<link rel="stylesheet" type="text/css" href="src/ui/web/css/main.css"> <link rel="stylesheet" type="text/css" href="src/platform/web/ui/css/main.css">
<link rel="stylesheet" type="text/css" href="src/ui/web/css/themes/element/theme.css" title="Element Theme"> <link rel="stylesheet" type="text/css" href="src/platform/web/ui/css/themes/element/theme.css" title="Element Theme">
<link rel="alternate stylesheet" type="text/css" href="src/ui/web/css/themes/bubbles/theme.css" title="Bubbles Theme"> <link rel="alternate stylesheet" type="text/css" href="src/platform/web/ui/css/themes/bubbles/theme.css" title="Bubbles Theme">
</head> </head>
<body class="hydrogen"> <body class="hydrogen">
<script id="version" type="disabled"> <script id="version" type="disabled">
window.HYDROGEN_VERSION = "%%VERSION%%"; window.HYDROGEN_VERSION = "%%VERSION%%";
window.HYDROGEN_GLOBAL_HASH = "%%GLOBAL_HASH%%"; window.HYDROGEN_GLOBAL_HASH = "%%GLOBAL_HASH%%";
</script> </script>
<script id="main" type="module"> <script id="main" type="module">
import {main} from "./src/main.js"; import {main} from "./src/main.js";
main(document.body, { import {Platform} from "./src/platform/web/Platform.js";
main(new Platform(document.body, {
worker: "src/worker.js", worker: "src/worker.js",
olm: { olm: {
wasm: "lib/olm/olm.wasm", wasm: "lib/olm/olm.wasm",
legacyBundle: "lib/olm/olm_legacy.js", legacyBundle: "lib/olm/olm_legacy.js",
wasmBundle: "lib/olm/olm.js", wasmBundle: "lib/olm/olm.js",
} }
}); }));
</script> </script>
</body> </body>
</html> </html>

View file

@ -1,6 +1,6 @@
{ {
"name": "hydrogen-web", "name": "hydrogen-web",
"version": "0.1.19", "version": "0.1.20",
"description": "A javascript matrix client prototype, trying to minize RAM usage by offloading as much as possible to IndexedDB", "description": "A javascript matrix client prototype, trying to minize RAM usage by offloading as much as possible to IndexedDB",
"main": "index.js", "main": "index.js",
"directories": { "directories": {

View file

@ -45,12 +45,12 @@ import flexbugsFixes from "postcss-flexbugs-fixes";
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename); const __dirname = dirname(__filename);
const projectDir = path.join(__dirname, "../"); const projectDir = path.join(__dirname, "../");
const cssSrcDir = path.join(projectDir, "src/ui/web/css/"); const cssSrcDir = path.join(projectDir, "src/platform/web/ui/css/");
const program = new commander.Command(); const parameters = new commander.Command();
program parameters
.option("--modern-only", "don't make a legacy build") .option("--modern-only", "don't make a legacy build")
program.parse(process.argv); parameters.parse(process.argv);
async function build({modernOnly}) { async function build({modernOnly}) {
// get version number // get version number
@ -70,10 +70,13 @@ async function build({modernOnly}) {
// copy olm assets // copy olm assets
const olmAssets = await copyFolder(path.join(projectDir, "lib/olm/"), assets.directory); const olmAssets = await copyFolder(path.join(projectDir, "lib/olm/"), assets.directory);
assets.addSubMap(olmAssets); assets.addSubMap(olmAssets);
await assets.write(`hydrogen.js`, await buildJs("src/main.js")); await assets.write(`hydrogen.js`, await buildJs("src/main.js", ["src/platform/web/Platform.js"]));
if (!modernOnly) { if (!modernOnly) {
await assets.write(`hydrogen-legacy.js`, await buildJsLegacy("src/main.js", ['src/legacy-polyfill.js', 'src/legacy-extras.js'])); await assets.write(`hydrogen-legacy.js`, await buildJsLegacy("src/main.js", [
await assets.write(`worker.js`, await buildJsLegacy("src/worker.js", ['src/worker-polyfill.js'])); 'src/platform/web/legacy-polyfill.js',
'src/platform/web/LegacyPlatform.js'
]));
await assets.write(`worker.js`, await buildJsLegacy("src/platform/web/worker/main.js", ['src/platform/web/worker/polyfill.js']));
} }
// creates the directories where the theme css bundles are placed in, // creates the directories where the theme css bundles are placed in,
// and writes to assets, so the build bundles can translate them, so do it first // and writes to assets, so the build bundles can translate them, so do it first
@ -82,7 +85,7 @@ async function build({modernOnly}) {
await buildManifest(assets); await buildManifest(assets);
// all assets have been added, create a hash from all assets name to cache unhashed files like index.html // all assets have been added, create a hash from all assets name to cache unhashed files like index.html
assets.addToHashForAll("index.html", devHtml); assets.addToHashForAll("index.html", devHtml);
let swSource = await fs.readFile(path.join(projectDir, "src/service-worker.template.js"), "utf8"); let swSource = await fs.readFile(path.join(projectDir, "src/platform/web/service-worker.template.js"), "utf8");
assets.addToHashForAll("sw.js", swSource); assets.addToHashForAll("sw.js", swSource);
const globalHash = assets.hashForAll(); const globalHash = assets.hashForAll();
@ -148,12 +151,12 @@ async function buildHtml(doc, version, globalHash, modernOnly, assets) {
} }
}); });
const mainScripts = [ const mainScripts = [
`<script type="module">import {main} from "./${assets.resolve(`hydrogen.js`)}"; main(document.body, ${pathsJSON});</script>` `<script type="module">import {main, Platform} from "./${assets.resolve(`hydrogen.js`)}"; main(new Platform(document.body, ${pathsJSON}));</script>`
]; ];
if (!modernOnly) { if (!modernOnly) {
mainScripts.push( mainScripts.push(
`<script type="text/javascript" nomodule src="${assets.resolve(`hydrogen-legacy.js`)}"></script>`, `<script type="text/javascript" nomodule src="${assets.resolve(`hydrogen-legacy.js`)}"></script>`,
`<script type="text/javascript" nomodule>hydrogenBundle.main(document.body, ${pathsJSON}, hydrogenBundle.legacyExtras);</script>` `<script type="text/javascript" nomodule>hydrogen.main(new hydrogen.Platform(document.body, ${pathsJSON}));</script>`
); );
} }
doc("script#main").replaceWith(mainScripts.join("")); doc("script#main").replaceWith(mainScripts.join(""));
@ -168,16 +171,16 @@ async function buildHtml(doc, version, globalHash, modernOnly, assets) {
await assets.writeUnhashed("index.html", doc.html()); await assets.writeUnhashed("index.html", doc.html());
} }
async function buildJs(inputFile) { async function buildJs(mainFile, extraFiles = []) {
// create js bundle // create js bundle
const bundle = await rollup({ const bundle = await rollup({
input: inputFile, input: extraFiles.concat(mainFile),
plugins: [removeJsComments({comments: "none"})] plugins: [multi(), removeJsComments({comments: "none"})]
}); });
const {output} = await bundle.generate({ const {output} = await bundle.generate({
format: 'es', format: 'es',
// TODO: can remove this? // TODO: can remove this?
name: `hydrogenBundle` name: `hydrogen`
}); });
const code = output[0].code; const code = output[0].code;
return code; return code;
@ -214,7 +217,7 @@ async function buildJsLegacy(mainFile, extraFiles = []) {
const bundle = await rollup(rollupConfig); const bundle = await rollup(rollupConfig);
const {output} = await bundle.generate({ const {output} = await bundle.generate({
format: 'iife', format: 'iife',
name: `hydrogenBundle` name: `hydrogen`
}); });
const code = output[0].code; const code = output[0].code;
return code; return code;
@ -460,4 +463,4 @@ class AssetMap {
} }
} }
build(program).catch(err => console.error(err)); build(parameters).catch(err => console.error(err));

View file

@ -1,17 +0,0 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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 {WebPlatform as Platform} from "./ui/web/WebPlatform.js";

View file

@ -23,10 +23,7 @@ import {ViewModel} from "./ViewModel.js";
export class RootViewModel extends ViewModel { export class RootViewModel extends ViewModel {
constructor(options) { constructor(options) {
super(options); super(options);
const {createSessionContainer, sessionInfoStorage, storageFactory} = options; this._createSessionContainer = options.createSessionContainer;
this._createSessionContainer = createSessionContainer;
this._sessionInfoStorage = sessionInfoStorage;
this._storageFactory = storageFactory;
this._error = null; this._error = null;
this._sessionPickerViewModel = null; this._sessionPickerViewModel = null;
this._sessionLoadViewModel = null; this._sessionLoadViewModel = null;
@ -73,7 +70,7 @@ export class RootViewModel extends ViewModel {
if (restoreUrlIfAtDefault) { if (restoreUrlIfAtDefault) {
this.urlCreator.pushUrl(restoreUrlIfAtDefault); this.urlCreator.pushUrl(restoreUrlIfAtDefault);
} else { } else {
const sessionInfos = await this._sessionInfoStorage.getAll(); const sessionInfos = await this.platform.sessionInfoStorage.getAll();
if (sessionInfos.length === 0) { if (sessionInfos.length === 0) {
this.navigation.push("login"); this.navigation.push("login");
} else if (sessionInfos.length === 1) { } else if (sessionInfos.length === 1) {
@ -90,10 +87,7 @@ export class RootViewModel extends ViewModel {
async _showPicker() { async _showPicker() {
this._setSection(() => { this._setSection(() => {
this._sessionPickerViewModel = new SessionPickerViewModel(this.childOptions({ this._sessionPickerViewModel = new SessionPickerViewModel(this.childOptions());
sessionInfoStorage: this._sessionInfoStorage,
storageFactory: this._storageFactory,
}));
}); });
try { try {
await this._sessionPickerViewModel.load(); await this._sessionPickerViewModel.load();
@ -125,11 +119,7 @@ export class RootViewModel extends ViewModel {
_showSession(sessionContainer) { _showSession(sessionContainer) {
this._setSection(() => { this._setSection(() => {
this._sessionViewModel = new SessionViewModel(this.childOptions({ this._sessionViewModel = new SessionViewModel(this.childOptions({sessionContainer}));
sessionContainer,
updateService: this.getOption("updateService"),
estimateStorageUsage: this.getOption("estimateStorageUsage"),
}));
this._sessionViewModel.start(); this._sessionViewModel.start();
}); });
} }

View file

@ -130,9 +130,6 @@ class SessionItemViewModel extends ViewModel {
export class SessionPickerViewModel extends ViewModel { export class SessionPickerViewModel extends ViewModel {
constructor(options) { constructor(options) {
super(options); super(options);
const {storageFactory, sessionInfoStorage} = options;
this._storageFactory = storageFactory;
this._sessionInfoStorage = sessionInfoStorage;
this._sessions = new SortedArray((s1, s2) => s1.id.localeCompare(s2.id)); this._sessions = new SortedArray((s1, s2) => s1.id.localeCompare(s2.id));
this._loadViewModel = null; this._loadViewModel = null;
this._error = null; this._error = null;
@ -140,7 +137,7 @@ export class SessionPickerViewModel extends ViewModel {
// this loads all the sessions // this loads all the sessions
async load() { async load() {
const sessions = await this._sessionInfoStorage.getAll(); const sessions = await this.platform.sessionInfoStorage.getAll();
this._sessions.setManyUnsorted(sessions.map(s => { this._sessions.setManyUnsorted(sessions.map(s => {
return new SessionItemViewModel(this.childOptions({sessionInfo: s}), this); return new SessionItemViewModel(this.childOptions({sessionInfo: s}), this);
})); }));
@ -152,8 +149,8 @@ export class SessionPickerViewModel extends ViewModel {
} }
async _exportData(id) { async _exportData(id) {
const sessionInfo = await this._sessionInfoStorage.get(id); const sessionInfo = await this.platform.sessionInfoStorage.get(id);
const stores = await this._storageFactory.export(id); const stores = await this.platform.storageFactory.export(id);
const data = {sessionInfo, stores}; const data = {sessionInfo, stores};
return data; return data;
} }
@ -164,8 +161,8 @@ export class SessionPickerViewModel extends ViewModel {
const {sessionInfo} = data; const {sessionInfo} = data;
sessionInfo.comment = `Imported on ${new Date().toLocaleString()} from id ${sessionInfo.id}.`; sessionInfo.comment = `Imported on ${new Date().toLocaleString()} from id ${sessionInfo.id}.`;
sessionInfo.id = this._createSessionContainer().createNewSessionId(); sessionInfo.id = this._createSessionContainer().createNewSessionId();
await this._storageFactory.import(sessionInfo.id, data.stores); await this.platform.storageFactory.import(sessionInfo.id, data.stores);
await this._sessionInfoStorage.add(sessionInfo); await this.platform.sessionInfoStorage.add(sessionInfo);
this._sessions.set(new SessionItemViewModel(sessionInfo, this)); this._sessions.set(new SessionItemViewModel(sessionInfo, this));
} catch (err) { } catch (err) {
alert(err.message); alert(err.message);
@ -175,13 +172,13 @@ export class SessionPickerViewModel extends ViewModel {
async delete(id) { async delete(id) {
const idx = this._sessions.array.findIndex(s => s.id === id); const idx = this._sessions.array.findIndex(s => s.id === id);
await this._sessionInfoStorage.delete(id); await this.platform.sessionInfoStorage.delete(id);
await this._storageFactory.delete(id); await this.platform.storageFactory.delete(id);
this._sessions.remove(idx); this._sessions.remove(idx);
} }
async clear(id) { async clear(id) {
await this._storageFactory.delete(id); await this.platform.storageFactory.delete(id);
} }
get sessions() { get sessions() {

View file

@ -30,8 +30,8 @@ export class ViewModel extends EventEmitter {
} }
childOptions(explicitOptions) { childOptions(explicitOptions) {
const {navigation, urlCreator, clock} = this._options; const {navigation, urlCreator, platform} = this._options;
return Object.assign({navigation, urlCreator, clock}, explicitOptions); return Object.assign({navigation, urlCreator, platform}, explicitOptions);
} }
// makes it easier to pass through dependencies of a sub-view model // makes it easier to pass through dependencies of a sub-view model
@ -100,8 +100,12 @@ export class ViewModel extends EventEmitter {
} }
} }
get platform() {
return this._options.platform;
}
get clock() { get clock() {
return this._options.clock; return this._options.platform.clock;
} }
/** /**

View file

@ -188,9 +188,7 @@ export class SessionViewModel extends ViewModel {
} }
if (settingsOpen) { if (settingsOpen) {
this._settingsViewModel = this.track(new SettingsViewModel(this.childOptions({ this._settingsViewModel = this.track(new SettingsViewModel(this.childOptions({
updateService: this.getOption("updateService"),
session: this._sessionContainer.session, session: this._sessionContainer.session,
estimateStorageUsage: this.getOption("estimateStorageUsage")
}))); })));
this._settingsViewModel.load(); this._settingsViewModel.load();
} }

View file

@ -43,7 +43,7 @@ export class TimelineViewModel extends ViewModel {
// once we support sending messages we could do // once we support sending messages we could do
// timeline.entries.concat(timeline.pendingEvents) // timeline.entries.concat(timeline.pendingEvents)
// for an ObservableList that also contains local echos // for an ObservableList that also contains local echos
this._tiles = new TilesCollection(timeline.entries, tilesCreator({room, ownUserId, clock: this.clock})); this._tiles = new TilesCollection(timeline.entries, tilesCreator({room, ownUserId, platform: this.platform}));
} }
async load() { async load() {

View file

@ -21,7 +21,6 @@ export class MessageTile extends SimpleTile {
constructor(options) { constructor(options) {
super(options); super(options);
this._mediaRepository = options.mediaRepository; this._mediaRepository = options.mediaRepository;
this._clock = options.clock;
this._isOwn = this._entry.sender === options.ownUserId; this._isOwn = this._entry.sender === options.ownUserId;
this._date = this._entry.timestamp ? new Date(this._entry.timestamp) : null; this._date = this._entry.timestamp ? new Date(this._entry.timestamp) : null;
this._isContinuation = false; this._isContinuation = false;
@ -88,8 +87,8 @@ export class MessageTile extends SimpleTile {
let isContinuation = false; let isContinuation = false;
if (prev && prev instanceof MessageTile && prev.sender === this.sender) { if (prev && prev instanceof MessageTile && prev.sender === this.sender) {
// timestamp is null for pending events // timestamp is null for pending events
const myTimestamp = this._entry.timestamp || this._clock.now(); const myTimestamp = this._entry.timestamp || this.clock.now();
const otherTimestamp = prev._entry.timestamp || this._clock.now(); const otherTimestamp = prev._entry.timestamp || this.clock.now();
// other message was sent less than 5min ago // other message was sent less than 5min ago
isContinuation = (myTimestamp - otherTimestamp) < (5 * 60 * 1000); isContinuation = (myTimestamp - otherTimestamp) < (5 * 60 * 1000);
} }

View file

@ -23,9 +23,9 @@ import {RoomMemberTile} from "./tiles/RoomMemberTile.js";
import {EncryptedEventTile} from "./tiles/EncryptedEventTile.js"; import {EncryptedEventTile} from "./tiles/EncryptedEventTile.js";
import {EncryptionEnabledTile} from "./tiles/EncryptionEnabledTile.js"; import {EncryptionEnabledTile} from "./tiles/EncryptionEnabledTile.js";
export function tilesCreator({room, ownUserId, clock}) { export function tilesCreator({room, ownUserId, platform}) {
return function tilesCreator(entry, emitUpdate) { return function tilesCreator(entry, emitUpdate) {
const options = {entry, emitUpdate, ownUserId, clock, const options = {entry, emitUpdate, ownUserId, platform,
mediaRepository: room.mediaRepository}; mediaRepository: room.mediaRepository};
if (entry.isGap) { if (entry.isGap) {
return new GapTile(options, room); return new GapTile(options, room);

View file

@ -35,12 +35,11 @@ export class SettingsViewModel extends ViewModel {
this._session = session; this._session = session;
this._sessionBackupViewModel = this.track(new SessionBackupViewModel(this.childOptions({session}))); this._sessionBackupViewModel = this.track(new SessionBackupViewModel(this.childOptions({session})));
this._closeUrl = this.urlCreator.urlUntilSegment("session"); this._closeUrl = this.urlCreator.urlUntilSegment("session");
this._estimateStorageUsage = options.estimateStorageUsage;
this._estimate = null; this._estimate = null;
} }
async load() { async load() {
this._estimate = await this._estimateStorageUsage(); this._estimate = await this.platform.estimateStorageUsage();
this.emitChange(""); this.emitChange("");
} }
@ -61,18 +60,19 @@ export class SettingsViewModel extends ViewModel {
} }
get version() { get version() {
if (this._updateService) { const {updateService} = this.platform;
return `${this._updateService.version} (${this._updateService.buildHash})`; if (updateService) {
return `${updateService.version} (${updateService.buildHash})`;
} }
return this.i18n`development version`; return this.i18n`development version`;
} }
checkForUpdate() { checkForUpdate() {
this._updateService?.checkForUpdate(); this.platform.updateService?.checkForUpdate();
} }
get showUpdateButton() { get showUpdateButton() {
return !!this._updateService; return !!this.platform.updateService;
} }
get sessionBackupViewModel() { get sessionBackupViewModel() {

View file

@ -1,6 +0,0 @@
import aesjs from "../lib/aes-js/index.js";
import {hkdf} from "./utils/crypto/hkdf.js";
// these are run-time dependencies that are only needed for the legacy bundle.
// they are exported here and passed into main to make them available to the app.
export const legacyExtras = {crypto:{aesjs, hkdf}};

View file

@ -16,82 +16,14 @@ limitations under the License.
*/ */
// import {RecordRequester, ReplayRequester} from "./matrix/net/request/replay.js"; // import {RecordRequester, ReplayRequester} from "./matrix/net/request/replay.js";
import {createFetchRequest} from "./matrix/net/request/fetch.js";
import {xhrRequest} from "./matrix/net/request/xhr.js";
import {SessionContainer} from "./matrix/SessionContainer.js"; import {SessionContainer} from "./matrix/SessionContainer.js";
import {StorageFactory} from "./matrix/storage/idb/StorageFactory.js";
import {SessionInfoStorage} from "./matrix/sessioninfo/localstorage/SessionInfoStorage.js";
import {RootViewModel} from "./domain/RootViewModel.js"; import {RootViewModel} from "./domain/RootViewModel.js";
import {createNavigation, createRouter} from "./domain/navigation/index.js"; import {createNavigation, createRouter} from "./domain/navigation/index.js";
import {RootView} from "./ui/web/RootView.js";
import {Clock} from "./ui/web/dom/Clock.js";
import {ServiceWorkerHandler} from "./ui/web/dom/ServiceWorkerHandler.js";
import {History} from "./ui/web/dom/History.js";
import {OnlineStatus} from "./ui/web/dom/OnlineStatus.js";
import {CryptoDriver} from "./ui/web/dom/CryptoDriver.js";
import {estimateStorageUsage} from "./ui/web/dom/StorageEstimate.js";
import {WorkerPool} from "./utils/WorkerPool.js";
import {OlmWorker} from "./matrix/e2ee/OlmWorker.js";
function addScript(src) {
return new Promise(function (resolve, reject) {
var s = document.createElement("script");
s.setAttribute("src", src );
s.onload=resolve;
s.onerror=reject;
document.body.appendChild(s);
});
}
async function loadOlm(olmPaths) {
// make crypto.getRandomValues available without
// a prefix on IE11, needed by olm to work
if (window.msCrypto && !window.crypto) {
window.crypto = window.msCrypto;
}
if (olmPaths) {
if (window.WebAssembly) {
await addScript(olmPaths.wasmBundle);
await window.Olm.init({locateFile: () => olmPaths.wasm});
} else {
await addScript(olmPaths.legacyBundle);
await window.Olm.init();
}
return window.Olm;
}
return null;
}
// make path relative to basePath,
// assuming it and basePath are relative to document
function relPath(path, basePath) {
const idx = basePath.lastIndexOf("/");
const dir = idx === -1 ? "" : basePath.slice(0, idx);
const dirCount = dir.length ? dir.split("/").length : 0;
return "../".repeat(dirCount) + path;
}
async function loadOlmWorker(paths) {
const workerPool = new WorkerPool(paths.worker, 4);
await workerPool.init();
const path = relPath(paths.olm.legacyBundle, paths.worker);
await workerPool.sendAll({type: "load_olm", path});
const olmWorker = new OlmWorker(workerPool);
return olmWorker;
}
// Don't use a default export here, as we use multiple entries during legacy build, // Don't use a default export here, as we use multiple entries during legacy build,
// which does not support default exports, // which does not support default exports,
// see https://github.com/rollup/plugins/tree/master/packages/multi-entry // see https://github.com/rollup/plugins/tree/master/packages/multi-entry
export async function main(container, paths, legacyExtras) { export async function main(platform) {
try { try {
// TODO: add .legacy to .hydrogen (container) in (legacy)platform.createAndMountRootView; and use .hydrogen:not(.legacy) if needed for modern stuff
const isIE11 = !!window.MSInputMethodContext && !!document.documentMode;
if (isIE11) {
document.body.className += " ie11";
} else {
document.body.className += " not-ie11";
}
// to replay: // to replay:
// const fetchLog = await (await fetch("/fetchlogs/constrainterror.json")).json(); // const fetchLog = await (await fetch("/fetchlogs/constrainterror.json")).json();
// const replay = new ReplayRequester(fetchLog, {delay: false}); // const replay = new ReplayRequester(fetchLog, {delay: false});
@ -101,61 +33,25 @@ export async function main(container, paths, legacyExtras) {
// const recorder = new RecordRequester(createFetchRequest(clock.createTimeout)); // const recorder = new RecordRequester(createFetchRequest(clock.createTimeout));
// const request = recorder.request; // const request = recorder.request;
// window.getBrawlFetchLog = () => recorder.log(); // window.getBrawlFetchLog = () => recorder.log();
const clock = new Clock();
let request;
if (typeof fetch === "function") {
request = createFetchRequest(clock.createTimeout);
} else {
request = xhrRequest;
}
const navigation = createNavigation(); const navigation = createNavigation();
const sessionInfoStorage = new SessionInfoStorage("hydrogen_sessions_v1"); platform.setNavigation(navigation);
let serviceWorkerHandler; const urlRouter = createRouter({navigation, history: platform.history});
if (paths.serviceWorker && "serviceWorker" in navigator) {
serviceWorkerHandler = new ServiceWorkerHandler({navigation});
serviceWorkerHandler.registerAndStart(paths.serviceWorker);
}
const storageFactory = new StorageFactory(serviceWorkerHandler);
const olmPromise = loadOlm(paths.olm);
// if wasm is not supported, we'll want
// to run some olm operations in a worker (mainly for IE11)
let workerPromise;
if (!window.WebAssembly) {
workerPromise = loadOlmWorker(paths);
}
const urlRouter = createRouter({navigation, history: new History()});
urlRouter.attach(); urlRouter.attach();
const olmPromise = platform.loadOlm();
const workerPromise = platform.loadOlmWorker();
const vm = new RootViewModel({ const vm = new RootViewModel({
createSessionContainer: () => { createSessionContainer: () => {
return new SessionContainer({ return new SessionContainer({platform, olmPromise, workerPromise});
random: Math.random,
onlineStatus: new OnlineStatus(),
storageFactory,
sessionInfoStorage,
request,
clock,
cryptoDriver: new CryptoDriver(legacyExtras?.crypto),
olmPromise,
workerPromise,
});
}, },
sessionInfoStorage, platform,
storageFactory,
clock,
// the only public interface of the router is to create urls, // the only public interface of the router is to create urls,
// so we call it that in the view models // so we call it that in the view models
urlCreator: urlRouter, urlCreator: urlRouter,
navigation, navigation,
updateService: serviceWorkerHandler,
estimateStorageUsage
}); });
window.__hydrogenViewModel = vm;
await vm.load(); await vm.load();
// TODO: replace with platform.createAndMountRootView(vm, container); platform.createAndMountRootView(vm);
const view = new RootView(vm);
container.appendChild(view.mount());
} catch(err) { } catch(err) {
console.error(`${err.message}:\n${err.stack}`); console.error(`${err.message}:\n${err.stack}`);
} }

View file

@ -41,8 +41,8 @@ const PICKLE_KEY = "DEFAULT_KEY";
export class Session { export class Session {
// sessionInfo contains deviceId, userId and homeServer // sessionInfo contains deviceId, userId and homeServer
constructor({clock, storage, hsApi, sessionInfo, olm, olmWorker, cryptoDriver, mediaRepository}) { constructor({storage, hsApi, sessionInfo, olm, olmWorker, platform, mediaRepository}) {
this._clock = clock; this._platform = platform;
this._storage = storage; this._storage = storage;
this._hsApi = hsApi; this._hsApi = hsApi;
this._mediaRepository = mediaRepository; this._mediaRepository = mediaRepository;
@ -61,7 +61,6 @@ export class Session {
this._megolmDecryption = null; this._megolmDecryption = null;
this._getSyncToken = () => this.syncToken; this._getSyncToken = () => this.syncToken;
this._olmWorker = olmWorker; this._olmWorker = olmWorker;
this._cryptoDriver = cryptoDriver;
this._sessionBackup = null; this._sessionBackup = null;
this._hasSecretStorageKey = new ObservableValue(null); this._hasSecretStorageKey = new ObservableValue(null);
@ -106,7 +105,7 @@ export class Session {
pickleKey: PICKLE_KEY, pickleKey: PICKLE_KEY,
olm: this._olm, olm: this._olm,
storage: this._storage, storage: this._storage,
now: this._clock.now, now: this._platform.clock.now,
ownUserId: this._user.id, ownUserId: this._user.id,
senderKeyLock senderKeyLock
}); });
@ -115,7 +114,7 @@ export class Session {
pickleKey: PICKLE_KEY, pickleKey: PICKLE_KEY,
olm: this._olm, olm: this._olm,
storage: this._storage, storage: this._storage,
now: this._clock.now, now: this._platform.clock.now,
ownUserId: this._user.id, ownUserId: this._user.id,
olmUtil: this._olmUtil, olmUtil: this._olmUtil,
senderKeyLock senderKeyLock
@ -125,7 +124,7 @@ export class Session {
pickleKey: PICKLE_KEY, pickleKey: PICKLE_KEY,
olm: this._olm, olm: this._olm,
storage: this._storage, storage: this._storage,
now: this._clock.now, now: this._platform.clock.now,
ownDeviceId: this._sessionInfo.deviceId, ownDeviceId: this._sessionInfo.deviceId,
}); });
this._megolmDecryption = new MegOlmDecryption({ this._megolmDecryption = new MegOlmDecryption({
@ -166,7 +165,7 @@ export class Session {
this.needsSessionBackup.set(true) this.needsSessionBackup.set(true)
} }
}, },
clock: this._clock clock: this._platform.clock
}); });
} }
@ -185,7 +184,7 @@ export class Session {
if (this._sessionBackup) { if (this._sessionBackup) {
return false; return false;
} }
const key = await ssssKeyFromCredential(type, credential, this._storage, this._cryptoDriver, this._olm); const key = await ssssKeyFromCredential(type, credential, this._storage, this._platform.crypto, this._olm);
// and create session backup, which needs to read from accountData // and create session backup, which needs to read from accountData
const readTxn = this._storage.readTxn([ const readTxn = this._storage.readTxn([
this._storage.storeNames.accountData, this._storage.storeNames.accountData,
@ -207,7 +206,7 @@ export class Session {
} }
async _createSessionBackup(ssssKey, txn) { async _createSessionBackup(ssssKey, txn) {
const secretStorage = new SecretStorage({key: ssssKey, cryptoDriver: this._cryptoDriver}); const secretStorage = new SecretStorage({key: ssssKey, crypto: this._platform.crypto});
this._sessionBackup = await SessionBackup.fromSecretStorage({olm: this._olm, secretStorage, hsApi: this._hsApi, txn}); this._sessionBackup = await SessionBackup.fromSecretStorage({olm: this._olm, secretStorage, hsApi: this._hsApi, txn});
if (this._sessionBackup) { if (this._sessionBackup) {
for (const room of this._rooms.values()) { for (const room of this._rooms.values()) {
@ -363,7 +362,7 @@ export class Session {
pendingEvents, pendingEvents,
user: this._user, user: this._user,
createRoomEncryption: this._createRoomEncryption, createRoomEncryption: this._createRoomEncryption,
clock: this._clock clock: this._platform.clock
}); });
this._rooms.add(roomId, room); this._rooms.add(roomId, room);
return room; return room;

View file

@ -44,13 +44,8 @@ export const LoginFailure = createEnum(
); );
export class SessionContainer { export class SessionContainer {
constructor({clock, random, onlineStatus, request, storageFactory, sessionInfoStorage, olmPromise, workerPromise, cryptoDriver}) { constructor({platform, olmPromise, workerPromise}) {
this._random = random; this._platform = platform;
this._clock = clock;
this._onlineStatus = onlineStatus;
this._request = request;
this._storageFactory = storageFactory;
this._sessionInfoStorage = sessionInfoStorage;
this._sessionStartedByReconnector = false; this._sessionStartedByReconnector = false;
this._status = new ObservableValue(LoadStatus.NotLoading); this._status = new ObservableValue(LoadStatus.NotLoading);
this._error = null; this._error = null;
@ -63,11 +58,10 @@ export class SessionContainer {
this._requestScheduler = null; this._requestScheduler = null;
this._olmPromise = olmPromise; this._olmPromise = olmPromise;
this._workerPromise = workerPromise; this._workerPromise = workerPromise;
this._cryptoDriver = cryptoDriver;
} }
createNewSessionId() { createNewSessionId() {
return (Math.floor(this._random() * Number.MAX_SAFE_INTEGER)).toString(); return (Math.floor(this._platform.random() * Number.MAX_SAFE_INTEGER)).toString();
} }
get sessionId() { get sessionId() {
@ -80,7 +74,7 @@ export class SessionContainer {
} }
this._status.set(LoadStatus.Loading); this._status.set(LoadStatus.Loading);
try { try {
const sessionInfo = await this._sessionInfoStorage.get(sessionId); const sessionInfo = await this._platform.sessionInfoStorage.get(sessionId);
if (!sessionInfo) { if (!sessionInfo) {
throw new Error("Invalid session id: " + sessionId); throw new Error("Invalid session id: " + sessionId);
} }
@ -96,9 +90,11 @@ export class SessionContainer {
return; return;
} }
this._status.set(LoadStatus.Login); this._status.set(LoadStatus.Login);
const clock = this._platform.clock;
let sessionInfo; let sessionInfo;
try { try {
const hsApi = new HomeServerApi({homeServer, request: this._request, createTimeout: this._clock.createTimeout}); const request = this._platform.request;
const hsApi = new HomeServerApi({homeServer, request, createTimeout: clock.createTimeout});
const loginData = await hsApi.passwordLogin(username, password, "Hydrogen").response(); const loginData = await hsApi.passwordLogin(username, password, "Hydrogen").response();
const sessionId = this.createNewSessionId(); const sessionId = this.createNewSessionId();
sessionInfo = { sessionInfo = {
@ -107,9 +103,9 @@ export class SessionContainer {
userId: loginData.user_id, userId: loginData.user_id,
homeServer: homeServer, homeServer: homeServer,
accessToken: loginData.access_token, accessToken: loginData.access_token,
lastUsed: this._clock.now() lastUsed: clock.now()
}; };
await this._sessionInfoStorage.add(sessionInfo); await this._platform.sessionInfoStorage.add(sessionInfo);
} catch (err) { } catch (err) {
this._error = err; this._error = err;
if (err instanceof HomeServerError) { if (err instanceof HomeServerError) {
@ -139,22 +135,23 @@ export class SessionContainer {
} }
async _loadSessionInfo(sessionInfo, isNewLogin) { async _loadSessionInfo(sessionInfo, isNewLogin) {
const clock = this._platform.clock;
this._sessionStartedByReconnector = false; this._sessionStartedByReconnector = false;
this._status.set(LoadStatus.Loading); this._status.set(LoadStatus.Loading);
this._reconnector = new Reconnector({ this._reconnector = new Reconnector({
onlineStatus: this._onlineStatus, onlineStatus: this._platform.onlineStatus,
retryDelay: new ExponentialRetryDelay(this._clock.createTimeout), retryDelay: new ExponentialRetryDelay(clock.createTimeout),
createMeasure: this._clock.createMeasure createMeasure: clock.createMeasure
}); });
const hsApi = new HomeServerApi({ const hsApi = new HomeServerApi({
homeServer: sessionInfo.homeServer, homeServer: sessionInfo.homeServer,
accessToken: sessionInfo.accessToken, accessToken: sessionInfo.accessToken,
request: this._request, request: this._platform.request,
reconnector: this._reconnector, reconnector: this._reconnector,
createTimeout: this._clock.createTimeout createTimeout: clock.createTimeout
}); });
this._sessionId = sessionInfo.id; this._sessionId = sessionInfo.id;
this._storage = await this._storageFactory.create(sessionInfo.id); this._storage = await this._platform.storageFactory.create(sessionInfo.id);
// no need to pass access token to session // no need to pass access token to session
const filteredSessionInfo = { const filteredSessionInfo = {
deviceId: sessionInfo.deviceId, deviceId: sessionInfo.deviceId,
@ -166,22 +163,21 @@ export class SessionContainer {
if (this._workerPromise) { if (this._workerPromise) {
olmWorker = await this._workerPromise; olmWorker = await this._workerPromise;
} }
this._requestScheduler = new RequestScheduler({hsApi, clock: this._clock}); this._requestScheduler = new RequestScheduler({hsApi, clock});
this._requestScheduler.start(); this._requestScheduler.start();
const mediaRepository = new MediaRepository({ const mediaRepository = new MediaRepository({
homeServer: sessionInfo.homeServer, homeServer: sessionInfo.homeServer,
cryptoDriver: this._cryptoDriver, crypto: this._platform.crypto,
request: this._request, request: this._platform.request,
}); });
this._session = new Session({ this._session = new Session({
storage: this._storage, storage: this._storage,
sessionInfo: filteredSessionInfo, sessionInfo: filteredSessionInfo,
hsApi: this._requestScheduler.hsApi, hsApi: this._requestScheduler.hsApi,
olm, olm,
clock: this._clock,
olmWorker, olmWorker,
cryptoDriver: this._cryptoDriver, mediaRepository,
mediaRepository platform: this._platform,
}); });
await this._session.load(); await this._session.load();
if (isNewLogin) { if (isNewLogin) {
@ -298,8 +294,8 @@ export class SessionContainer {
// if one fails, don't block the other from trying // if one fails, don't block the other from trying
// also, run in parallel // also, run in parallel
await Promise.all([ await Promise.all([
this._storageFactory.delete(this._sessionId), this._platform.storageFactory.delete(this._sessionId),
this._sessionInfoStorage.delete(this._sessionId), this._platform.sessionInfoStorage.delete(this._sessionId),
]); ]);
this._sessionId = null; this._sessionId = null;
} }

View file

@ -25,7 +25,7 @@ import base64 from "../../../lib/base64-arraybuffer/index.js";
* @param {string} info.hashes.sha256 Base64 encoded SHA-256 hash of the ciphertext. * @param {string} info.hashes.sha256 Base64 encoded SHA-256 hash of the ciphertext.
* @return {Promise} A promise that resolves with an ArrayBuffer when the attachment is decrypted. * @return {Promise} A promise that resolves with an ArrayBuffer when the attachment is decrypted.
*/ */
export async function decryptAttachment(cryptoDriver, ciphertextBuffer, info) { export async function decryptAttachment(crypto, ciphertextBuffer, info) {
if (info === undefined || info.key === undefined || info.iv === undefined if (info === undefined || info.key === undefined || info.iv === undefined
|| info.hashes === undefined || info.hashes.sha256 === undefined) { || info.hashes === undefined || info.hashes.sha256 === undefined) {
throw new Error("Invalid info. Missing info.key, info.iv or info.hashes.sha256 key"); throw new Error("Invalid info. Missing info.key, info.iv or info.hashes.sha256 key");
@ -35,7 +35,7 @@ export async function decryptAttachment(cryptoDriver, ciphertextBuffer, info) {
// re-encode to not deal with padded vs unpadded // re-encode to not deal with padded vs unpadded
var expectedSha256base64 = base64.encode(base64.decode(info.hashes.sha256)); var expectedSha256base64 = base64.encode(base64.decode(info.hashes.sha256));
// Check the sha256 hash // Check the sha256 hash
const digestResult = await cryptoDriver.digest("SHA-256", ciphertextBuffer); const digestResult = await crypto.digest("SHA-256", ciphertextBuffer);
if (base64.encode(new Uint8Array(digestResult)) != expectedSha256base64) { if (base64.encode(new Uint8Array(digestResult)) != expectedSha256base64) {
throw new Error("Mismatched SHA-256 digest"); throw new Error("Mismatched SHA-256 digest");
} }
@ -48,7 +48,7 @@ export async function decryptAttachment(cryptoDriver, ciphertextBuffer, info) {
counterLength = 128; counterLength = 128;
} }
const decryptedBuffer = await cryptoDriver.aes.decryptCTR({ const decryptedBuffer = await crypto.aes.decryptCTR({
jwkKey: info.key, jwkKey: info.key,
iv: ivArray, iv: ivArray,
data: ciphertextBuffer, data: ciphertextBuffer,

View file

@ -18,9 +18,9 @@ import {encodeQueryParams} from "./common.js";
import {decryptAttachment} from "../e2ee/attachment.js"; import {decryptAttachment} from "../e2ee/attachment.js";
export class MediaRepository { export class MediaRepository {
constructor({homeServer, cryptoDriver, request}) { constructor({homeServer, crypto, request}) {
this._homeServer = homeServer; this._homeServer = homeServer;
this._cryptoDriver = cryptoDriver; this._crypto = crypto;
this._request = request; this._request = request;
} }
@ -56,7 +56,7 @@ export class MediaRepository {
async downloadEncryptedFile(fileEntry) { async downloadEncryptedFile(fileEntry) {
const url = this.mxcUrl(fileEntry.url); const url = this.mxcUrl(fileEntry.url);
const {body: encryptedBuffer} = await this._request(url, {format: "buffer", cache: true}).response(); const {body: encryptedBuffer} = await this._request(url, {format: "buffer", cache: true}).response();
const decryptedBuffer = await decryptAttachment(this._cryptoDriver, encryptedBuffer, fileEntry); const decryptedBuffer = await decryptAttachment(this._crypto, encryptedBuffer, fileEntry);
return decryptedBuffer; return decryptedBuffer;
} }
} }

View file

@ -26,25 +26,3 @@ export function encodeQueryParams(queryParams) {
}) })
.join("&"); .join("&");
} }
export function addCacheBuster(urlStr, random = Math.random) {
// XHR doesn't have a good way to disable cache,
// so add a random query param
// see https://davidtranscend.com/blog/prevent-ie11-cache-ajax-requests/
if (urlStr.includes("?")) {
urlStr = urlStr + "&";
} else {
urlStr = urlStr + "?";
}
return urlStr + `_cacheBuster=${Math.ceil(random() * Number.MAX_SAFE_INTEGER)}`;
}
export function tests() {
return {
"add cache buster": assert => {
const random = () => 0.5;
assert.equal(addCacheBuster("http://foo", random), "http://foo?_cacheBuster=4503599627370496");
assert.equal(addCacheBuster("http://foo?bar=baz", random), "http://foo?bar=baz&_cacheBuster=4503599627370496");
}
}
}

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import {Platform} from "../../../Platform.js"; import {KeyLimits} from "../../storage/common.js";
// key for events in the timelineEvents store // key for events in the timelineEvents store
export class EventKey { export class EventKey {
@ -25,7 +25,7 @@ export class EventKey {
nextFragmentKey() { nextFragmentKey() {
// could take MIN_EVENT_INDEX here if it can't be paged back // could take MIN_EVENT_INDEX here if it can't be paged back
return new EventKey(this.fragmentId + 1, Platform.middleStorageKey); return new EventKey(this.fragmentId + 1, KeyLimits.middleStorageKey);
} }
nextKeyForDirection(direction) { nextKeyForDirection(direction) {
@ -45,19 +45,19 @@ export class EventKey {
} }
static get maxKey() { static get maxKey() {
return new EventKey(Platform.maxStorageKey, Platform.maxStorageKey); return new EventKey(KeyLimits.maxStorageKey, KeyLimits.maxStorageKey);
} }
static get minKey() { static get minKey() {
return new EventKey(Platform.minStorageKey, Platform.minStorageKey); return new EventKey(KeyLimits.minStorageKey, KeyLimits.minStorageKey);
} }
static get defaultLiveKey() { static get defaultLiveKey() {
return EventKey.defaultFragmentKey(Platform.minStorageKey); return EventKey.defaultFragmentKey(KeyLimits.minStorageKey);
} }
static defaultFragmentKey(fragmentId) { static defaultFragmentKey(fragmentId) {
return new EventKey(fragmentId, Platform.middleStorageKey); return new EventKey(fragmentId, KeyLimits.middleStorageKey);
} }
toString() { toString() {

View file

@ -17,7 +17,7 @@ limitations under the License.
import {BaseEntry} from "./BaseEntry.js"; import {BaseEntry} from "./BaseEntry.js";
import {Direction} from "../Direction.js"; import {Direction} from "../Direction.js";
import {isValidFragmentId} from "../common.js"; import {isValidFragmentId} from "../common.js";
import {Platform} from "../../../../Platform.js"; import {KeyLimits} from "../../../storage/common.js";
export class FragmentBoundaryEntry extends BaseEntry { export class FragmentBoundaryEntry extends BaseEntry {
constructor(fragment, isFragmentStart, fragmentIdComparer) { constructor(fragment, isFragmentStart, fragmentIdComparer) {
@ -53,9 +53,9 @@ export class FragmentBoundaryEntry extends BaseEntry {
get entryIndex() { get entryIndex() {
if (this.started) { if (this.started) {
return Platform.minStorageKey; return KeyLimits.minStorageKey;
} else { } else {
return Platform.maxStorageKey; return KeyLimits.maxStorageKey;
} }
} }

View file

@ -17,9 +17,9 @@ limitations under the License.
import base64 from "../../../lib/base64-arraybuffer/index.js"; import base64 from "../../../lib/base64-arraybuffer/index.js";
export class SecretStorage { export class SecretStorage {
constructor({key, cryptoDriver}) { constructor({key, crypto}) {
this._key = key; this._key = key;
this._cryptoDriver = cryptoDriver; this._crypto = crypto;
} }
async readSecret(name, txn) { async readSecret(name, txn) {
@ -44,7 +44,7 @@ export class SecretStorage {
const textEncoder = new TextEncoder(); const textEncoder = new TextEncoder();
const textDecoder = new TextDecoder(); const textDecoder = new TextDecoder();
// now derive the aes and mac key from the 4s key // now derive the aes and mac key from the 4s key
const hkdfKey = await this._cryptoDriver.derive.hkdf( const hkdfKey = await this._crypto.derive.hkdf(
this._key.binaryKey, this._key.binaryKey,
new Uint8Array(8).buffer, //zero salt new Uint8Array(8).buffer, //zero salt
textEncoder.encode(type), // info textEncoder.encode(type), // info
@ -56,7 +56,7 @@ export class SecretStorage {
const ciphertextBytes = base64.decode(encryptedData.ciphertext); const ciphertextBytes = base64.decode(encryptedData.ciphertext);
const isVerified = await this._cryptoDriver.hmac.verify( const isVerified = await this._crypto.hmac.verify(
hmacKey, base64.decode(encryptedData.mac), hmacKey, base64.decode(encryptedData.mac),
ciphertextBytes, "SHA-256"); ciphertextBytes, "SHA-256");
@ -64,7 +64,7 @@ export class SecretStorage {
throw new Error("Bad MAC"); throw new Error("Bad MAC");
} }
const plaintextBytes = await this._cryptoDriver.aes.decryptCTR({ const plaintextBytes = await this._crypto.aes.decryptCTR({
key: aesKey, key: aesKey,
iv: base64.decode(encryptedData.iv), iv: base64.decode(encryptedData.iv),
data: ciphertextBytes data: ciphertextBytes

View file

@ -47,14 +47,14 @@ export async function readKey(txn) {
return new Key(new KeyDescription(keyData.id, keyAccountData), keyData.binaryKey); return new Key(new KeyDescription(keyData.id, keyAccountData), keyData.binaryKey);
} }
export async function keyFromCredential(type, credential, storage, cryptoDriver, olm) { export async function keyFromCredential(type, credential, storage, crypto, olm) {
const keyDescription = await readDefaultKeyDescription(storage); const keyDescription = await readDefaultKeyDescription(storage);
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");
} }
let key; let key;
if (type === "phrase") { if (type === "phrase") {
key = await keyFromPassphrase(keyDescription, credential, cryptoDriver); key = await keyFromPassphrase(keyDescription, credential, crypto);
} else if (type === "key") { } else if (type === "key") {
key = keyFromRecoveryKey(olm, keyDescription, credential); key = keyFromRecoveryKey(olm, keyDescription, credential);
} else { } else {

View file

@ -22,10 +22,10 @@ const DEFAULT_BITSIZE = 256;
/** /**
* @param {KeyDescription} keyDescription * @param {KeyDescription} keyDescription
* @param {string} passphrase * @param {string} passphrase
* @param {CryptoDriver} cryptoDriver * @param {Crypto} crypto
* @return {Key} * @return {Key}
*/ */
export async function keyFromPassphrase(keyDescription, passphrase, cryptoDriver) { export async function keyFromPassphrase(keyDescription, passphrase, crypto) {
const {passphraseParams} = keyDescription; const {passphraseParams} = keyDescription;
if (!passphraseParams) { if (!passphraseParams) {
throw new Error("not a passphrase key"); throw new Error("not a passphrase key");
@ -35,7 +35,7 @@ export async function keyFromPassphrase(keyDescription, passphrase, cryptoDriver
} }
// TODO: we should we move this to platform specific code // TODO: we should we move this to platform specific code
const textEncoder = new TextEncoder(); const textEncoder = new TextEncoder();
const keyBits = await cryptoDriver.derive.pbkdf2( const keyBits = await crypto.derive.pbkdf2(
textEncoder.encode(passphrase), textEncoder.encode(passphrase),
passphraseParams.iterations || DEFAULT_ITERATIONS, passphraseParams.iterations || DEFAULT_ITERATIONS,
// salt is just a random string, not encoded in any way // salt is just a random string, not encoded in any way

View file

@ -50,3 +50,20 @@ export class StorageError extends Error {
return "StorageError"; return "StorageError";
} }
} }
export const KeyLimits = {
get minStorageKey() {
// for indexeddb, we use unsigned 32 bit integers as keys
return 0;
},
get middleStorageKey() {
// for indexeddb, we use unsigned 32 bit integers as keys
return 0x7FFFFFFF;
},
get maxStorageKey() {
// for indexeddb, we use unsigned 32 bit integers as keys
return 0xFFFFFFFF;
}
}

View file

@ -15,7 +15,7 @@ limitations under the License.
*/ */
import { encodeUint32, decodeUint32 } from "../utils.js"; import { encodeUint32, decodeUint32 } from "../utils.js";
import {Platform} from "../../../../Platform.js"; import {KeyLimits} from "../../common.js";
function encodeKey(roomId, queueIndex) { function encodeKey(roomId, queueIndex) {
return `${roomId}|${encodeUint32(queueIndex)}`; return `${roomId}|${encodeUint32(queueIndex)}`;
@ -34,8 +34,8 @@ export class PendingEventStore {
async getMaxQueueIndex(roomId) { async getMaxQueueIndex(roomId) {
const range = IDBKeyRange.bound( const range = IDBKeyRange.bound(
encodeKey(roomId, Platform.minStorageKey), encodeKey(roomId, KeyLimits.minStorageKey),
encodeKey(roomId, Platform.maxStorageKey), encodeKey(roomId, KeyLimits.maxStorageKey),
false, false,
false, false,
); );

View file

@ -17,7 +17,7 @@ limitations under the License.
import {EventKey} from "../../../room/timeline/EventKey.js"; import {EventKey} from "../../../room/timeline/EventKey.js";
import { StorageError } from "../../common.js"; import { StorageError } from "../../common.js";
import { encodeUint32 } from "../utils.js"; import { encodeUint32 } from "../utils.js";
import {Platform} from "../../../../Platform.js"; import {KeyLimits} from "../../common.js";
function encodeKey(roomId, fragmentId, eventIndex) { function encodeKey(roomId, fragmentId, eventIndex) {
return `${roomId}|${encodeUint32(fragmentId)}|${encodeUint32(eventIndex)}`; return `${roomId}|${encodeUint32(fragmentId)}|${encodeUint32(eventIndex)}`;
@ -52,7 +52,7 @@ class Range {
if (this._lower && !this._upper) { if (this._lower && !this._upper) {
return IDBKeyRange.bound( return IDBKeyRange.bound(
encodeKey(roomId, this._lower.fragmentId, this._lower.eventIndex), encodeKey(roomId, this._lower.fragmentId, this._lower.eventIndex),
encodeKey(roomId, this._lower.fragmentId, Platform.maxStorageKey), encodeKey(roomId, this._lower.fragmentId, KeyLimits.maxStorageKey),
this._lowerOpen, this._lowerOpen,
false false
); );
@ -61,7 +61,7 @@ class Range {
// also bound as we don't want to move into another roomId // also bound as we don't want to move into another roomId
if (!this._lower && this._upper) { if (!this._lower && this._upper) {
return IDBKeyRange.bound( return IDBKeyRange.bound(
encodeKey(roomId, this._upper.fragmentId, Platform.minStorageKey), encodeKey(roomId, this._upper.fragmentId, KeyLimits.minStorageKey),
encodeKey(roomId, this._upper.fragmentId, this._upper.eventIndex), encodeKey(roomId, this._upper.fragmentId, this._upper.eventIndex),
false, false,
this._upperOpen this._upperOpen

View file

@ -15,7 +15,7 @@ limitations under the License.
*/ */
import { StorageError } from "../../common.js"; import { StorageError } from "../../common.js";
import {Platform} from "../../../../Platform.js"; import {KeyLimits} from "../../common.js";
import { encodeUint32 } from "../utils.js"; import { encodeUint32 } from "../utils.js";
function encodeKey(roomId, fragmentId) { function encodeKey(roomId, fragmentId) {
@ -30,8 +30,8 @@ export class TimelineFragmentStore {
_allRange(roomId) { _allRange(roomId) {
try { try {
return IDBKeyRange.bound( return IDBKeyRange.bound(
encodeKey(roomId, Platform.minStorageKey), encodeKey(roomId, KeyLimits.minStorageKey),
encodeKey(roomId, Platform.maxStorageKey) encodeKey(roomId, KeyLimits.maxStorageKey)
); );
} catch (err) { } catch (err) {
throw new StorageError(`error from IDBKeyRange with roomId ${roomId}`, err); throw new StorageError(`error from IDBKeyRange with roomId ${roomId}`, err);

View file

@ -48,7 +48,7 @@ export async function checkNeedsSyncPromise() {
return needsSyncPromise; return needsSyncPromise;
} }
// storage keys are defined to be unsigned 32bit numbers in WebPlatform.js, which is assumed by idb // storage keys are defined to be unsigned 32bit numbers in KeyLimits, which is assumed by idb
export function encodeUint32(n) { export function encodeUint32(n) {
const hex = n.toString(16); const hex = n.toString(16);
return "0".repeat(8 - hex.length) + hex; return "0".repeat(8 - hex.length) + hex;

View file

@ -0,0 +1,7 @@
import aesjs from "../../../lib/aes-js/index.js";
import {hkdf} from "../../utils/crypto/hkdf.js";
import {Platform as ModernPlatform} from "./Platform.js";
export function Platform(container, paths) {
return new ModernPlatform(container, paths, {aesjs, hkdf});
}

View file

@ -0,0 +1,130 @@
/*
Copyright 2020 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.
*/
import {createFetchRequest} from "./dom/request/fetch.js";
import {xhrRequest} from "./dom/request/xhr.js";
import {StorageFactory} from "../../matrix/storage/idb/StorageFactory.js";
import {SessionInfoStorage} from "../../matrix/sessioninfo/localstorage/SessionInfoStorage.js";
import {OlmWorker} from "../../matrix/e2ee/OlmWorker.js";
import {RootView} from "./ui/RootView.js";
import {Clock} from "./dom/Clock.js";
import {ServiceWorkerHandler} from "./dom/ServiceWorkerHandler.js";
import {History} from "./dom/History.js";
import {OnlineStatus} from "./dom/OnlineStatus.js";
import {Crypto} from "./dom/Crypto.js";
import {estimateStorageUsage} from "./dom/StorageEstimate.js";
import {WorkerPool} from "./dom/WorkerPool.js";
function addScript(src) {
return new Promise(function (resolve, reject) {
var s = document.createElement("script");
s.setAttribute("src", src );
s.onload=resolve;
s.onerror=reject;
document.body.appendChild(s);
});
}
async function loadOlm(olmPaths) {
// make crypto.getRandomValues available without
// a prefix on IE11, needed by olm to work
if (window.msCrypto && !window.crypto) {
window.crypto = window.msCrypto;
}
if (olmPaths) {
if (window.WebAssembly) {
await addScript(olmPaths.wasmBundle);
await window.Olm.init({locateFile: () => olmPaths.wasm});
} else {
await addScript(olmPaths.legacyBundle);
await window.Olm.init();
}
return window.Olm;
}
return null;
}
// make path relative to basePath,
// assuming it and basePath are relative to document
function relPath(path, basePath) {
const idx = basePath.lastIndexOf("/");
const dir = idx === -1 ? "" : basePath.slice(0, idx);
const dirCount = dir.length ? dir.split("/").length : 0;
return "../".repeat(dirCount) + path;
}
async function loadOlmWorker(paths) {
const workerPool = new WorkerPool(paths.worker, 4);
await workerPool.init();
const path = relPath(paths.olm.legacyBundle, paths.worker);
await workerPool.sendAll({type: "load_olm", path});
const olmWorker = new OlmWorker(workerPool);
return olmWorker;
}
export class Platform {
constructor(container, paths, cryptoExtras = null) {
this._paths = paths;
this._container = container;
this.clock = new Clock();
this.history = new History();
this.onlineStatus = new OnlineStatus();
this._serviceWorkerHandler = null;
if (paths.serviceWorker && "serviceWorker" in navigator) {
this._serviceWorkerHandler = new ServiceWorkerHandler();
this._serviceWorkerHandler.registerAndStart(paths.serviceWorker);
}
this.crypto = new Crypto(cryptoExtras);
this.storageFactory = new StorageFactory(this._serviceWorkerHandler);
this.sessionInfoStorage = new SessionInfoStorage("hydrogen_sessions_v1");
this.estimateStorageUsage = estimateStorageUsage;
this.random = Math.random;
if (typeof fetch === "function") {
this.request = createFetchRequest(this.clock.createTimeout);
} else {
this.request = xhrRequest;
}
}
get updateService() {
return this._serviceWorkerHandler;
}
loadOlm() {
return loadOlm(this._paths.olm);
}
async loadOlmWorker() {
if (!window.WebAssembly) {
return await loadOlmWorker(this._paths);
}
}
createAndMountRootView(vm) {
const isIE11 = !!window.MSInputMethodContext && !!document.documentMode;
if (isIE11) {
this._container.className += " legacy";
}
window.__hydrogenViewModel = vm;
const view = new RootView(vm);
this._container.appendChild(view.mount());
}
setNavigation(navigation) {
this._serviceWorkerHandler?.setNavigation(navigation);
}
}

View file

@ -26,7 +26,7 @@ function subtleCryptoResult(promiseOrOp, method) {
} }
} }
class CryptoHMACDriver { class HMACCrypto {
constructor(subtleCrypto) { constructor(subtleCrypto) {
this._subtleCrypto = subtleCrypto; this._subtleCrypto = subtleCrypto;
} }
@ -80,10 +80,10 @@ class CryptoHMACDriver {
} }
} }
class CryptoDeriveDriver { class DeriveCrypto {
constructor(subtleCrypto, cryptoDriver, cryptoExtras) { constructor(subtleCrypto, crypto, cryptoExtras) {
this._subtleCrypto = subtleCrypto; this._subtleCrypto = subtleCrypto;
this._cryptoDriver = cryptoDriver; this._crypto = crypto;
this._cryptoExtras = cryptoExtras; this._cryptoExtras = cryptoExtras;
} }
/** /**
@ -130,7 +130,7 @@ class CryptoDeriveDriver {
*/ */
async hkdf(key, salt, info, hash, length) { async hkdf(key, salt, info, hash, length) {
if (!this._subtleCrypto.deriveBits) { if (!this._subtleCrypto.deriveBits) {
return this._cryptoExtras.hkdf(this._cryptoDriver, key, salt, info, hash, length); return this._cryptoExtras.hkdf(this._crypto, key, salt, info, hash, length);
} }
const hkdfkey = await subtleCryptoResult(this._subtleCrypto.importKey( const hkdfkey = await subtleCryptoResult(this._subtleCrypto.importKey(
'raw', 'raw',
@ -152,7 +152,7 @@ class CryptoDeriveDriver {
} }
} }
class CryptoAESDriver { class AESCrypto {
constructor(subtleCrypto) { constructor(subtleCrypto) {
this._subtleCrypto = subtleCrypto; this._subtleCrypto = subtleCrypto;
} }
@ -200,7 +200,7 @@ class CryptoAESDriver {
} }
class CryptoLegacyAESDriver { class AESLegacyCrypto {
constructor(aesjs) { constructor(aesjs) {
this._aesjs = aesjs; this._aesjs = aesjs;
} }
@ -237,20 +237,20 @@ function hashName(name) {
return name; return name;
} }
export class CryptoDriver { export class Crypto {
constructor(cryptoExtras) { constructor(cryptoExtras) {
const crypto = window.crypto || window.msCrypto; const crypto = window.crypto || window.msCrypto;
const subtleCrypto = crypto.subtle || crypto.webkitSubtle; const subtleCrypto = crypto.subtle || crypto.webkitSubtle;
this._subtleCrypto = subtleCrypto; this._subtleCrypto = subtleCrypto;
// not exactly guaranteeing AES-CTR support // not exactly guaranteeing AES-CTR support
// but in practice IE11 doesn't have this // but in practice IE11 doesn't have this
if (!subtleCrypto.deriveBits && cryptoExtras.aesjs) { if (!subtleCrypto.deriveBits && cryptoExtras?.aesjs) {
this.aes = new CryptoLegacyAESDriver(cryptoExtras.aesjs); this.aes = new AESLegacyCrypto(cryptoExtras.aesjs);
} else { } else {
this.aes = new CryptoAESDriver(subtleCrypto); this.aes = new AESCrypto(subtleCrypto);
} }
this.hmac = new CryptoHMACDriver(subtleCrypto); this.hmac = new HMACCrypto(subtleCrypto);
this.derive = new CryptoDeriveDriver(subtleCrypto, this, cryptoExtras); this.derive = new DeriveCrypto(subtleCrypto, this, cryptoExtras);
} }
/** /**

View file

@ -19,15 +19,19 @@ limitations under the License.
// - UpdateService (see checkForUpdate method, and should also emit events rather than showing confirm dialog here) // - UpdateService (see checkForUpdate method, and should also emit events rather than showing confirm dialog here)
// - ConcurrentAccessBlocker (see preventConcurrentSessionAccess method) // - ConcurrentAccessBlocker (see preventConcurrentSessionAccess method)
export class ServiceWorkerHandler { export class ServiceWorkerHandler {
constructor({navigation}) { constructor() {
this._waitingForReply = new Map(); this._waitingForReply = new Map();
this._messageIdCounter = 0; this._messageIdCounter = 0;
this._navigation = null;
this._registration = null; this._registration = null;
this._navigation = navigation;
this._registrationPromise = null; this._registrationPromise = null;
this._currentController = null; this._currentController = null;
} }
setNavigation(navigation) {
this._navigation = navigation;
}
registerAndStart(path) { registerAndStart(path) {
this._registrationPromise = (async () => { this._registrationPromise = (async () => {
navigator.serviceWorker.addEventListener("message", this); navigator.serviceWorker.addEventListener("message", this);
@ -61,7 +65,7 @@ export class ServiceWorkerHandler {
} }
_closeSessionIfNeeded(sessionId) { _closeSessionIfNeeded(sessionId) {
const currentSession = this._navigation.path.get("session"); const currentSession = this._navigation?.path.get("session");
if (sessionId && currentSession?.value === sessionId) { if (sessionId && currentSession?.value === sessionId) {
return new Promise(resolve => { return new Promise(resolve => {
const unsubscribe = this._navigation.pathObservable.subscribe(path => { const unsubscribe = this._navigation.pathObservable.subscribe(path => {

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import {AbortError} from "./error.js"; import {AbortError} from "../../../utils/error.js";
class WorkerState { class WorkerState {
constructor(worker) { constructor(worker) {

View file

@ -0,0 +1,38 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
Copyright 2020 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 function addCacheBuster(urlStr, random = Math.random) {
// XHR doesn't have a good way to disable cache,
// so add a random query param
// see https://davidtranscend.com/blog/prevent-ie11-cache-ajax-requests/
if (urlStr.includes("?")) {
urlStr = urlStr + "&";
} else {
urlStr = urlStr + "?";
}
return urlStr + `_cacheBuster=${Math.ceil(random() * Number.MAX_SAFE_INTEGER)}`;
}
export function tests() {
return {
"add cache buster": assert => {
const random = () => 0.5;
assert.equal(addCacheBuster("http://foo", random), "http://foo?_cacheBuster=4503599627370496");
assert.equal(addCacheBuster("http://foo?bar=baz", random), "http://foo?bar=baz&_cacheBuster=4503599627370496");
}
}
}

View file

@ -18,9 +18,9 @@ limitations under the License.
import { import {
AbortError, AbortError,
ConnectionError ConnectionError
} from "../../error.js"; } from "../../../../matrix/error.js";
import {abortOnTimeout} from "../timeout.js"; import {abortOnTimeout} from "./timeout.js";
import {addCacheBuster} from "../common.js"; import {addCacheBuster} from "./common.js";
class RequestResult { class RequestResult {
constructor(promise, controller) { constructor(promise, controller) {

View file

@ -18,7 +18,7 @@ limitations under the License.
import { import {
AbortError, AbortError,
ConnectionError ConnectionError
} from "../error.js"; } from "../../../../matrix/error.js";
export function abortOnTimeout(createTimeout, timeoutAmount, requestResult, responsePromise) { export function abortOnTimeout(createTimeout, timeoutAmount, requestResult, responsePromise) {

View file

@ -17,8 +17,8 @@ limitations under the License.
import { import {
AbortError, AbortError,
ConnectionError ConnectionError
} from "../../error.js"; } from "../../../../matrix/error.js";
import {addCacheBuster} from "../common.js"; import {addCacheBuster} from "./common.js";
class RequestResult { class RequestResult {
constructor(promise, xhr) { constructor(promise, xhr) {

View file

@ -15,8 +15,8 @@ limitations under the License.
*/ */
// polyfills needed for IE11 // polyfills needed for IE11
import Promise from "../lib/es6-promise/index.js"; import Promise from "../../../lib/es6-promise/index.js";
import {checkNeedsSyncPromise} from "./matrix/storage/idb/utils.js"; import {checkNeedsSyncPromise} from "../../matrix/storage/idb/utils.js";
if (typeof window.Promise === "undefined") { if (typeof window.Promise === "undefined") {
window.Promise = Promise; window.Promise = Promise;

View file

@ -14,8 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
const container = document.querySelector(".hydrogen");
export function spinner(t, extraClasses = undefined) { export function spinner(t, extraClasses = undefined) {
if (document.body.classList.contains("ie11")) { if (container.classList.contains("legacy")) {
return t.div({className: "spinner"}, [ return t.div({className: "spinner"}, [
t.div(), t.div(),
t.div(), t.div(),

View file

@ -32,7 +32,7 @@ limitations under the License.
} }
} }
.not-ie11 .spinner circle { .hydrogen:not(.legacy) .spinner circle {
transform-origin: 50% 50%; transform-origin: 50% 50%;
animation-name: spinner; animation-name: spinner;
animation-duration: 2s; animation-duration: 2s;
@ -45,35 +45,35 @@ limitations under the License.
stroke-linecap: butt; stroke-linecap: butt;
} }
.ie11 .spinner { .hydrogen.legacy .spinner {
display: inline-block; display: inline-block;
position: relative; position: relative;
} }
.ie11 .spinner div { .hydrogen.legacy .spinner div {
box-sizing: border-box; box-sizing: border-box;
display: block; display: block;
position: absolute; position: absolute;
padding: 2px; padding: 2px;
border: 2px solid currentcolor; border: 2px solid currentcolor;
border-radius: 50%; border-radius: 50%;
animation: ie-spinner 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite; animation: legacy-spinner 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
border-color: currentcolor transparent transparent transparent; border-color: currentcolor transparent transparent transparent;
width: var(--size); width: var(--size);
height: var(--size); height: var(--size);
} }
.ie11 .spinner div:nth-child(1) { .hydrogen.legacy .spinner div:nth-child(1) {
animation-delay: -0.45s; animation-delay: -0.45s;
} }
.ie11 .spinner div:nth-child(2) { .hydrogen.legacy .spinner div:nth-child(2) {
animation-delay: -0.3s; animation-delay: -0.3s;
} }
.ie11 .spinner div:nth-child(3) { .hydrogen.legacy .spinner div:nth-child(3) {
animation-delay: -0.15s; animation-delay: -0.15s;
} }
@keyframes ie-spinner { @keyframes legacy-spinner {
0% { 0% {
transform: rotate(0deg); transform: rotate(0deg);
} }

View file

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

Before

Width:  |  Height:  |  Size: 657 B

After

Width:  |  Height:  |  Size: 657 B

View file

Before

Width:  |  Height:  |  Size: 212 B

After

Width:  |  Height:  |  Size: 212 B

View file

Before

Width:  |  Height:  |  Size: 307 B

After

Width:  |  Height:  |  Size: 307 B

View file

Before

Width:  |  Height:  |  Size: 621 B

After

Width:  |  Height:  |  Size: 621 B

View file

Before

Width:  |  Height:  |  Size: 307 B

After

Width:  |  Height:  |  Size: 307 B

View file

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

Before

Width:  |  Height:  |  Size: 785 B

After

Width:  |  Height:  |  Size: 785 B

View file

Before

Width:  |  Height:  |  Size: 621 B

After

Width:  |  Height:  |  Size: 621 B

View file

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Some files were not shown because too many files have changed in this diff Show more