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:
parent
374cee9080
commit
d25d529e86
12 changed files with 435 additions and 176 deletions
88
src/utils/tokens/pickling.ts
Normal file
88
src/utils/tokens/pickling.ts
Normal 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;
|
||||
}
|
|
@ -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);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue