You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-11-25 05:23:13 +03:00
Move out crypto/aes (#4431)
* Move `SecretEncryptedPayload` in `src/utils/@types` * Move `encryptAES` to a dedicated file. Moved in a utils folder. * Move `deriveKeys` to a dedicated file in order to share it * Move `decryptAES` to a dedicated file. Moved in a utils folder. * Move `calculateKeyCheck` to a dedicated file. Moved in a utils folder. * Remove AES functions in `aes.ts` and export new ones for backward compatibility * Update import to use new functions * Add `src/utils` entrypoint in `README.md` * - Rename `SecretEncryptedPayload` to `AESEncryptedSecretStoragePayload`. - Move into `src/@types` * Move `calculateKeyCheck` into `secret-storage.ts`. * Move `deriveKeys` into `src/utils/internal` folder. * - Rename `encryptAES` on `encryptAESSecretStorageItem` - Change named export by default export * - Rename `decryptAES` on `decryptAESSecretStorageItem` - Change named export by default export * Update documentation * Update `decryptAESSecretStorageItem` doc * Add lnk to spec for `calculateKeyCheck` * Fix downstream tests
This commit is contained in:
@@ -191,6 +191,7 @@ As well as the primary entry point (`matrix-js-sdk`), there are several other en
|
|||||||
| `matrix-js-sdk/lib/crypto-api` | Cryptography functionality. |
|
| `matrix-js-sdk/lib/crypto-api` | Cryptography functionality. |
|
||||||
| `matrix-js-sdk/lib/types` | Low-level types, reflecting data structures defined in the Matrix spec. |
|
| `matrix-js-sdk/lib/types` | Low-level types, reflecting data structures defined in the Matrix spec. |
|
||||||
| `matrix-js-sdk/lib/testing` | Test utilities, which may be useful in test code but should not be used in production code. |
|
| `matrix-js-sdk/lib/testing` | Test utilities, which may be useful in test code but should not be used in production code. |
|
||||||
|
| `matrix-js-sdk/lib/utils/*.js` | A set of modules exporting standalone functions (and their types). |
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import { IDBFactory } from "fake-indexeddb";
|
|||||||
import { CRYPTO_BACKENDS, InitCrypto, syncPromise } from "../../test-utils/test-utils";
|
import { CRYPTO_BACKENDS, InitCrypto, syncPromise } from "../../test-utils/test-utils";
|
||||||
import { AuthDict, createClient, CryptoEvent, MatrixClient } from "../../../src";
|
import { AuthDict, createClient, CryptoEvent, MatrixClient } from "../../../src";
|
||||||
import { mockInitialApiRequests, mockSetupCrossSigningRequests } from "../../test-utils/mockEndpoints";
|
import { mockInitialApiRequests, mockSetupCrossSigningRequests } from "../../test-utils/mockEndpoints";
|
||||||
import { encryptAES } from "../../../src/crypto/aes";
|
import encryptAESSecretStorageItem from "../../../src/utils/encryptAESSecretStorageItem.ts";
|
||||||
import { CryptoCallbacks, CrossSigningKey } from "../../../src/crypto-api";
|
import { CryptoCallbacks, CrossSigningKey } from "../../../src/crypto-api";
|
||||||
import { SECRET_STORAGE_ALGORITHM_V1_AES } from "../../../src/secret-storage";
|
import { SECRET_STORAGE_ALGORITHM_V1_AES } from "../../../src/secret-storage";
|
||||||
import { ISyncResponder, SyncResponder } from "../../test-utils/SyncResponder";
|
import { ISyncResponder, SyncResponder } from "../../test-utils/SyncResponder";
|
||||||
@@ -169,17 +169,17 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("cross-signing (%s)", (backend: s
|
|||||||
mockInitialApiRequests(aliceClient.getHomeserverUrl());
|
mockInitialApiRequests(aliceClient.getHomeserverUrl());
|
||||||
|
|
||||||
// Encrypt the private keys and return them in the /sync response as if they are in Secret Storage
|
// Encrypt the private keys and return them in the /sync response as if they are in Secret Storage
|
||||||
const masterKey = await encryptAES(
|
const masterKey = await encryptAESSecretStorageItem(
|
||||||
MASTER_CROSS_SIGNING_PRIVATE_KEY_BASE64,
|
MASTER_CROSS_SIGNING_PRIVATE_KEY_BASE64,
|
||||||
encryptionKey,
|
encryptionKey,
|
||||||
"m.cross_signing.master",
|
"m.cross_signing.master",
|
||||||
);
|
);
|
||||||
const selfSigningKey = await encryptAES(
|
const selfSigningKey = await encryptAESSecretStorageItem(
|
||||||
SELF_CROSS_SIGNING_PRIVATE_KEY_BASE64,
|
SELF_CROSS_SIGNING_PRIVATE_KEY_BASE64,
|
||||||
encryptionKey,
|
encryptionKey,
|
||||||
"m.cross_signing.self_signing",
|
"m.cross_signing.self_signing",
|
||||||
);
|
);
|
||||||
const userSigningKey = await encryptAES(
|
const userSigningKey = await encryptAESSecretStorageItem(
|
||||||
USER_CROSS_SIGNING_PRIVATE_KEY_BASE64,
|
USER_CROSS_SIGNING_PRIVATE_KEY_BASE64,
|
||||||
encryptionKey,
|
encryptionKey,
|
||||||
"m.cross_signing.user_signing",
|
"m.cross_signing.user_signing",
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import { IObject } from "../../../src/crypto/olmlib";
|
|||||||
import { MatrixEvent } from "../../../src/models/event";
|
import { MatrixEvent } from "../../../src/models/event";
|
||||||
import { TestClient } from "../../TestClient";
|
import { TestClient } from "../../TestClient";
|
||||||
import { makeTestClients } from "./verification/util";
|
import { makeTestClients } from "./verification/util";
|
||||||
import { encryptAES } from "../../../src/crypto/aes";
|
import encryptAESSecretStorageItem from "../../../src/utils/encryptAESSecretStorageItem.ts";
|
||||||
import { createSecretStorageKey, resetCrossSigningKeys } from "./crypto-utils";
|
import { createSecretStorageKey, resetCrossSigningKeys } from "./crypto-utils";
|
||||||
import { logger } from "../../../src/logger";
|
import { logger } from "../../../src/logger";
|
||||||
import { ClientEvent, ICreateClientOpts, MatrixClient } from "../../../src/client";
|
import { ClientEvent, ICreateClientOpts, MatrixClient } from "../../../src/client";
|
||||||
@@ -612,7 +612,7 @@ describe("Secrets", function () {
|
|||||||
type: "m.megolm_backup.v1",
|
type: "m.megolm_backup.v1",
|
||||||
content: {
|
content: {
|
||||||
encrypted: {
|
encrypted: {
|
||||||
key_id: await encryptAES(
|
key_id: await encryptAESSecretStorageItem(
|
||||||
"123,45,6,7,89,1,234,56,78,90,12,34,5,67,8,90",
|
"123,45,6,7,89,1,234,56,78,90,12,34,5,67,8,90",
|
||||||
secretStorageKeys.key_id,
|
secretStorageKeys.key_id,
|
||||||
"m.megolm_backup.v1",
|
"m.megolm_backup.v1",
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ import { logger } from "../../../src/logger";
|
|||||||
import { OutgoingRequestsManager } from "../../../src/rust-crypto/OutgoingRequestsManager";
|
import { OutgoingRequestsManager } from "../../../src/rust-crypto/OutgoingRequestsManager";
|
||||||
import { ClientEvent, ClientEventHandlerMap } from "../../../src/client";
|
import { ClientEvent, ClientEventHandlerMap } from "../../../src/client";
|
||||||
import { Curve25519AuthData } from "../../../src/crypto-api/keybackup";
|
import { Curve25519AuthData } from "../../../src/crypto-api/keybackup";
|
||||||
import { encryptAES } from "../../../src/crypto/aes";
|
import encryptAESSecretStorageItem from "../../../src/utils/encryptAESSecretStorageItem.ts";
|
||||||
import { CryptoStore, SecretStorePrivateKeys } from "../../../src/crypto/store/base";
|
import { CryptoStore, SecretStorePrivateKeys } from "../../../src/crypto/store/base";
|
||||||
|
|
||||||
const TEST_USER = "@alice:example.com";
|
const TEST_USER = "@alice:example.com";
|
||||||
@@ -425,7 +425,7 @@ describe("initRustCrypto", () => {
|
|||||||
}, 10000);
|
}, 10000);
|
||||||
|
|
||||||
async function encryptAndStoreSecretKey(type: string, key: Uint8Array, pickleKey: string, store: CryptoStore) {
|
async function encryptAndStoreSecretKey(type: string, key: Uint8Array, pickleKey: string, store: CryptoStore) {
|
||||||
const encryptedKey = await encryptAES(encodeBase64(key), Buffer.from(pickleKey), type);
|
const encryptedKey = await encryptAESSecretStorageItem(encodeBase64(key), Buffer.from(pickleKey), type);
|
||||||
store.storeSecretStorePrivateKey(undefined, type as keyof SecretStorePrivateKeys, encryptedKey);
|
store.storeSecretStorePrivateKey(undefined, type as keyof SecretStorePrivateKeys, encryptedKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ import {
|
|||||||
ServerSideSecretStorageImpl,
|
ServerSideSecretStorageImpl,
|
||||||
trimTrailingEquals,
|
trimTrailingEquals,
|
||||||
} from "../../src/secret-storage";
|
} from "../../src/secret-storage";
|
||||||
import { calculateKeyCheck } from "../../src/crypto/aes";
|
|
||||||
import { randomString } from "../../src/randomstring";
|
import { randomString } from "../../src/randomstring";
|
||||||
|
import { calculateKeyCheck } from "../../src/calculateKeyCheck.ts";
|
||||||
|
|
||||||
describe("ServerSideSecretStorageImpl", function () {
|
describe("ServerSideSecretStorageImpl", function () {
|
||||||
describe(".addKey", function () {
|
describe(".addKey", function () {
|
||||||
|
|||||||
29
src/@types/AESEncryptedSecretStoragePayload.ts
Normal file
29
src/@types/AESEncryptedSecretStoragePayload.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An AES-encrypted secret storage payload.
|
||||||
|
* See https://spec.matrix.org/v1.11/client-server-api/#msecret_storagev1aes-hmac-sha2-1
|
||||||
|
*/
|
||||||
|
export interface AESEncryptedSecretStoragePayload {
|
||||||
|
[key: string]: any; // extensible
|
||||||
|
/** the initialization vector in base64 */
|
||||||
|
iv: string;
|
||||||
|
/** the ciphertext in base64 */
|
||||||
|
ciphertext: string;
|
||||||
|
/** the HMAC in base64 */
|
||||||
|
mac: string;
|
||||||
|
}
|
||||||
34
src/calculateKeyCheck.ts
Normal file
34
src/calculateKeyCheck.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// string of zeroes, for calculating the key check
|
||||||
|
import encryptAESSecretStorageItem from "./utils/encryptAESSecretStorageItem.ts";
|
||||||
|
import { AESEncryptedSecretStoragePayload } from "./@types/AESEncryptedSecretStoragePayload.ts";
|
||||||
|
|
||||||
|
const ZERO_STR = "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the MAC for checking the key.
|
||||||
|
* See https://spec.matrix.org/v1.11/client-server-api/#msecret_storagev1aes-hmac-sha2, steps 3 and 4.
|
||||||
|
*
|
||||||
|
* @param key - the key to use
|
||||||
|
* @param iv - The initialization vector as a base64-encoded string.
|
||||||
|
* If omitted, a random initialization vector will be created.
|
||||||
|
* @returns An object that contains, `mac` and `iv` properties.
|
||||||
|
*/
|
||||||
|
export function calculateKeyCheck(key: Uint8Array, iv?: string): Promise<AESEncryptedSecretStoragePayload> {
|
||||||
|
return encryptAESSecretStorageItem(ZERO_STR, key, "", iv);
|
||||||
|
}
|
||||||
@@ -15,7 +15,7 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { ISigned } from "../@types/signed.ts";
|
import { ISigned } from "../@types/signed.ts";
|
||||||
import { IEncryptedPayload } from "../crypto/aes.ts";
|
import { AESEncryptedSecretStoragePayload } from "../@types/AESEncryptedSecretStoragePayload.ts";
|
||||||
|
|
||||||
export interface Curve25519AuthData {
|
export interface Curve25519AuthData {
|
||||||
public_key: string;
|
public_key: string;
|
||||||
@@ -77,7 +77,7 @@ export interface Curve25519SessionData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* eslint-disable camelcase */
|
/* eslint-disable camelcase */
|
||||||
export interface KeyBackupSession<T = Curve25519SessionData | IEncryptedPayload> {
|
export interface KeyBackupSession<T = Curve25519SessionData | AESEncryptedSecretStoragePayload> {
|
||||||
first_message_index: number;
|
first_message_index: number;
|
||||||
forwarded_count: number;
|
forwarded_count: number;
|
||||||
is_verified: boolean;
|
is_verified: boolean;
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import type { PkSigning } from "@matrix-org/olm";
|
|||||||
import { IObject, pkSign, pkVerify } from "./olmlib.ts";
|
import { IObject, pkSign, pkVerify } from "./olmlib.ts";
|
||||||
import { logger } from "../logger.ts";
|
import { logger } from "../logger.ts";
|
||||||
import { IndexedDBCryptoStore } from "../crypto/store/indexeddb-crypto-store.ts";
|
import { IndexedDBCryptoStore } from "../crypto/store/indexeddb-crypto-store.ts";
|
||||||
import { decryptAES, encryptAES } from "./aes.ts";
|
|
||||||
import { DeviceInfo } from "./deviceinfo.ts";
|
import { DeviceInfo } from "./deviceinfo.ts";
|
||||||
import { ISignedKey, MatrixClient } from "../client.ts";
|
import { ISignedKey, MatrixClient } from "../client.ts";
|
||||||
import { OlmDevice } from "./OlmDevice.ts";
|
import { OlmDevice } from "./OlmDevice.ts";
|
||||||
@@ -36,6 +35,8 @@ import {
|
|||||||
UserVerificationStatus as UserTrustLevel,
|
UserVerificationStatus as UserTrustLevel,
|
||||||
} from "../crypto-api/index.ts";
|
} from "../crypto-api/index.ts";
|
||||||
import { decodeBase64, encodeBase64 } from "../base64.ts";
|
import { decodeBase64, encodeBase64 } from "../base64.ts";
|
||||||
|
import encryptAESSecretStorageItem from "../utils/encryptAESSecretStorageItem.ts";
|
||||||
|
import decryptAESSecretStorageItem from "../utils/decryptAESSecretStorageItem.ts";
|
||||||
|
|
||||||
// backwards-compatibility re-exports
|
// backwards-compatibility re-exports
|
||||||
export { UserTrustLevel };
|
export { UserTrustLevel };
|
||||||
@@ -662,7 +663,7 @@ export function createCryptoStoreCacheCallbacks(store: CryptoStore, olmDevice: O
|
|||||||
|
|
||||||
if (key && key.ciphertext) {
|
if (key && key.ciphertext) {
|
||||||
const pickleKey = Buffer.from(olmDevice.pickleKey);
|
const pickleKey = Buffer.from(olmDevice.pickleKey);
|
||||||
const decrypted = await decryptAES(key, pickleKey, type);
|
const decrypted = await decryptAESSecretStorageItem(key, pickleKey, type);
|
||||||
return decodeBase64(decrypted);
|
return decodeBase64(decrypted);
|
||||||
} else {
|
} else {
|
||||||
return key;
|
return key;
|
||||||
@@ -676,7 +677,7 @@ export function createCryptoStoreCacheCallbacks(store: CryptoStore, olmDevice: O
|
|||||||
throw new Error(`storeCrossSigningKeyCache expects Uint8Array, got ${key}`);
|
throw new Error(`storeCrossSigningKeyCache expects Uint8Array, got ${key}`);
|
||||||
}
|
}
|
||||||
const pickleKey = Buffer.from(olmDevice.pickleKey);
|
const pickleKey = Buffer.from(olmDevice.pickleKey);
|
||||||
const encryptedKey = await encryptAES(encodeBase64(key), pickleKey, type);
|
const encryptedKey = await encryptAESSecretStorageItem(encodeBase64(key), pickleKey, type);
|
||||||
return store.doTxn("readwrite", [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
|
return store.doTxn("readwrite", [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
|
||||||
store.storeSecretStorePrivateKey(txn, type, encryptedKey);
|
store.storeSecretStorePrivateKey(txn, type, encryptedKey);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,153 +14,13 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { decodeBase64, encodeBase64 } from "../base64.ts";
|
import encryptAESSecretStorageItem from "../utils/encryptAESSecretStorageItem.ts";
|
||||||
|
import decryptAESSecretStorageItem from "../utils/decryptAESSecretStorageItem.ts";
|
||||||
|
import { AESEncryptedSecretStoragePayload } from "../@types/AESEncryptedSecretStoragePayload.ts";
|
||||||
|
|
||||||
// salt for HKDF, with 8 bytes of zeros
|
// Export for backwards compatibility
|
||||||
const zeroSalt = new Uint8Array(8);
|
export type { AESEncryptedSecretStoragePayload as IEncryptedPayload };
|
||||||
|
// Export with new names instead of using `as` to not break react-sdk tests
|
||||||
export interface IEncryptedPayload {
|
export const encryptAES = encryptAESSecretStorageItem;
|
||||||
[key: string]: any; // extensible
|
export const decryptAES = decryptAESSecretStorageItem;
|
||||||
/** the initialization vector in base64 */
|
export { calculateKeyCheck } from "../calculateKeyCheck.ts";
|
||||||
iv: string;
|
|
||||||
/** the ciphertext in base64 */
|
|
||||||
ciphertext: string;
|
|
||||||
/** the HMAC in base64 */
|
|
||||||
mac: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Encrypt a string using AES-CTR.
|
|
||||||
*
|
|
||||||
* @param data - the plaintext to encrypt
|
|
||||||
* @param key - the encryption key to use as an input to the HKDF function which is used to derive the AES key for
|
|
||||||
* encryption. Obviously, the same key must be provided when decrypting.
|
|
||||||
* @param name - the name of the secret. Used as an input to the HKDF operation which is used to derive the AES key,
|
|
||||||
* so again the same value must be provided when decrypting.
|
|
||||||
* @param ivStr - the base64-encoded initialization vector to use. If not supplied, a random one will be generated.
|
|
||||||
*
|
|
||||||
* @returns The encrypted result, including the ciphertext itself, the initialization vector (as supplied in `ivStr`,
|
|
||||||
* or generated), and an HMAC on the ciphertext — all base64-encoded.
|
|
||||||
*/
|
|
||||||
export async function encryptAES(
|
|
||||||
data: string,
|
|
||||||
key: Uint8Array,
|
|
||||||
name: string,
|
|
||||||
ivStr?: string,
|
|
||||||
): Promise<IEncryptedPayload> {
|
|
||||||
let iv: Uint8Array;
|
|
||||||
if (ivStr) {
|
|
||||||
iv = decodeBase64(ivStr);
|
|
||||||
} else {
|
|
||||||
iv = new Uint8Array(16);
|
|
||||||
globalThis.crypto.getRandomValues(iv);
|
|
||||||
|
|
||||||
// clear bit 63 of the IV to stop us hitting the 64-bit counter boundary
|
|
||||||
// (which would mean we wouldn't be able to decrypt on Android). The loss
|
|
||||||
// of a single bit of iv is a price we have to pay.
|
|
||||||
iv[8] &= 0x7f;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [aesKey, hmacKey] = await deriveKeys(key, name);
|
|
||||||
const encodedData = new TextEncoder().encode(data);
|
|
||||||
|
|
||||||
const ciphertext = await globalThis.crypto.subtle.encrypt(
|
|
||||||
{
|
|
||||||
name: "AES-CTR",
|
|
||||||
counter: iv,
|
|
||||||
length: 64,
|
|
||||||
},
|
|
||||||
aesKey,
|
|
||||||
encodedData,
|
|
||||||
);
|
|
||||||
|
|
||||||
const hmac = await globalThis.crypto.subtle.sign({ name: "HMAC" }, hmacKey, ciphertext);
|
|
||||||
|
|
||||||
return {
|
|
||||||
iv: encodeBase64(iv),
|
|
||||||
ciphertext: encodeBase64(ciphertext),
|
|
||||||
mac: encodeBase64(hmac),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decrypt an AES-encrypted string.
|
|
||||||
*
|
|
||||||
* @param data - the encrypted data, returned by {@link encryptAES}.
|
|
||||||
* @param key - the encryption key to use as an input to the HKDF function which is used to derive the AES key. Must
|
|
||||||
* be the same as provided to {@link encryptAES}.
|
|
||||||
* @param name - the name of the secret. Also used as an input to the HKDF operation which is used to derive the AES
|
|
||||||
* key, so again must be the same as provided to {@link encryptAES}.
|
|
||||||
*/
|
|
||||||
export async function decryptAES(data: IEncryptedPayload, key: Uint8Array, name: string): Promise<string> {
|
|
||||||
const [aesKey, hmacKey] = await deriveKeys(key, name);
|
|
||||||
|
|
||||||
const ciphertext = decodeBase64(data.ciphertext);
|
|
||||||
|
|
||||||
if (!(await globalThis.crypto.subtle.verify({ name: "HMAC" }, hmacKey, decodeBase64(data.mac), ciphertext))) {
|
|
||||||
throw new Error(`Error decrypting secret ${name}: bad MAC`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const plaintext = await globalThis.crypto.subtle.decrypt(
|
|
||||||
{
|
|
||||||
name: "AES-CTR",
|
|
||||||
counter: decodeBase64(data.iv),
|
|
||||||
length: 64,
|
|
||||||
},
|
|
||||||
aesKey,
|
|
||||||
ciphertext,
|
|
||||||
);
|
|
||||||
|
|
||||||
return new TextDecoder().decode(new Uint8Array(plaintext));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function deriveKeys(key: Uint8Array, name: string): Promise<[CryptoKey, CryptoKey]> {
|
|
||||||
const hkdfkey = await globalThis.crypto.subtle.importKey("raw", key, { name: "HKDF" }, false, ["deriveBits"]);
|
|
||||||
const keybits = await globalThis.crypto.subtle.deriveBits(
|
|
||||||
{
|
|
||||||
name: "HKDF",
|
|
||||||
salt: zeroSalt,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore: https://github.com/microsoft/TypeScript-DOM-lib-generator/pull/879
|
|
||||||
info: new TextEncoder().encode(name),
|
|
||||||
hash: "SHA-256",
|
|
||||||
},
|
|
||||||
hkdfkey,
|
|
||||||
512,
|
|
||||||
);
|
|
||||||
|
|
||||||
const aesKey = keybits.slice(0, 32);
|
|
||||||
const hmacKey = keybits.slice(32);
|
|
||||||
|
|
||||||
const aesProm = globalThis.crypto.subtle.importKey("raw", aesKey, { name: "AES-CTR" }, false, [
|
|
||||||
"encrypt",
|
|
||||||
"decrypt",
|
|
||||||
]);
|
|
||||||
|
|
||||||
const hmacProm = globalThis.crypto.subtle.importKey(
|
|
||||||
"raw",
|
|
||||||
hmacKey,
|
|
||||||
{
|
|
||||||
name: "HMAC",
|
|
||||||
hash: { name: "SHA-256" },
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
["sign", "verify"],
|
|
||||||
);
|
|
||||||
|
|
||||||
return Promise.all([aesProm, hmacProm]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// string of zeroes, for calculating the key check
|
|
||||||
const ZERO_STR = "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0";
|
|
||||||
|
|
||||||
/** Calculate the MAC for checking the key.
|
|
||||||
*
|
|
||||||
* @param key - the key to use
|
|
||||||
* @param iv - The initialization vector as a base64-encoded string.
|
|
||||||
* If omitted, a random initialization vector will be created.
|
|
||||||
* @returns An object that contains, `mac` and `iv` properties.
|
|
||||||
*/
|
|
||||||
export function calculateKeyCheck(key: Uint8Array, iv?: string): Promise<IEncryptedPayload> {
|
|
||||||
return encryptAES(ZERO_STR, key, "", iv);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ import { DeviceTrustLevel } from "./CrossSigning.ts";
|
|||||||
import { keyFromPassphrase } from "./key_passphrase.ts";
|
import { keyFromPassphrase } from "./key_passphrase.ts";
|
||||||
import { encodeUri, safeSet, sleep } from "../utils.ts";
|
import { encodeUri, safeSet, sleep } from "../utils.ts";
|
||||||
import { IndexedDBCryptoStore } from "./store/indexeddb-crypto-store.ts";
|
import { IndexedDBCryptoStore } from "./store/indexeddb-crypto-store.ts";
|
||||||
import { calculateKeyCheck, decryptAES, encryptAES, IEncryptedPayload } from "./aes.ts";
|
|
||||||
import {
|
import {
|
||||||
Curve25519SessionData,
|
Curve25519SessionData,
|
||||||
IAes256AuthData,
|
IAes256AuthData,
|
||||||
@@ -41,6 +40,10 @@ import { ClientPrefix, HTTPError, MatrixError, Method } from "../http-api/index.
|
|||||||
import { BackupTrustInfo } from "../crypto-api/keybackup.ts";
|
import { BackupTrustInfo } from "../crypto-api/keybackup.ts";
|
||||||
import { BackupDecryptor } from "../common-crypto/CryptoBackend.ts";
|
import { BackupDecryptor } from "../common-crypto/CryptoBackend.ts";
|
||||||
import { encodeRecoveryKey } from "../crypto-api/index.ts";
|
import { encodeRecoveryKey } from "../crypto-api/index.ts";
|
||||||
|
import decryptAESSecretStorageItem from "../utils/decryptAESSecretStorageItem.ts";
|
||||||
|
import encryptAESSecretStorageItem from "../utils/encryptAESSecretStorageItem.ts";
|
||||||
|
import { AESEncryptedSecretStoragePayload } from "../@types/AESEncryptedSecretStoragePayload.ts";
|
||||||
|
import { calculateKeyCheck } from "../calculateKeyCheck.ts";
|
||||||
|
|
||||||
const KEY_BACKUP_KEYS_PER_REQUEST = 200;
|
const KEY_BACKUP_KEYS_PER_REQUEST = 200;
|
||||||
const KEY_BACKUP_CHECK_RATE_LIMIT = 5000; // ms
|
const KEY_BACKUP_CHECK_RATE_LIMIT = 5000; // ms
|
||||||
@@ -94,7 +97,7 @@ interface BackupAlgorithmClass {
|
|||||||
|
|
||||||
interface BackupAlgorithm {
|
interface BackupAlgorithm {
|
||||||
untrusted: boolean;
|
untrusted: boolean;
|
||||||
encryptSession(data: Record<string, any>): Promise<Curve25519SessionData | IEncryptedPayload>;
|
encryptSession(data: Record<string, any>): Promise<Curve25519SessionData | AESEncryptedSecretStoragePayload>;
|
||||||
decryptSessions(ciphertexts: Record<string, IKeyBackupSession>): Promise<IMegolmSessionData[]>;
|
decryptSessions(ciphertexts: Record<string, IKeyBackupSession>): Promise<IMegolmSessionData[]>;
|
||||||
authData: AuthData;
|
authData: AuthData;
|
||||||
keyMatches(key: ArrayLike<number>): Promise<boolean>;
|
keyMatches(key: ArrayLike<number>): Promise<boolean>;
|
||||||
@@ -825,22 +828,24 @@ export class Aes256 implements BackupAlgorithm {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public encryptSession(data: Record<string, any>): Promise<IEncryptedPayload> {
|
public encryptSession(data: Record<string, any>): Promise<AESEncryptedSecretStoragePayload> {
|
||||||
const plainText: Record<string, any> = Object.assign({}, data);
|
const plainText: Record<string, any> = Object.assign({}, data);
|
||||||
delete plainText.session_id;
|
delete plainText.session_id;
|
||||||
delete plainText.room_id;
|
delete plainText.room_id;
|
||||||
delete plainText.first_known_index;
|
delete plainText.first_known_index;
|
||||||
return encryptAES(JSON.stringify(plainText), this.key, data.session_id);
|
return encryptAESSecretStorageItem(JSON.stringify(plainText), this.key, data.session_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async decryptSessions(
|
public async decryptSessions(
|
||||||
sessions: Record<string, IKeyBackupSession<IEncryptedPayload>>,
|
sessions: Record<string, IKeyBackupSession<AESEncryptedSecretStoragePayload>>,
|
||||||
): Promise<IMegolmSessionData[]> {
|
): Promise<IMegolmSessionData[]> {
|
||||||
const keys: IMegolmSessionData[] = [];
|
const keys: IMegolmSessionData[] = [];
|
||||||
|
|
||||||
for (const [sessionId, sessionData] of Object.entries(sessions)) {
|
for (const [sessionId, sessionData] of Object.entries(sessions)) {
|
||||||
try {
|
try {
|
||||||
const decrypted = JSON.parse(await decryptAES(sessionData.session_data, this.key, sessionId));
|
const decrypted = JSON.parse(
|
||||||
|
await decryptAESSecretStorageItem(sessionData.session_data, this.key, sessionId),
|
||||||
|
);
|
||||||
decrypted.session_id = sessionId;
|
decrypted.session_id = sessionId;
|
||||||
keys.push(decrypted);
|
keys.push(decrypted);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -19,11 +19,12 @@ import anotherjson from "another-json";
|
|||||||
import type { IDeviceKeys, IOneTimeKey } from "../@types/crypto.ts";
|
import type { IDeviceKeys, IOneTimeKey } from "../@types/crypto.ts";
|
||||||
import { decodeBase64, encodeBase64 } from "../base64.ts";
|
import { decodeBase64, encodeBase64 } from "../base64.ts";
|
||||||
import { IndexedDBCryptoStore } from "../crypto/store/indexeddb-crypto-store.ts";
|
import { IndexedDBCryptoStore } from "../crypto/store/indexeddb-crypto-store.ts";
|
||||||
import { decryptAES, encryptAES } from "./aes.ts";
|
|
||||||
import { logger } from "../logger.ts";
|
import { logger } from "../logger.ts";
|
||||||
import { Crypto } from "./index.ts";
|
import { Crypto } from "./index.ts";
|
||||||
import { Method } from "../http-api/index.ts";
|
import { Method } from "../http-api/index.ts";
|
||||||
import { SecretStorageKeyDescription } from "../secret-storage.ts";
|
import { SecretStorageKeyDescription } from "../secret-storage.ts";
|
||||||
|
import decryptAESSecretStorageItem from "../utils/decryptAESSecretStorageItem.ts";
|
||||||
|
import encryptAESSecretStorageItem from "../utils/encryptAESSecretStorageItem.ts";
|
||||||
|
|
||||||
export interface IDehydratedDevice {
|
export interface IDehydratedDevice {
|
||||||
device_id: string; // eslint-disable-line camelcase
|
device_id: string; // eslint-disable-line camelcase
|
||||||
@@ -61,7 +62,7 @@ export class DehydrationManager {
|
|||||||
if (result) {
|
if (result) {
|
||||||
const { key, keyInfo, deviceDisplayName, time } = result;
|
const { key, keyInfo, deviceDisplayName, time } = result;
|
||||||
const pickleKey = Buffer.from(this.crypto.olmDevice.pickleKey);
|
const pickleKey = Buffer.from(this.crypto.olmDevice.pickleKey);
|
||||||
const decrypted = await decryptAES(key, pickleKey, DEHYDRATION_ALGORITHM);
|
const decrypted = await decryptAESSecretStorageItem(key, pickleKey, DEHYDRATION_ALGORITHM);
|
||||||
this.key = decodeBase64(decrypted);
|
this.key = decodeBase64(decrypted);
|
||||||
this.keyInfo = keyInfo;
|
this.keyInfo = keyInfo;
|
||||||
this.deviceDisplayName = deviceDisplayName;
|
this.deviceDisplayName = deviceDisplayName;
|
||||||
@@ -141,7 +142,7 @@ export class DehydrationManager {
|
|||||||
const pickleKey = Buffer.from(this.crypto.olmDevice.pickleKey);
|
const pickleKey = Buffer.from(this.crypto.olmDevice.pickleKey);
|
||||||
|
|
||||||
// update the crypto store with the timestamp
|
// update the crypto store with the timestamp
|
||||||
const key = await encryptAES(encodeBase64(this.key!), pickleKey, DEHYDRATION_ALGORITHM);
|
const key = await encryptAESSecretStorageItem(encodeBase64(this.key!), pickleKey, DEHYDRATION_ALGORITHM);
|
||||||
await this.crypto.cryptoStore.doTxn("readwrite", [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
|
await this.crypto.cryptoStore.doTxn("readwrite", [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
|
||||||
this.crypto.cryptoStore.storeSecretStorePrivateKey(txn, "dehydration", {
|
this.crypto.cryptoStore.storeSecretStorePrivateKey(txn, "dehydration", {
|
||||||
keyInfo: this.keyInfo,
|
keyInfo: this.keyInfo,
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ import { InRoomChannel, InRoomRequests } from "./verification/request/InRoomChan
|
|||||||
import { Request, ToDeviceChannel, ToDeviceRequests } from "./verification/request/ToDeviceChannel.ts";
|
import { Request, ToDeviceChannel, ToDeviceRequests } from "./verification/request/ToDeviceChannel.ts";
|
||||||
import { IllegalMethod } from "./verification/IllegalMethod.ts";
|
import { IllegalMethod } from "./verification/IllegalMethod.ts";
|
||||||
import { KeySignatureUploadError } from "../errors.ts";
|
import { KeySignatureUploadError } from "../errors.ts";
|
||||||
import { calculateKeyCheck, decryptAES, encryptAES, IEncryptedPayload } from "./aes.ts";
|
|
||||||
import { DehydrationManager } from "./dehydration.ts";
|
import { DehydrationManager } from "./dehydration.ts";
|
||||||
import { BackupManager, LibOlmBackupDecryptor, backupTrustInfoFromLegacyTrustInfo } from "./backup.ts";
|
import { BackupManager, LibOlmBackupDecryptor, backupTrustInfoFromLegacyTrustInfo } from "./backup.ts";
|
||||||
import { IStore } from "../store/index.ts";
|
import { IStore } from "../store/index.ts";
|
||||||
@@ -107,6 +106,10 @@ import { deviceInfoToDevice } from "./device-converter.ts";
|
|||||||
import { ClientPrefix, MatrixError, Method } from "../http-api/index.ts";
|
import { ClientPrefix, MatrixError, Method } from "../http-api/index.ts";
|
||||||
import { decodeBase64, encodeBase64 } from "../base64.ts";
|
import { decodeBase64, encodeBase64 } from "../base64.ts";
|
||||||
import { KnownMembership } from "../@types/membership.ts";
|
import { KnownMembership } from "../@types/membership.ts";
|
||||||
|
import decryptAESSecretStorageItem from "../utils/decryptAESSecretStorageItem.ts";
|
||||||
|
import encryptAESSecretStorageItem from "../utils/encryptAESSecretStorageItem.ts";
|
||||||
|
import { AESEncryptedSecretStoragePayload } from "../@types/AESEncryptedSecretStoragePayload.ts";
|
||||||
|
import { calculateKeyCheck } from "../calculateKeyCheck.ts";
|
||||||
|
|
||||||
/* re-exports for backwards compatibility */
|
/* re-exports for backwards compatibility */
|
||||||
export type {
|
export type {
|
||||||
@@ -1322,11 +1325,13 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
|||||||
* @returns the key, if any, or null
|
* @returns the key, if any, or null
|
||||||
*/
|
*/
|
||||||
public async getSessionBackupPrivateKey(): Promise<Uint8Array | null> {
|
public async getSessionBackupPrivateKey(): Promise<Uint8Array | null> {
|
||||||
const encodedKey = await new Promise<Uint8Array | IEncryptedPayload | string | null>((resolve) => {
|
const encodedKey = await new Promise<Uint8Array | AESEncryptedSecretStoragePayload | string | null>(
|
||||||
|
(resolve) => {
|
||||||
this.cryptoStore.doTxn("readonly", [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
|
this.cryptoStore.doTxn("readonly", [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
|
||||||
this.cryptoStore.getSecretStorePrivateKey(txn, resolve, "m.megolm_backup.v1");
|
this.cryptoStore.getSecretStorePrivateKey(txn, resolve, "m.megolm_backup.v1");
|
||||||
});
|
});
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
let key: Uint8Array | null = null;
|
let key: Uint8Array | null = null;
|
||||||
|
|
||||||
@@ -1337,7 +1342,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
|||||||
}
|
}
|
||||||
if (encodedKey && typeof encodedKey === "object" && "ciphertext" in encodedKey) {
|
if (encodedKey && typeof encodedKey === "object" && "ciphertext" in encodedKey) {
|
||||||
const pickleKey = Buffer.from(this.olmDevice.pickleKey);
|
const pickleKey = Buffer.from(this.olmDevice.pickleKey);
|
||||||
const decrypted = await decryptAES(encodedKey, pickleKey, "m.megolm_backup.v1");
|
const decrypted = await decryptAESSecretStorageItem(encodedKey, pickleKey, "m.megolm_backup.v1");
|
||||||
key = decodeBase64(decrypted);
|
key = decodeBase64(decrypted);
|
||||||
}
|
}
|
||||||
return key;
|
return key;
|
||||||
@@ -1354,7 +1359,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
|||||||
throw new Error(`storeSessionBackupPrivateKey expects Uint8Array, got ${key}`);
|
throw new Error(`storeSessionBackupPrivateKey expects Uint8Array, got ${key}`);
|
||||||
}
|
}
|
||||||
const pickleKey = Buffer.from(this.olmDevice.pickleKey);
|
const pickleKey = Buffer.from(this.olmDevice.pickleKey);
|
||||||
const encryptedKey = await encryptAES(encodeBase64(key), pickleKey, "m.megolm_backup.v1");
|
const encryptedKey = await encryptAESSecretStorageItem(encodeBase64(key), pickleKey, "m.megolm_backup.v1");
|
||||||
return this.cryptoStore.doTxn("readwrite", [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
|
return this.cryptoStore.doTxn("readwrite", [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
|
||||||
this.cryptoStore.storeSecretStorePrivateKey(txn, "m.megolm_backup.v1", encryptedKey);
|
this.cryptoStore.storeSecretStorePrivateKey(txn, "m.megolm_backup.v1", encryptedKey);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ import { Logger } from "../../logger.ts";
|
|||||||
import { InboundGroupSessionData } from "../OlmDevice.ts";
|
import { InboundGroupSessionData } from "../OlmDevice.ts";
|
||||||
import { MatrixEvent } from "../../models/event.ts";
|
import { MatrixEvent } from "../../models/event.ts";
|
||||||
import { DehydrationManager } from "../dehydration.ts";
|
import { DehydrationManager } from "../dehydration.ts";
|
||||||
import { IEncryptedPayload } from "../aes.ts";
|
|
||||||
import { CrossSigningKeyInfo } from "../../crypto-api/index.ts";
|
import { CrossSigningKeyInfo } from "../../crypto-api/index.ts";
|
||||||
|
import { AESEncryptedSecretStoragePayload } from "../../@types/AESEncryptedSecretStoragePayload.ts";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal module. Definitions for storage for the crypto module
|
* Internal module. Definitions for storage for the crypto module
|
||||||
@@ -35,11 +35,11 @@ import { CrossSigningKeyInfo } from "../../crypto-api/index.ts";
|
|||||||
export interface SecretStorePrivateKeys {
|
export interface SecretStorePrivateKeys {
|
||||||
"dehydration": {
|
"dehydration": {
|
||||||
keyInfo: DehydrationManager["keyInfo"];
|
keyInfo: DehydrationManager["keyInfo"];
|
||||||
key: IEncryptedPayload;
|
key: AESEncryptedSecretStoragePayload;
|
||||||
deviceDisplayName: string;
|
deviceDisplayName: string;
|
||||||
time: number;
|
time: number;
|
||||||
} | null;
|
} | null;
|
||||||
"m.megolm_backup.v1": IEncryptedPayload;
|
"m.megolm_backup.v1": AESEncryptedSecretStoragePayload;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -33,10 +33,10 @@ import { encodeUri, logDuration } from "../utils.ts";
|
|||||||
import { OutgoingRequestProcessor } from "./OutgoingRequestProcessor.ts";
|
import { OutgoingRequestProcessor } from "./OutgoingRequestProcessor.ts";
|
||||||
import { sleep } from "../utils.ts";
|
import { sleep } from "../utils.ts";
|
||||||
import { BackupDecryptor } from "../common-crypto/CryptoBackend.ts";
|
import { BackupDecryptor } from "../common-crypto/CryptoBackend.ts";
|
||||||
import { IEncryptedPayload } from "../crypto/aes.ts";
|
|
||||||
import { ImportRoomKeyProgressData, ImportRoomKeysOpts } from "../crypto-api/index.ts";
|
import { ImportRoomKeyProgressData, ImportRoomKeysOpts } from "../crypto-api/index.ts";
|
||||||
import { IKeyBackupInfo } from "../crypto/keybackup.ts";
|
import { IKeyBackupInfo } from "../crypto/keybackup.ts";
|
||||||
import { IKeyBackup } from "../crypto/backup.ts";
|
import { IKeyBackup } from "../crypto/backup.ts";
|
||||||
|
import { AESEncryptedSecretStoragePayload } from "../@types/AESEncryptedSecretStoragePayload.ts";
|
||||||
|
|
||||||
/** Authentification of the backup info, depends on algorithm */
|
/** Authentification of the backup info, depends on algorithm */
|
||||||
type AuthData = KeyBackupInfo["auth_data"];
|
type AuthData = KeyBackupInfo["auth_data"];
|
||||||
@@ -622,7 +622,7 @@ export class RustBackupDecryptor implements BackupDecryptor {
|
|||||||
* Implements {@link BackupDecryptor#decryptSessions}
|
* Implements {@link BackupDecryptor#decryptSessions}
|
||||||
*/
|
*/
|
||||||
public async decryptSessions(
|
public async decryptSessions(
|
||||||
ciphertexts: Record<string, KeyBackupSession<Curve25519SessionData | IEncryptedPayload>>,
|
ciphertexts: Record<string, KeyBackupSession<Curve25519SessionData | AESEncryptedSecretStoragePayload>>,
|
||||||
): Promise<IMegolmSessionData[]> {
|
): Promise<IMegolmSessionData[]> {
|
||||||
const keys: IMegolmSessionData[] = [];
|
const keys: IMegolmSessionData[] = [];
|
||||||
for (const [sessionId, sessionData] of Object.entries(ciphertexts)) {
|
for (const [sessionId, sessionData] of Object.entries(ciphertexts)) {
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-wasm";
|
|||||||
import { Logger } from "../logger.ts";
|
import { Logger } from "../logger.ts";
|
||||||
import { CryptoStore, MigrationState, SecretStorePrivateKeys } from "../crypto/store/base.ts";
|
import { CryptoStore, MigrationState, SecretStorePrivateKeys } from "../crypto/store/base.ts";
|
||||||
import { IndexedDBCryptoStore } from "../crypto/store/indexeddb-crypto-store.ts";
|
import { IndexedDBCryptoStore } from "../crypto/store/indexeddb-crypto-store.ts";
|
||||||
import { decryptAES, IEncryptedPayload } from "../crypto/aes.ts";
|
|
||||||
import { IHttpOpts, MatrixHttpApi } from "../http-api/index.ts";
|
import { IHttpOpts, MatrixHttpApi } from "../http-api/index.ts";
|
||||||
import { requestKeyBackupVersion } from "./backup.ts";
|
import { requestKeyBackupVersion } from "./backup.ts";
|
||||||
import { IRoomEncryption } from "../crypto/RoomList.ts";
|
import { IRoomEncryption } from "../crypto/RoomList.ts";
|
||||||
@@ -28,6 +27,8 @@ import { RustCrypto } from "./rust-crypto.ts";
|
|||||||
import { KeyBackupInfo } from "../crypto-api/keybackup.ts";
|
import { KeyBackupInfo } from "../crypto-api/keybackup.ts";
|
||||||
import { sleep } from "../utils.ts";
|
import { sleep } from "../utils.ts";
|
||||||
import { encodeBase64 } from "../base64.ts";
|
import { encodeBase64 } from "../base64.ts";
|
||||||
|
import decryptAESSecretStorageItem from "../utils/decryptAESSecretStorageItem.ts";
|
||||||
|
import { AESEncryptedSecretStoragePayload } from "../@types/AESEncryptedSecretStoragePayload.ts";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine if any data needs migrating from the legacy store, and do so.
|
* Determine if any data needs migrating from the legacy store, and do so.
|
||||||
@@ -421,7 +422,7 @@ async function getAndDecryptCachedSecretKey(
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (key && key.ciphertext && key.iv && key.mac) {
|
if (key && key.ciphertext && key.iv && key.mac) {
|
||||||
return await decryptAES(key as IEncryptedPayload, legacyPickleKey, name);
|
return await decryptAESSecretStorageItem(key as AESEncryptedSecretStoragePayload, legacyPickleKey, name);
|
||||||
} else if (key instanceof Uint8Array) {
|
} else if (key instanceof Uint8Array) {
|
||||||
// This is a legacy backward compatibility case where the key was stored in clear.
|
// This is a legacy backward compatibility case where the key was stored in clear.
|
||||||
return encodeBase64(key);
|
return encodeBase64(key);
|
||||||
|
|||||||
@@ -23,9 +23,12 @@ limitations under the License.
|
|||||||
import { TypedEventEmitter } from "./models/typed-event-emitter.ts";
|
import { TypedEventEmitter } from "./models/typed-event-emitter.ts";
|
||||||
import { ClientEvent, ClientEventHandlerMap } from "./client.ts";
|
import { ClientEvent, ClientEventHandlerMap } from "./client.ts";
|
||||||
import { MatrixEvent } from "./models/event.ts";
|
import { MatrixEvent } from "./models/event.ts";
|
||||||
import { calculateKeyCheck, decryptAES, encryptAES, IEncryptedPayload } from "./crypto/aes.ts";
|
|
||||||
import { randomString } from "./randomstring.ts";
|
import { randomString } from "./randomstring.ts";
|
||||||
import { logger } from "./logger.ts";
|
import { logger } from "./logger.ts";
|
||||||
|
import encryptAESSecretStorageItem from "./utils/encryptAESSecretStorageItem.ts";
|
||||||
|
import decryptAESSecretStorageItem from "./utils/decryptAESSecretStorageItem.ts";
|
||||||
|
import { AESEncryptedSecretStoragePayload } from "./@types/AESEncryptedSecretStoragePayload.ts";
|
||||||
|
import { calculateKeyCheck } from "./crypto/aes.ts";
|
||||||
|
|
||||||
export const SECRET_STORAGE_ALGORITHM_V1_AES = "m.secret_storage.v1.aes-hmac-sha2";
|
export const SECRET_STORAGE_ALGORITHM_V1_AES = "m.secret_storage.v1.aes-hmac-sha2";
|
||||||
|
|
||||||
@@ -200,13 +203,13 @@ export interface SecretStorageCallbacks {
|
|||||||
|
|
||||||
interface SecretInfo {
|
interface SecretInfo {
|
||||||
encrypted: {
|
encrypted: {
|
||||||
[keyId: string]: IEncryptedPayload;
|
[keyId: string]: AESEncryptedSecretStoragePayload;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Decryptors {
|
interface Decryptors {
|
||||||
encrypt: (plaintext: string) => Promise<IEncryptedPayload>;
|
encrypt: (plaintext: string) => Promise<AESEncryptedSecretStoragePayload>;
|
||||||
decrypt: (ciphertext: IEncryptedPayload) => Promise<string>;
|
decrypt: (ciphertext: AESEncryptedSecretStoragePayload) => Promise<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -491,7 +494,7 @@ export class ServerSideSecretStorageImpl implements ServerSideSecretStorage {
|
|||||||
* @param keys - The IDs of the keys to use to encrypt the secret, or null/undefined to use the default key.
|
* @param keys - The IDs of the keys to use to encrypt the secret, or null/undefined to use the default key.
|
||||||
*/
|
*/
|
||||||
public async store(name: string, secret: string, keys?: string[] | null): Promise<void> {
|
public async store(name: string, secret: string, keys?: string[] | null): Promise<void> {
|
||||||
const encrypted: Record<string, IEncryptedPayload> = {};
|
const encrypted: Record<string, AESEncryptedSecretStoragePayload> = {};
|
||||||
|
|
||||||
if (!keys) {
|
if (!keys) {
|
||||||
const defaultKeyId = await this.getDefaultKeyId();
|
const defaultKeyId = await this.getDefaultKeyId();
|
||||||
@@ -638,11 +641,11 @@ export class ServerSideSecretStorageImpl implements ServerSideSecretStorage {
|
|||||||
|
|
||||||
if (keys[keyId].algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
|
if (keys[keyId].algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
|
||||||
const decryption = {
|
const decryption = {
|
||||||
encrypt: function (secret: string): Promise<IEncryptedPayload> {
|
encrypt: function (secret: string): Promise<AESEncryptedSecretStoragePayload> {
|
||||||
return encryptAES(secret, privateKey, name);
|
return encryptAESSecretStorageItem(secret, privateKey, name);
|
||||||
},
|
},
|
||||||
decrypt: function (encInfo: IEncryptedPayload): Promise<string> {
|
decrypt: function (encInfo: AESEncryptedSecretStoragePayload): Promise<string> {
|
||||||
return decryptAES(encInfo, privateKey, name);
|
return decryptAESSecretStorageItem(encInfo, privateKey, name);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
return [keyId, decryption];
|
return [keyId, decryption];
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export * from "./@types/membership.ts";
|
|||||||
export type * from "./@types/event.ts";
|
export type * from "./@types/event.ts";
|
||||||
export type * from "./@types/events.ts";
|
export type * from "./@types/events.ts";
|
||||||
export type * from "./@types/state_events.ts";
|
export type * from "./@types/state_events.ts";
|
||||||
|
export type * from "./@types/AESEncryptedSecretStoragePayload.ts";
|
||||||
|
|
||||||
/** The different methods for device and user verification */
|
/** The different methods for device and user verification */
|
||||||
export enum VerificationMethod {
|
export enum VerificationMethod {
|
||||||
|
|||||||
54
src/utils/decryptAESSecretStorageItem.ts
Normal file
54
src/utils/decryptAESSecretStorageItem.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 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 { decodeBase64 } from "../base64.ts";
|
||||||
|
import { deriveKeys } from "./internal/deriveKeys.ts";
|
||||||
|
import { AESEncryptedSecretStoragePayload } from "../@types/AESEncryptedSecretStoragePayload.ts";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrypt an AES-encrypted Secret Storage item.
|
||||||
|
*
|
||||||
|
* @param data - the encrypted data, returned by {@link utils/encryptAESSecretStorageItem.default | encryptAESSecretStorageItem}.
|
||||||
|
* @param key - the encryption key to use as an input to the HKDF function which is used to derive the AES key. Must
|
||||||
|
* be the same as provided to {@link utils/encryptAESSecretStorageItem.default | encryptAESSecretStorageItem}.
|
||||||
|
* @param name - the name of the secret. Also used as an input to the HKDF operation which is used to derive the AES
|
||||||
|
* key, so again must be the same as provided to {@link utils/encryptAESSecretStorageItem.default | encryptAESSecretStorageItem}.
|
||||||
|
*/
|
||||||
|
export default async function decryptAESSecretStorageItem(
|
||||||
|
data: AESEncryptedSecretStoragePayload,
|
||||||
|
key: Uint8Array,
|
||||||
|
name: string,
|
||||||
|
): Promise<string> {
|
||||||
|
const [aesKey, hmacKey] = await deriveKeys(key, name);
|
||||||
|
|
||||||
|
const ciphertext = decodeBase64(data.ciphertext);
|
||||||
|
|
||||||
|
if (!(await globalThis.crypto.subtle.verify({ name: "HMAC" }, hmacKey, decodeBase64(data.mac), ciphertext))) {
|
||||||
|
throw new Error(`Error decrypting secret ${name}: bad MAC`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const plaintext = await globalThis.crypto.subtle.decrypt(
|
||||||
|
{
|
||||||
|
name: "AES-CTR",
|
||||||
|
counter: decodeBase64(data.iv),
|
||||||
|
length: 64,
|
||||||
|
},
|
||||||
|
aesKey,
|
||||||
|
ciphertext,
|
||||||
|
);
|
||||||
|
|
||||||
|
return new TextDecoder().decode(new Uint8Array(plaintext));
|
||||||
|
}
|
||||||
73
src/utils/encryptAESSecretStorageItem.ts
Normal file
73
src/utils/encryptAESSecretStorageItem.ts
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 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 { decodeBase64, encodeBase64 } from "../base64.ts";
|
||||||
|
import { deriveKeys } from "./internal/deriveKeys.ts";
|
||||||
|
import { AESEncryptedSecretStoragePayload } from "../@types/AESEncryptedSecretStoragePayload.ts";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encrypt a string as a secret storage item, using AES-CTR.
|
||||||
|
*
|
||||||
|
* @param data - the plaintext to encrypt
|
||||||
|
* @param key - the encryption key to use as an input to the HKDF function which is used to derive the AES key for
|
||||||
|
* encryption. Obviously, the same key must be provided when decrypting.
|
||||||
|
* @param name - the name of the secret. Used as an input to the HKDF operation which is used to derive the AES key,
|
||||||
|
* so again the same value must be provided when decrypting.
|
||||||
|
* @param ivStr - the base64-encoded initialization vector to use. If not supplied, a random one will be generated.
|
||||||
|
*
|
||||||
|
* @returns The encrypted result, including the ciphertext itself, the initialization vector (as supplied in `ivStr`,
|
||||||
|
* or generated), and an HMAC on the ciphertext — all base64-encoded.
|
||||||
|
*/
|
||||||
|
export default async function encryptAESSecretStorageItem(
|
||||||
|
data: string,
|
||||||
|
key: Uint8Array,
|
||||||
|
name: string,
|
||||||
|
ivStr?: string,
|
||||||
|
): Promise<AESEncryptedSecretStoragePayload> {
|
||||||
|
let iv: Uint8Array;
|
||||||
|
if (ivStr) {
|
||||||
|
iv = decodeBase64(ivStr);
|
||||||
|
} else {
|
||||||
|
iv = new Uint8Array(16);
|
||||||
|
globalThis.crypto.getRandomValues(iv);
|
||||||
|
|
||||||
|
// clear bit 63 of the IV to stop us hitting the 64-bit counter boundary
|
||||||
|
// (which would mean we wouldn't be able to decrypt on Android). The loss
|
||||||
|
// of a single bit of iv is a price we have to pay.
|
||||||
|
iv[8] &= 0x7f;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [aesKey, hmacKey] = await deriveKeys(key, name);
|
||||||
|
const encodedData = new TextEncoder().encode(data);
|
||||||
|
|
||||||
|
const ciphertext = await globalThis.crypto.subtle.encrypt(
|
||||||
|
{
|
||||||
|
name: "AES-CTR",
|
||||||
|
counter: iv,
|
||||||
|
length: 64,
|
||||||
|
},
|
||||||
|
aesKey,
|
||||||
|
encodedData,
|
||||||
|
);
|
||||||
|
|
||||||
|
const hmac = await globalThis.crypto.subtle.sign({ name: "HMAC" }, hmacKey, ciphertext);
|
||||||
|
|
||||||
|
return {
|
||||||
|
iv: encodeBase64(iv),
|
||||||
|
ciphertext: encodeBase64(ciphertext),
|
||||||
|
mac: encodeBase64(hmac),
|
||||||
|
};
|
||||||
|
}
|
||||||
63
src/utils/internal/deriveKeys.ts
Normal file
63
src/utils/internal/deriveKeys.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// salt for HKDF, with 8 bytes of zeros
|
||||||
|
const zeroSalt = new Uint8Array(8);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Derive AES and HMAC keys from a master key.
|
||||||
|
*
|
||||||
|
* This is used for deriving secret storage keys: see https://spec.matrix.org/v1.11/client-server-api/#msecret_storagev1aes-hmac-sha2 (step 1).
|
||||||
|
*
|
||||||
|
* @param key
|
||||||
|
* @param name
|
||||||
|
*/
|
||||||
|
export async function deriveKeys(key: Uint8Array, name: string): Promise<[CryptoKey, CryptoKey]> {
|
||||||
|
const hkdfkey = await globalThis.crypto.subtle.importKey("raw", key, { name: "HKDF" }, false, ["deriveBits"]);
|
||||||
|
const keybits = await globalThis.crypto.subtle.deriveBits(
|
||||||
|
{
|
||||||
|
name: "HKDF",
|
||||||
|
salt: zeroSalt,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore: https://github.com/microsoft/TypeScript-DOM-lib-generator/pull/879
|
||||||
|
info: new TextEncoder().encode(name),
|
||||||
|
hash: "SHA-256",
|
||||||
|
},
|
||||||
|
hkdfkey,
|
||||||
|
512,
|
||||||
|
);
|
||||||
|
|
||||||
|
const aesKey = keybits.slice(0, 32);
|
||||||
|
const hmacKey = keybits.slice(32);
|
||||||
|
|
||||||
|
const aesProm = globalThis.crypto.subtle.importKey("raw", aesKey, { name: "AES-CTR" }, false, [
|
||||||
|
"encrypt",
|
||||||
|
"decrypt",
|
||||||
|
]);
|
||||||
|
|
||||||
|
const hmacProm = globalThis.crypto.subtle.importKey(
|
||||||
|
"raw",
|
||||||
|
hmacKey,
|
||||||
|
{
|
||||||
|
name: "HMAC",
|
||||||
|
hash: { name: "SHA-256" },
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
["sign", "verify"],
|
||||||
|
);
|
||||||
|
|
||||||
|
return Promise.all([aesProm, hmacProm]);
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
"$schema": "https://typedoc.org/schema.json",
|
"$schema": "https://typedoc.org/schema.json",
|
||||||
"plugin": ["typedoc-plugin-mdn-links", "typedoc-plugin-missing-exports", "typedoc-plugin-coverage"],
|
"plugin": ["typedoc-plugin-mdn-links", "typedoc-plugin-missing-exports", "typedoc-plugin-coverage"],
|
||||||
"coverageLabel": "TypeDoc",
|
"coverageLabel": "TypeDoc",
|
||||||
"entryPoints": ["src/matrix.ts", "src/types.ts", "src/testing.ts"],
|
"entryPoints": ["src/matrix.ts", "src/types.ts", "src/testing.ts", "src/utils/*.ts"],
|
||||||
"excludeExternals": true,
|
"excludeExternals": true,
|
||||||
"out": "_docs"
|
"out": "_docs"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user