-
-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
2 changed files
with
148 additions
and
40 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,24 +1,40 @@ | ||
/* | ||
Copyright 2024 New Vector Ltd | ||
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. | ||
*/ | ||
|
||
Check failure Code scanning / SonarCloud Origins should be verified during cross-origin communications High
Verify the origin of the received message. See more on SonarCloud
|
||
import { idbLoad } from "matrix-react-sdk/src/utils/StorageAccess"; | ||
import { ACCESS_TOKEN_IV, tryDecryptToken } from "matrix-react-sdk/src/utils/tokens/tokens"; | ||
import { encodeUnpaddedBase64 } from "matrix-js-sdk/src/base64"; | ||
|
||
const serverSupportMap: { | ||
[serverUrl: string]: { | ||
supportsMSC3916: boolean; | ||
cacheExpires: number; | ||
}; | ||
} = {}; | ||
|
||
const credentialStore: { | ||
[serverUrl: string]: string; | ||
} = {}; | ||
|
||
// We skipWaiting() to update the service worker more frequently, particularly in development environments. | ||
// @ts-expect-error - service worker types are not available. See 'fetch' event handler. | ||
skipWaiting(); | ||
self.addEventListener("install", (event) => { | ||
// We skipWaiting() to update the service worker more frequently, particularly in development environments. | ||
// @ts-expect-error - service worker types are not available. See 'fetch' event handler. | ||
event.waitUntil(skipWaiting()); | ||
}); | ||
|
||
self.addEventListener("message", (event) => { | ||
if (event.data?.type !== "credentials") return; // ignore | ||
credentialStore[event.data.homeserverUrl] = event.data.accessToken; | ||
console.log( | ||
`[Service Worker] Updated access token for ${event.data.homeserverUrl} (accessToken? ${Boolean(event.data.accessToken)})`, | ||
); | ||
self.addEventListener("activate", (event) => { | ||
// We force all clients to be under our control, immediately. This could be old tabs. | ||
// @ts-expect-error - service worker types are not available. See 'fetch' event handler. | ||
event.waitUntil(clients.claim()); | ||
}); | ||
|
||
// @ts-expect-error - getting types to work for this is difficult, so we anticipate that "addEventListener" doesn't | ||
|
@@ -42,34 +58,40 @@ self.addEventListener("fetch", (event: FetchEvent) => { | |
// later on we need to proxy the request through if it turns out the server doesn't support authentication. | ||
event.respondWith( | ||
(async (): Promise<Response> => { | ||
// Figure out which homeserver we're communicating with | ||
const csApi = url.substring(0, url.indexOf("/_matrix/media/v3")); | ||
|
||
// Locate our access token, and populate the fetchConfig with the authentication header. | ||
const accessToken = credentialStore[csApi]; | ||
let fetchConfig: { headers?: { [key: string]: string } } = {}; | ||
if (accessToken) { | ||
fetchConfig = { | ||
headers: { | ||
Authorization: `Bearer ${accessToken}`, | ||
}, | ||
}; | ||
} | ||
try { | ||
// Figure out which homeserver we're communicating with | ||
const csApi = url.substring(0, url.indexOf("/_matrix/media/v3")); | ||
|
||
// Update or populate the server support map using a (usually) authenticated `/versions` call. | ||
if (!serverSupportMap[csApi] || serverSupportMap[csApi].cacheExpires <= new Date().getTime()) { | ||
const versions = await (await fetch(`${csApi}/_matrix/client/versions`, fetchConfig)).json(); | ||
serverSupportMap[csApi] = { | ||
supportsMSC3916: Boolean(versions?.unstable_features?.["org.matrix.msc3916"]), | ||
cacheExpires: new Date().getTime() + 2 * 60 * 60 * 1000, // 2 hours from now | ||
}; | ||
} | ||
// Locate our access token, and populate the fetchConfig with the authentication header. | ||
// @ts-expect-error - service worker types are not available. See 'fetch' event handler. | ||
const client = await self.clients.get(event.clientId); | ||
const accessToken = await getAccessToken(client); | ||
if (accessToken) { | ||
fetchConfig = { | ||
headers: { | ||
Authorization: `Bearer ${accessToken}`, | ||
}, | ||
}; | ||
} | ||
|
||
// Update or populate the server support map using a (usually) authenticated `/versions` call. | ||
if (!serverSupportMap[csApi] || serverSupportMap[csApi].cacheExpires <= new Date().getTime()) { | ||
const versions = await (await fetch(`${csApi}/_matrix/client/versions`, fetchConfig)).json(); | ||
serverSupportMap[csApi] = { | ||
supportsMSC3916: Boolean(versions?.unstable_features?.["org.matrix.msc3916"]), | ||
cacheExpires: new Date().getTime() + 2 * 60 * 60 * 1000, // 2 hours from now | ||
}; | ||
} | ||
|
||
// If we have server support (and a means of authentication), rewrite the URL to use MSC3916 endpoints. | ||
if (serverSupportMap[csApi].supportsMSC3916 && accessToken) { | ||
// Currently unstable only. | ||
url = url.replace(/\/media\/v3\/(.*)\//, "/client/unstable/org.matrix.msc3916/media/$1/"); | ||
} // else by default we make no changes | ||
// If we have server support (and a means of authentication), rewrite the URL to use MSC3916 endpoints. | ||
if (serverSupportMap[csApi].supportsMSC3916 && accessToken) { | ||
// Currently unstable only. | ||
url = url.replace(/\/media\/v3\/(.*)\//, "/client/unstable/org.matrix.msc3916/media/$1/"); | ||
} // else by default we make no changes | ||
} catch (err) { | ||
console.error("SW: Error in request rewrite.", err); | ||
} | ||
|
||
// Add authentication and send the request. We add authentication even if MSC3916 endpoints aren't | ||
// being used to ensure patches like this work: | ||
|
@@ -79,3 +101,69 @@ self.addEventListener("fetch", (event: FetchEvent) => { | |
); | ||
} | ||
}); | ||
|
||
// Ideally we'd use the `Client` interface for `client`, but since it's not available (see 'fetch' listener), we use | ||
// unknown for now and force-cast it to something close enough later. | ||
async function getAccessToken(client: unknown): Promise<string | undefined> { | ||
// Access tokens are encrypted at rest, so while we can grab the "access token", we'll need to do work to get the | ||
// real thing. | ||
const encryptedAccessToken = await idbLoad("account", "mx_access_token"); | ||
|
||
// We need to extract a user ID and device ID from localstorage, which means calling WebPlatform for the | ||
// read operation. Service workers can't access localstorage. | ||
const { userId, deviceId } = await new Promise<{ userId: string; deviceId: string }>((resolve, reject) => { | ||
// Avoid stalling the tab in case something goes wrong. | ||
const timeoutId = setTimeout(() => reject(new Error("timeout in postMessage")), 1000); | ||
|
||
// We don't need particularly good randomness here - we just use this to generate a request ID, so we know | ||
// which postMessage reply is for our active request. | ||
const responseKey = Math.random().toString(36); | ||
|
||
// Add the listener first, just in case the tab is *really* fast. | ||
const listener = (event: MessageEvent): void => { | ||
if (event.data?.responseKey !== responseKey) return; // not for us | ||
clearTimeout(timeoutId); // do this as soon as possible, avoiding a race between resolve and reject. | ||
resolve(event.data); // "unblock" the remainder of the thread, if that were such a thing in JavaScript. | ||
self.removeEventListener("message", listener); // cleanup, since we're not going to do anything else. | ||
}; | ||
self.addEventListener("message", listener); | ||
|
||
// Ask the tab for the information we need. This is handled by WebPlatform. | ||
(client as Window).postMessage({ responseKey, type: "userinfo" }); | ||
}); | ||
|
||
// ... and this is why we need the user ID and device ID: they're index keys for the pickle key table. | ||
const pickleKeyData = await idbLoad("pickleKey", [userId, deviceId]); | ||
if (pickleKeyData && (!pickleKeyData.encrypted || !pickleKeyData.iv || !pickleKeyData.cryptoKey)) { | ||
console.error("SW: Invalid pickle key loaded - ignoring"); | ||
return undefined; | ||
} | ||
|
||
let pickleKey: string | undefined; | ||
|
||
// Extract a useful pickle key out of our queries. | ||
if (pickleKeyData) { | ||
// We also need to generate the additional data needed for the key | ||
const additionalData = new Uint8Array(userId.length + deviceId.length + 1); | ||
for (let i = 0; i < userId.length; i++) { | ||
additionalData[i] = userId.charCodeAt(i); | ||
} | ||
additionalData[userId.length] = 124; // "|" | ||
for (let i = 0; i < deviceId.length; i++) { | ||
additionalData[userId.length + 1 + i] = deviceId.charCodeAt(i); | ||
} | ||
|
||
// Convert pickle key to a base64 key we can use | ||
const pickleKeyBuf = await crypto.subtle.decrypt( | ||
{ name: "AES-GCM", iv: pickleKeyData.iv, additionalData }, | ||
pickleKeyData.cryptoKey, | ||
pickleKeyData.encrypted, | ||
); | ||
if (pickleKeyBuf) { | ||
pickleKey = encodeUnpaddedBase64(pickleKeyBuf); | ||
} | ||
} | ||
|
||
// Finally, try decrypting the thing and return that. This may fail, but that's okay. | ||
return tryDecryptToken(pickleKey, encryptedAccessToken, ACCESS_TOKEN_IV); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters