Extract functions for service worker usage, and add initial MSC3916 playwright test (when supported) (#12414)

* Send user credentials to service worker for MSC3916 authentication

* appease linter

* Add initial test

The test fails, seemingly because the service worker isn't being installed or because the network mock can't reach that far.

* Remove unsafe access token code

* Split out base IDB operations to avoid importing `document` in serviceworkers

* Use safe crypto access for service workers

* Fix tests/unsafe access

* Remove backwards compatibility layer & appease linter

* Add docs

* Fix tests

* Appease the linter

* Iterate tests

* Factor out pickle key handling for service workers

* Enable everything we can about service workers

* Appease the linter

* Add docs

* Rename win32 image to linux in hopes of it just working

* Use actual image

* Apply suggestions from code review

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Improve documentation

* Document `??` not working

* Try to appease the tests

* Add some notes

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
This commit is contained in:
Travis Ralston 2024-05-02 16:19:55 -06:00 committed by GitHub
parent 374cee9080
commit d25d529e86
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 435 additions and 176 deletions

View file

@ -0,0 +1,88 @@
/*
Copyright 2016 Aviral Dasgupta
Copyright 2016 OpenMarket Ltd
Copyright 2018 New Vector Ltd
Copyright 2020, 2024 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 { encodeUnpaddedBase64 } from "matrix-js-sdk/src/matrix";
import { logger } from "matrix-js-sdk/src/logger";
/**
* Calculates the `additionalData` for the AES-GCM key used by the pickling processes. This
* additional data is *not* encrypted, but *is* authenticated. The additional data is constructed
* from the user ID and device ID provided.
*
* The later-constructed pickle key is used to decrypt values, such as access tokens, from IndexedDB.
*
* See https://developer.mozilla.org/en-US/docs/Web/API/AesGcmParams for more information on
* `additionalData`.
*
* @param {string} userId The user ID who owns the pickle key.
* @param {string} deviceId The device ID which owns the pickle key.
* @return {Uint8Array} The additional data as a Uint8Array.
*/
export function getPickleAdditionalData(userId: string, deviceId: string): Uint8Array {
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);
}
return additionalData;
}
/**
* Decrypts the provided data into a pickle key and base64-encodes it ready for use elsewhere.
*
* If `data` is undefined in part or in full, returns undefined.
*
* If crypto functions are not available, returns undefined regardless of input.
*
* @param data An object containing the encrypted pickle key data: encrypted payload, initialization vector (IV), and crypto key. Typically loaded from indexedDB.
* @param userId The user ID the pickle key belongs to.
* @param deviceId The device ID the pickle key belongs to.
* @returns A promise that resolves to the encoded pickle key, or undefined if the key cannot be built and encoded.
*/
export async function buildAndEncodePickleKey(
data: { encrypted?: BufferSource; iv?: BufferSource; cryptoKey?: CryptoKey } | undefined,
userId: string,
deviceId: string,
): Promise<string | undefined> {
if (!crypto?.subtle) {
return undefined;
}
if (!data || !data.encrypted || !data.iv || !data.cryptoKey) {
return undefined;
}
try {
const additionalData = getPickleAdditionalData(userId, deviceId);
const pickleKeyBuf = await crypto.subtle.decrypt(
{ name: "AES-GCM", iv: data.iv, additionalData },
data.cryptoKey,
data.encrypted,
);
if (pickleKeyBuf) {
return encodeUnpaddedBase64(pickleKeyBuf);
}
} catch (e) {
logger.error("Error decrypting pickle key");
}
return undefined;
}

View file

@ -17,7 +17,7 @@ limitations under the License.
import { decryptAES, encryptAES, IEncryptedPayload } from "matrix-js-sdk/src/crypto/aes";
import { logger } from "matrix-js-sdk/src/logger";
import * as StorageManager from "../StorageManager";
import * as StorageAccess from "../StorageAccess";
/**
* Utility functions related to the storage and retrieval of access tokens
@ -50,10 +50,10 @@ async function pickleKeyToAesKey(pickleKey: string): Promise<Uint8Array> {
for (let i = 0; i < pickleKey.length; i++) {
pickleKeyBuffer[i] = pickleKey.charCodeAt(i);
}
const hkdfKey = await window.crypto.subtle.importKey("raw", pickleKeyBuffer, "HKDF", false, ["deriveBits"]);
const hkdfKey = await crypto.subtle.importKey("raw", pickleKeyBuffer, "HKDF", false, ["deriveBits"]);
pickleKeyBuffer.fill(0);
return new Uint8Array(
await window.crypto.subtle.deriveBits(
await crypto.subtle.deriveBits(
{
name: "HKDF",
hash: "SHA-256",
@ -142,7 +142,7 @@ export async function persistTokenInStorage(
// Save either the encrypted access token, or the plain access
// token if there is no token or we were unable to encrypt (e.g. if the browser doesn't
// have WebCrypto).
await StorageManager.idbSave("account", storageKey, encryptedToken || token);
await StorageAccess.idbSave("account", storageKey, encryptedToken || token);
} catch (e) {
// if we couldn't save to indexedDB, fall back to localStorage. We
// store the access token unencrypted since localStorage only saves
@ -155,7 +155,7 @@ export async function persistTokenInStorage(
}
} else {
try {
await StorageManager.idbSave("account", storageKey, token);
await StorageAccess.idbSave("account", storageKey, token);
} catch (e) {
if (!!token) {
localStorage.setItem(storageKey, token);