From eae6bb439564c821a671678b9213dc69f0c44762 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Wed, 25 Sep 2024 15:48:57 +0200 Subject: [PATCH 01/17] Move `SecretEncryptedPayload` in `src/utils/@types` --- src/utils/@types/SecretEncryptedPayload.ts | 29 ++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 src/utils/@types/SecretEncryptedPayload.ts diff --git a/src/utils/@types/SecretEncryptedPayload.ts b/src/utils/@types/SecretEncryptedPayload.ts new file mode 100644 index 00000000000..5240963f5cd --- /dev/null +++ b/src/utils/@types/SecretEncryptedPayload.ts @@ -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 encrypted payload, as returned by the secret storage API. + * See https://spec.matrix.org/v1.11/client-server-api/#msecret_storagev1aes-hmac-sha2-1 + */ +export interface SecretEncryptedPayload { + [key: string]: any; // extensible + /** the initialization vector in base64 */ + iv: string; + /** the ciphertext in base64 */ + ciphertext: string; + /** the HMAC in base64 */ + mac: string; +} From 91327a435d6e03346e3e1b7991f75e39e2befca3 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Wed, 25 Sep 2024 14:28:40 +0200 Subject: [PATCH 02/17] Move `encryptAES` to a dedicated file. Moved in a utils folder. --- src/utils/encryptAES.ts | 73 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 src/utils/encryptAES.ts diff --git a/src/utils/encryptAES.ts b/src/utils/encryptAES.ts new file mode 100644 index 00000000000..75509775396 --- /dev/null +++ b/src/utils/encryptAES.ts @@ -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 "../rust-crypto/deriveKeys.ts"; +import { SecretEncryptedPayload } from "./@types/SecretEncryptedPayload.ts"; + +/** + * 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 { + 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), + }; +} From 9d7a074b8b24343ae34dee8b1858bae54dec55e3 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Wed, 25 Sep 2024 14:30:47 +0200 Subject: [PATCH 03/17] Move `deriveKeys` to a dedicated file in order to share it --- src/rust-crypto/deriveKeys.ts | 60 +++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 src/rust-crypto/deriveKeys.ts diff --git a/src/rust-crypto/deriveKeys.ts b/src/rust-crypto/deriveKeys.ts new file mode 100644 index 00000000000..6a0503aaac2 --- /dev/null +++ b/src/rust-crypto/deriveKeys.ts @@ -0,0 +1,60 @@ +/* + * 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. + * @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]); +} From 854f38b8c3e1630637933bef1e3be73c6e703040 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Wed, 25 Sep 2024 14:37:45 +0200 Subject: [PATCH 04/17] Move `decryptAES` to a dedicated file. Moved in a utils folder. --- src/utils/decryptAES.ts | 50 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 src/utils/decryptAES.ts diff --git a/src/utils/decryptAES.ts b/src/utils/decryptAES.ts new file mode 100644 index 00000000000..04284ce8a6c --- /dev/null +++ b/src/utils/decryptAES.ts @@ -0,0 +1,50 @@ +/* + * 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 "../rust-crypto/deriveKeys.ts"; +import { SecretEncryptedPayload } from "./@types/SecretEncryptedPayload.ts"; + +/** + * 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: SecretEncryptedPayload, key: Uint8Array, name: string): Promise { + 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)); +} From 3216a7d7a24d4710a30cd5bae66f06926b19a7cb Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Wed, 25 Sep 2024 14:53:00 +0200 Subject: [PATCH 05/17] Move `calculateKeyCheck` to a dedicated file. Moved in a utils folder. --- src/utils/calculateKeyCheck.ts | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 src/utils/calculateKeyCheck.ts diff --git a/src/utils/calculateKeyCheck.ts b/src/utils/calculateKeyCheck.ts new file mode 100644 index 00000000000..a53132fd95b --- /dev/null +++ b/src/utils/calculateKeyCheck.ts @@ -0,0 +1,32 @@ +/* + * 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 { encryptAES } from "./encryptAES.ts"; +import { SecretEncryptedPayload } from "./@types/SecretEncryptedPayload.ts"; + +// 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 { + return encryptAES(ZERO_STR, key, "", iv); +} From 48ceb73e6268281cd9a0b467745724fc00a47450 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Wed, 25 Sep 2024 14:53:46 +0200 Subject: [PATCH 06/17] Remove AES functions in `aes.ts` and export new ones for backward compatibility --- src/crypto/aes.ts | 154 ++-------------------------------------------- 1 file changed, 4 insertions(+), 150 deletions(-) diff --git a/src/crypto/aes.ts b/src/crypto/aes.ts index 2fc32aec412..d13cc3d37a1 100644 --- a/src/crypto/aes.ts +++ b/src/crypto/aes.ts @@ -14,153 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { decodeBase64, encodeBase64 } from "../base64.ts"; - -// salt for HKDF, with 8 bytes of zeros -const zeroSalt = new Uint8Array(8); - -export interface IEncryptedPayload { - [key: string]: any; // extensible - /** the initialization vector in base64 */ - 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 { - 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 { - 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 { - return encryptAES(ZERO_STR, key, "", iv); -} +// Export for backwards compatibility +export type { SecretEncryptedPayload as IEncryptedPayload } from "../utils/@types/SecretEncryptedPayload.ts"; +export { encryptAES } from "../utils/encryptAES.ts"; +export { decryptAES } from "../utils/decryptAES.ts"; From ca95c3dcc211681ab9c2b9ab011d8861aa878232 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Wed, 25 Sep 2024 15:17:21 +0200 Subject: [PATCH 07/17] Update import to use new functions --- spec/integ/crypto/cross-signing.spec.ts | 2 +- spec/unit/crypto/secrets.spec.ts | 2 +- spec/unit/rust-crypto/rust-crypto.spec.ts | 2 +- spec/unit/secret-storage.spec.ts | 2 +- src/crypto-api/keybackup.ts | 4 ++-- src/crypto/CrossSigning.ts | 3 ++- src/crypto/backup.ts | 11 +++++++---- src/crypto/dehydration.ts | 3 ++- src/crypto/index.ts | 7 +++++-- src/crypto/store/base.ts | 6 +++--- src/rust-crypto/backup.ts | 4 ++-- src/rust-crypto/libolm_migration.ts | 5 +++-- src/secret-storage.ts | 17 ++++++++++------- 13 files changed, 40 insertions(+), 28 deletions(-) diff --git a/spec/integ/crypto/cross-signing.spec.ts b/spec/integ/crypto/cross-signing.spec.ts index b81bf6f00f4..2ef95393e53 100644 --- a/spec/integ/crypto/cross-signing.spec.ts +++ b/spec/integ/crypto/cross-signing.spec.ts @@ -21,7 +21,7 @@ import { IDBFactory } from "fake-indexeddb"; import { CRYPTO_BACKENDS, InitCrypto, syncPromise } from "../../test-utils/test-utils"; import { AuthDict, createClient, CryptoEvent, MatrixClient } from "../../../src"; import { mockInitialApiRequests, mockSetupCrossSigningRequests } from "../../test-utils/mockEndpoints"; -import { encryptAES } from "../../../src/crypto/aes"; +import { encryptAES } from "../../../src/utils/encryptAES"; import { CryptoCallbacks, CrossSigningKey } from "../../../src/crypto-api"; import { SECRET_STORAGE_ALGORITHM_V1_AES } from "../../../src/secret-storage"; import { ISyncResponder, SyncResponder } from "../../test-utils/SyncResponder"; diff --git a/spec/unit/crypto/secrets.spec.ts b/spec/unit/crypto/secrets.spec.ts index 835c593d8e5..8c2ac5bcc6e 100644 --- a/spec/unit/crypto/secrets.spec.ts +++ b/spec/unit/crypto/secrets.spec.ts @@ -20,7 +20,7 @@ import { IObject } from "../../../src/crypto/olmlib"; import { MatrixEvent } from "../../../src/models/event"; import { TestClient } from "../../TestClient"; import { makeTestClients } from "./verification/util"; -import { encryptAES } from "../../../src/crypto/aes"; +import { encryptAES } from "../../../src/utils/encryptAES"; import { createSecretStorageKey, resetCrossSigningKeys } from "./crypto-utils"; import { logger } from "../../../src/logger"; import { ClientEvent, ICreateClientOpts, MatrixClient } from "../../../src/client"; diff --git a/spec/unit/rust-crypto/rust-crypto.spec.ts b/spec/unit/rust-crypto/rust-crypto.spec.ts index 07501a47c95..2de47122125 100644 --- a/spec/unit/rust-crypto/rust-crypto.spec.ts +++ b/spec/unit/rust-crypto/rust-crypto.spec.ts @@ -69,7 +69,7 @@ import { logger } from "../../../src/logger"; import { OutgoingRequestsManager } from "../../../src/rust-crypto/OutgoingRequestsManager"; import { ClientEvent, ClientEventHandlerMap } from "../../../src/client"; import { Curve25519AuthData } from "../../../src/crypto-api/keybackup"; -import { encryptAES } from "../../../src/crypto/aes"; +import { encryptAES } from "../../../src/utils/encryptAES"; import { CryptoStore, SecretStorePrivateKeys } from "../../../src/crypto/store/base"; const TEST_USER = "@alice:example.com"; diff --git a/spec/unit/secret-storage.spec.ts b/spec/unit/secret-storage.spec.ts index e2f07b79d19..1ddcebd9297 100644 --- a/spec/unit/secret-storage.spec.ts +++ b/spec/unit/secret-storage.spec.ts @@ -25,7 +25,7 @@ import { ServerSideSecretStorageImpl, trimTrailingEquals, } from "../../src/secret-storage"; -import { calculateKeyCheck } from "../../src/crypto/aes"; +import { calculateKeyCheck } from "../../src/utils/calculateKeyCheck"; import { randomString } from "../../src/randomstring"; describe("ServerSideSecretStorageImpl", function () { diff --git a/src/crypto-api/keybackup.ts b/src/crypto-api/keybackup.ts index 3209de6c320..b15f5c38fa4 100644 --- a/src/crypto-api/keybackup.ts +++ b/src/crypto-api/keybackup.ts @@ -15,7 +15,7 @@ limitations under the License. */ import { ISigned } from "../@types/signed.ts"; -import { IEncryptedPayload } from "../crypto/aes.ts"; +import { SecretEncryptedPayload } from "../utils/@types/SecretEncryptedPayload.ts"; export interface Curve25519AuthData { public_key: string; @@ -77,7 +77,7 @@ export interface Curve25519SessionData { } /* eslint-disable camelcase */ -export interface KeyBackupSession { +export interface KeyBackupSession { first_message_index: number; forwarded_count: number; is_verified: boolean; diff --git a/src/crypto/CrossSigning.ts b/src/crypto/CrossSigning.ts index a4caf1c53a1..c429a4a8068 100644 --- a/src/crypto/CrossSigning.ts +++ b/src/crypto/CrossSigning.ts @@ -22,7 +22,6 @@ import type { PkSigning } from "@matrix-org/olm"; import { IObject, pkSign, pkVerify } from "./olmlib.ts"; import { logger } from "../logger.ts"; import { IndexedDBCryptoStore } from "../crypto/store/indexeddb-crypto-store.ts"; -import { decryptAES, encryptAES } from "./aes.ts"; import { DeviceInfo } from "./deviceinfo.ts"; import { ISignedKey, MatrixClient } from "../client.ts"; import { OlmDevice } from "./OlmDevice.ts"; @@ -36,6 +35,8 @@ import { UserVerificationStatus as UserTrustLevel, } from "../crypto-api/index.ts"; import { decodeBase64, encodeBase64 } from "../base64.ts"; +import { encryptAES } from "../utils/encryptAES.ts"; +import { decryptAES } from "../utils/decryptAES.ts"; // backwards-compatibility re-exports export { UserTrustLevel }; diff --git a/src/crypto/backup.ts b/src/crypto/backup.ts index 2dc374f9868..b60e428812e 100644 --- a/src/crypto/backup.ts +++ b/src/crypto/backup.ts @@ -27,7 +27,6 @@ import { DeviceTrustLevel } from "./CrossSigning.ts"; import { keyFromPassphrase } from "./key_passphrase.ts"; import { encodeUri, safeSet, sleep } from "../utils.ts"; import { IndexedDBCryptoStore } from "./store/indexeddb-crypto-store.ts"; -import { calculateKeyCheck, decryptAES, encryptAES, IEncryptedPayload } from "./aes.ts"; import { Curve25519SessionData, IAes256AuthData, @@ -41,6 +40,10 @@ import { ClientPrefix, HTTPError, MatrixError, Method } from "../http-api/index. import { BackupTrustInfo } from "../crypto-api/keybackup.ts"; import { BackupDecryptor } from "../common-crypto/CryptoBackend.ts"; import { encodeRecoveryKey } from "../crypto-api/index.ts"; +import { decryptAES } from "../utils/decryptAES.ts"; +import { calculateKeyCheck } from "../utils/calculateKeyCheck.ts"; +import { encryptAES } from "../utils/encryptAES.ts"; +import { SecretEncryptedPayload } from "../utils/@types/SecretEncryptedPayload.ts"; const KEY_BACKUP_KEYS_PER_REQUEST = 200; const KEY_BACKUP_CHECK_RATE_LIMIT = 5000; // ms @@ -94,7 +97,7 @@ interface BackupAlgorithmClass { interface BackupAlgorithm { untrusted: boolean; - encryptSession(data: Record): Promise; + encryptSession(data: Record): Promise; decryptSessions(ciphertexts: Record): Promise; authData: AuthData; keyMatches(key: ArrayLike): Promise; @@ -825,7 +828,7 @@ export class Aes256 implements BackupAlgorithm { return false; } - public encryptSession(data: Record): Promise { + public encryptSession(data: Record): Promise { const plainText: Record = Object.assign({}, data); delete plainText.session_id; delete plainText.room_id; @@ -834,7 +837,7 @@ export class Aes256 implements BackupAlgorithm { } public async decryptSessions( - sessions: Record>, + sessions: Record>, ): Promise { const keys: IMegolmSessionData[] = []; diff --git a/src/crypto/dehydration.ts b/src/crypto/dehydration.ts index 01dafa83c71..80890c29adc 100644 --- a/src/crypto/dehydration.ts +++ b/src/crypto/dehydration.ts @@ -19,11 +19,12 @@ import anotherjson from "another-json"; import type { IDeviceKeys, IOneTimeKey } from "../@types/crypto.ts"; import { decodeBase64, encodeBase64 } from "../base64.ts"; import { IndexedDBCryptoStore } from "../crypto/store/indexeddb-crypto-store.ts"; -import { decryptAES, encryptAES } from "./aes.ts"; import { logger } from "../logger.ts"; import { Crypto } from "./index.ts"; import { Method } from "../http-api/index.ts"; import { SecretStorageKeyDescription } from "../secret-storage.ts"; +import { decryptAES } from "../utils/decryptAES.ts"; +import { encryptAES } from "../utils/encryptAES.ts"; export interface IDehydratedDevice { device_id: string; // eslint-disable-line camelcase diff --git a/src/crypto/index.ts b/src/crypto/index.ts index 38bf03c7fd8..d1f5cbbeef7 100644 --- a/src/crypto/index.ts +++ b/src/crypto/index.ts @@ -47,7 +47,6 @@ import { InRoomChannel, InRoomRequests } from "./verification/request/InRoomChan import { Request, ToDeviceChannel, ToDeviceRequests } from "./verification/request/ToDeviceChannel.ts"; import { IllegalMethod } from "./verification/IllegalMethod.ts"; import { KeySignatureUploadError } from "../errors.ts"; -import { calculateKeyCheck, decryptAES, encryptAES, IEncryptedPayload } from "./aes.ts"; import { DehydrationManager } from "./dehydration.ts"; import { BackupManager, LibOlmBackupDecryptor, backupTrustInfoFromLegacyTrustInfo } from "./backup.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 { decodeBase64, encodeBase64 } from "../base64.ts"; import { KnownMembership } from "../@types/membership.ts"; +import { calculateKeyCheck } from "../utils/calculateKeyCheck.ts"; +import { decryptAES } from "../utils/decryptAES.ts"; +import { encryptAES } from "../utils/encryptAES.ts"; +import { SecretEncryptedPayload } from "../utils/@types/SecretEncryptedPayload.ts"; /* re-exports for backwards compatibility */ export type { @@ -1323,7 +1326,7 @@ export class Crypto extends TypedEventEmitter { - const encodedKey = await new Promise((resolve) => { + const encodedKey = await new Promise((resolve) => { this.cryptoStore.doTxn("readonly", [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => { this.cryptoStore.getSecretStorePrivateKey(txn, resolve, "m.megolm_backup.v1"); }); diff --git a/src/crypto/store/base.ts b/src/crypto/store/base.ts index daf11b5b0b3..7d96e3d3af9 100644 --- a/src/crypto/store/base.ts +++ b/src/crypto/store/base.ts @@ -25,8 +25,8 @@ import { Logger } from "../../logger.ts"; import { InboundGroupSessionData } from "../OlmDevice.ts"; import { MatrixEvent } from "../../models/event.ts"; import { DehydrationManager } from "../dehydration.ts"; -import { IEncryptedPayload } from "../aes.ts"; import { CrossSigningKeyInfo } from "../../crypto-api/index.ts"; +import { SecretEncryptedPayload } from "../../utils/@types/SecretEncryptedPayload.ts"; /** * Internal module. Definitions for storage for the crypto module @@ -35,11 +35,11 @@ import { CrossSigningKeyInfo } from "../../crypto-api/index.ts"; export interface SecretStorePrivateKeys { "dehydration": { keyInfo: DehydrationManager["keyInfo"]; - key: IEncryptedPayload; + key: SecretEncryptedPayload; deviceDisplayName: string; time: number; } | null; - "m.megolm_backup.v1": IEncryptedPayload; + "m.megolm_backup.v1": SecretEncryptedPayload; } /** diff --git a/src/rust-crypto/backup.ts b/src/rust-crypto/backup.ts index c4407da21f8..38554a4ba68 100644 --- a/src/rust-crypto/backup.ts +++ b/src/rust-crypto/backup.ts @@ -33,10 +33,10 @@ import { encodeUri, logDuration } from "../utils.ts"; import { OutgoingRequestProcessor } from "./OutgoingRequestProcessor.ts"; import { sleep } from "../utils.ts"; import { BackupDecryptor } from "../common-crypto/CryptoBackend.ts"; -import { IEncryptedPayload } from "../crypto/aes.ts"; import { ImportRoomKeyProgressData, ImportRoomKeysOpts } from "../crypto-api/index.ts"; import { IKeyBackupInfo } from "../crypto/keybackup.ts"; import { IKeyBackup } from "../crypto/backup.ts"; +import { SecretEncryptedPayload } from "../utils/@types/SecretEncryptedPayload.ts"; /** Authentification of the backup info, depends on algorithm */ type AuthData = KeyBackupInfo["auth_data"]; @@ -622,7 +622,7 @@ export class RustBackupDecryptor implements BackupDecryptor { * Implements {@link BackupDecryptor#decryptSessions} */ public async decryptSessions( - ciphertexts: Record>, + ciphertexts: Record>, ): Promise { const keys: IMegolmSessionData[] = []; for (const [sessionId, sessionData] of Object.entries(ciphertexts)) { diff --git a/src/rust-crypto/libolm_migration.ts b/src/rust-crypto/libolm_migration.ts index 50d92a626e1..8b1bb13b79f 100644 --- a/src/rust-crypto/libolm_migration.ts +++ b/src/rust-crypto/libolm_migration.ts @@ -19,7 +19,6 @@ import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-wasm"; import { Logger } from "../logger.ts"; import { CryptoStore, MigrationState, SecretStorePrivateKeys } from "../crypto/store/base.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 { requestKeyBackupVersion } from "./backup.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 { sleep } from "../utils.ts"; import { encodeBase64 } from "../base64.ts"; +import { decryptAES } from "../utils/decryptAES.ts"; +import { SecretEncryptedPayload } from "../utils/@types/SecretEncryptedPayload.ts"; /** * 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) { - return await decryptAES(key as IEncryptedPayload, legacyPickleKey, name); + return await decryptAES(key as SecretEncryptedPayload, legacyPickleKey, name); } else if (key instanceof Uint8Array) { // This is a legacy backward compatibility case where the key was stored in clear. return encodeBase64(key); diff --git a/src/secret-storage.ts b/src/secret-storage.ts index 82dd4272e8a..c02ae33ebb5 100644 --- a/src/secret-storage.ts +++ b/src/secret-storage.ts @@ -23,9 +23,12 @@ limitations under the License. import { TypedEventEmitter } from "./models/typed-event-emitter.ts"; import { ClientEvent, ClientEventHandlerMap } from "./client.ts"; import { MatrixEvent } from "./models/event.ts"; -import { calculateKeyCheck, decryptAES, encryptAES, IEncryptedPayload } from "./crypto/aes.ts"; import { randomString } from "./randomstring.ts"; import { logger } from "./logger.ts"; +import { calculateKeyCheck } from "./utils/calculateKeyCheck.ts"; +import { encryptAES } from "./utils/encryptAES.ts"; +import { decryptAES } from "./utils/decryptAES.ts"; +import { SecretEncryptedPayload } from "./utils/@types/SecretEncryptedPayload.ts"; export const SECRET_STORAGE_ALGORITHM_V1_AES = "m.secret_storage.v1.aes-hmac-sha2"; @@ -200,13 +203,13 @@ export interface SecretStorageCallbacks { interface SecretInfo { encrypted: { - [keyId: string]: IEncryptedPayload; + [keyId: string]: SecretEncryptedPayload; }; } interface Decryptors { - encrypt: (plaintext: string) => Promise; - decrypt: (ciphertext: IEncryptedPayload) => Promise; + encrypt: (plaintext: string) => Promise; + decrypt: (ciphertext: SecretEncryptedPayload) => Promise; } /** @@ -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. */ public async store(name: string, secret: string, keys?: string[] | null): Promise { - const encrypted: Record = {}; + const encrypted: Record = {}; if (!keys) { const defaultKeyId = await this.getDefaultKeyId(); @@ -638,10 +641,10 @@ export class ServerSideSecretStorageImpl implements ServerSideSecretStorage { if (keys[keyId].algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) { const decryption = { - encrypt: function (secret: string): Promise { + encrypt: function (secret: string): Promise { return encryptAES(secret, privateKey, name); }, - decrypt: function (encInfo: IEncryptedPayload): Promise { + decrypt: function (encInfo: SecretEncryptedPayload): Promise { return decryptAES(encInfo, privateKey, name); }, }; From 83bcbb14d5e25ac3816d64d2a4655ff1a8b7e311 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Wed, 25 Sep 2024 16:18:42 +0200 Subject: [PATCH 08/17] Add `src/utils` entrypoint in `README.md` --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 338012c9f50..f0b4912fbf1 100644 --- a/README.md +++ b/README.md @@ -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/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/utils` | Standalone functions and their types | ## Examples From 073af1652e841acf77c48f546b9fd0751e33b825 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Fri, 27 Sep 2024 13:42:02 +0200 Subject: [PATCH 09/17] - Rename `SecretEncryptedPayload` to `AESEncryptedSecretStoragePayload`. - Move into `src/@types` --- .../AESEncryptedSecretStoragePayload.ts} | 4 ++-- src/crypto-api/keybackup.ts | 4 ++-- src/crypto/aes.ts | 2 +- src/crypto/backup.ts | 8 ++++---- src/crypto/index.ts | 14 ++++++++------ src/crypto/store/base.ts | 6 +++--- src/rust-crypto/backup.ts | 4 ++-- src/rust-crypto/libolm_migration.ts | 4 ++-- src/secret-storage.ts | 14 +++++++------- src/types.ts | 1 + src/utils/calculateKeyCheck.ts | 4 ++-- src/utils/decryptAES.ts | 8 ++++++-- src/utils/encryptAES.ts | 4 ++-- 13 files changed, 42 insertions(+), 35 deletions(-) rename src/{utils/@types/SecretEncryptedPayload.ts => @types/AESEncryptedSecretStoragePayload.ts} (89%) diff --git a/src/utils/@types/SecretEncryptedPayload.ts b/src/@types/AESEncryptedSecretStoragePayload.ts similarity index 89% rename from src/utils/@types/SecretEncryptedPayload.ts rename to src/@types/AESEncryptedSecretStoragePayload.ts index 5240963f5cd..5f33680b257 100644 --- a/src/utils/@types/SecretEncryptedPayload.ts +++ b/src/@types/AESEncryptedSecretStoragePayload.ts @@ -15,10 +15,10 @@ */ /** - * An encrypted payload, as returned by the secret storage API. + * An AES-encrypted secret storage payload. * See https://spec.matrix.org/v1.11/client-server-api/#msecret_storagev1aes-hmac-sha2-1 */ -export interface SecretEncryptedPayload { +export interface AESEncryptedSecretStoragePayload { [key: string]: any; // extensible /** the initialization vector in base64 */ iv: string; diff --git a/src/crypto-api/keybackup.ts b/src/crypto-api/keybackup.ts index b15f5c38fa4..8a82a5f4c13 100644 --- a/src/crypto-api/keybackup.ts +++ b/src/crypto-api/keybackup.ts @@ -15,7 +15,7 @@ limitations under the License. */ import { ISigned } from "../@types/signed.ts"; -import { SecretEncryptedPayload } from "../utils/@types/SecretEncryptedPayload.ts"; +import { AESEncryptedSecretStoragePayload } from "../@types/AESEncryptedSecretStoragePayload.ts"; export interface Curve25519AuthData { public_key: string; @@ -77,7 +77,7 @@ export interface Curve25519SessionData { } /* eslint-disable camelcase */ -export interface KeyBackupSession { +export interface KeyBackupSession { first_message_index: number; forwarded_count: number; is_verified: boolean; diff --git a/src/crypto/aes.ts b/src/crypto/aes.ts index d13cc3d37a1..bc410a3fcb1 100644 --- a/src/crypto/aes.ts +++ b/src/crypto/aes.ts @@ -15,6 +15,6 @@ limitations under the License. */ // Export for backwards compatibility -export type { SecretEncryptedPayload as IEncryptedPayload } from "../utils/@types/SecretEncryptedPayload.ts"; +export type { AESEncryptedSecretStoragePayload as IEncryptedPayload } from "../@types/AESEncryptedSecretStoragePayload.ts"; export { encryptAES } from "../utils/encryptAES.ts"; export { decryptAES } from "../utils/decryptAES.ts"; diff --git a/src/crypto/backup.ts b/src/crypto/backup.ts index b60e428812e..edd6d707d5a 100644 --- a/src/crypto/backup.ts +++ b/src/crypto/backup.ts @@ -43,7 +43,7 @@ import { encodeRecoveryKey } from "../crypto-api/index.ts"; import { decryptAES } from "../utils/decryptAES.ts"; import { calculateKeyCheck } from "../utils/calculateKeyCheck.ts"; import { encryptAES } from "../utils/encryptAES.ts"; -import { SecretEncryptedPayload } from "../utils/@types/SecretEncryptedPayload.ts"; +import { AESEncryptedSecretStoragePayload } from "../@types/AESEncryptedSecretStoragePayload.ts"; const KEY_BACKUP_KEYS_PER_REQUEST = 200; const KEY_BACKUP_CHECK_RATE_LIMIT = 5000; // ms @@ -97,7 +97,7 @@ interface BackupAlgorithmClass { interface BackupAlgorithm { untrusted: boolean; - encryptSession(data: Record): Promise; + encryptSession(data: Record): Promise; decryptSessions(ciphertexts: Record): Promise; authData: AuthData; keyMatches(key: ArrayLike): Promise; @@ -828,7 +828,7 @@ export class Aes256 implements BackupAlgorithm { return false; } - public encryptSession(data: Record): Promise { + public encryptSession(data: Record): Promise { const plainText: Record = Object.assign({}, data); delete plainText.session_id; delete plainText.room_id; @@ -837,7 +837,7 @@ export class Aes256 implements BackupAlgorithm { } public async decryptSessions( - sessions: Record>, + sessions: Record>, ): Promise { const keys: IMegolmSessionData[] = []; diff --git a/src/crypto/index.ts b/src/crypto/index.ts index d0768c9d6a6..f28eda1279a 100644 --- a/src/crypto/index.ts +++ b/src/crypto/index.ts @@ -109,7 +109,7 @@ import { KnownMembership } from "../@types/membership.ts"; import { calculateKeyCheck } from "../utils/calculateKeyCheck.ts"; import { decryptAES } from "../utils/decryptAES.ts"; import { encryptAES } from "../utils/encryptAES.ts"; -import { SecretEncryptedPayload } from "../utils/@types/SecretEncryptedPayload.ts"; +import { AESEncryptedSecretStoragePayload } from "../@types/AESEncryptedSecretStoragePayload.ts"; /* re-exports for backwards compatibility */ export type { @@ -1325,11 +1325,13 @@ export class Crypto extends TypedEventEmitter { - const encodedKey = await new Promise((resolve) => { - this.cryptoStore.doTxn("readonly", [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => { - this.cryptoStore.getSecretStorePrivateKey(txn, resolve, "m.megolm_backup.v1"); - }); - }); + const encodedKey = await new Promise( + (resolve) => { + this.cryptoStore.doTxn("readonly", [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => { + this.cryptoStore.getSecretStorePrivateKey(txn, resolve, "m.megolm_backup.v1"); + }); + }, + ); let key: Uint8Array | null = null; diff --git a/src/crypto/store/base.ts b/src/crypto/store/base.ts index 7d96e3d3af9..6bbd139d19b 100644 --- a/src/crypto/store/base.ts +++ b/src/crypto/store/base.ts @@ -26,7 +26,7 @@ import { InboundGroupSessionData } from "../OlmDevice.ts"; import { MatrixEvent } from "../../models/event.ts"; import { DehydrationManager } from "../dehydration.ts"; import { CrossSigningKeyInfo } from "../../crypto-api/index.ts"; -import { SecretEncryptedPayload } from "../../utils/@types/SecretEncryptedPayload.ts"; +import { AESEncryptedSecretStoragePayload } from "../../@types/AESEncryptedSecretStoragePayload.ts"; /** * Internal module. Definitions for storage for the crypto module @@ -35,11 +35,11 @@ import { SecretEncryptedPayload } from "../../utils/@types/SecretEncryptedPayloa export interface SecretStorePrivateKeys { "dehydration": { keyInfo: DehydrationManager["keyInfo"]; - key: SecretEncryptedPayload; + key: AESEncryptedSecretStoragePayload; deviceDisplayName: string; time: number; } | null; - "m.megolm_backup.v1": SecretEncryptedPayload; + "m.megolm_backup.v1": AESEncryptedSecretStoragePayload; } /** diff --git a/src/rust-crypto/backup.ts b/src/rust-crypto/backup.ts index 38554a4ba68..b83a8e7a712 100644 --- a/src/rust-crypto/backup.ts +++ b/src/rust-crypto/backup.ts @@ -36,7 +36,7 @@ import { BackupDecryptor } from "../common-crypto/CryptoBackend.ts"; import { ImportRoomKeyProgressData, ImportRoomKeysOpts } from "../crypto-api/index.ts"; import { IKeyBackupInfo } from "../crypto/keybackup.ts"; import { IKeyBackup } from "../crypto/backup.ts"; -import { SecretEncryptedPayload } from "../utils/@types/SecretEncryptedPayload.ts"; +import { AESEncryptedSecretStoragePayload } from "../@types/AESEncryptedSecretStoragePayload.ts"; /** Authentification of the backup info, depends on algorithm */ type AuthData = KeyBackupInfo["auth_data"]; @@ -622,7 +622,7 @@ export class RustBackupDecryptor implements BackupDecryptor { * Implements {@link BackupDecryptor#decryptSessions} */ public async decryptSessions( - ciphertexts: Record>, + ciphertexts: Record>, ): Promise { const keys: IMegolmSessionData[] = []; for (const [sessionId, sessionData] of Object.entries(ciphertexts)) { diff --git a/src/rust-crypto/libolm_migration.ts b/src/rust-crypto/libolm_migration.ts index 8b1bb13b79f..dee76b85d25 100644 --- a/src/rust-crypto/libolm_migration.ts +++ b/src/rust-crypto/libolm_migration.ts @@ -28,7 +28,7 @@ import { KeyBackupInfo } from "../crypto-api/keybackup.ts"; import { sleep } from "../utils.ts"; import { encodeBase64 } from "../base64.ts"; import { decryptAES } from "../utils/decryptAES.ts"; -import { SecretEncryptedPayload } from "../utils/@types/SecretEncryptedPayload.ts"; +import { AESEncryptedSecretStoragePayload } from "../@types/AESEncryptedSecretStoragePayload.ts"; /** * Determine if any data needs migrating from the legacy store, and do so. @@ -422,7 +422,7 @@ async function getAndDecryptCachedSecretKey( }); if (key && key.ciphertext && key.iv && key.mac) { - return await decryptAES(key as SecretEncryptedPayload, legacyPickleKey, name); + return await decryptAES(key as AESEncryptedSecretStoragePayload, legacyPickleKey, name); } else if (key instanceof Uint8Array) { // This is a legacy backward compatibility case where the key was stored in clear. return encodeBase64(key); diff --git a/src/secret-storage.ts b/src/secret-storage.ts index c02ae33ebb5..d80c0bfa711 100644 --- a/src/secret-storage.ts +++ b/src/secret-storage.ts @@ -28,7 +28,7 @@ import { logger } from "./logger.ts"; import { calculateKeyCheck } from "./utils/calculateKeyCheck.ts"; import { encryptAES } from "./utils/encryptAES.ts"; import { decryptAES } from "./utils/decryptAES.ts"; -import { SecretEncryptedPayload } from "./utils/@types/SecretEncryptedPayload.ts"; +import { AESEncryptedSecretStoragePayload } from "./@types/AESEncryptedSecretStoragePayload.ts"; export const SECRET_STORAGE_ALGORITHM_V1_AES = "m.secret_storage.v1.aes-hmac-sha2"; @@ -203,13 +203,13 @@ export interface SecretStorageCallbacks { interface SecretInfo { encrypted: { - [keyId: string]: SecretEncryptedPayload; + [keyId: string]: AESEncryptedSecretStoragePayload; }; } interface Decryptors { - encrypt: (plaintext: string) => Promise; - decrypt: (ciphertext: SecretEncryptedPayload) => Promise; + encrypt: (plaintext: string) => Promise; + decrypt: (ciphertext: AESEncryptedSecretStoragePayload) => Promise; } /** @@ -494,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. */ public async store(name: string, secret: string, keys?: string[] | null): Promise { - const encrypted: Record = {}; + const encrypted: Record = {}; if (!keys) { const defaultKeyId = await this.getDefaultKeyId(); @@ -641,10 +641,10 @@ export class ServerSideSecretStorageImpl implements ServerSideSecretStorage { if (keys[keyId].algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) { const decryption = { - encrypt: function (secret: string): Promise { + encrypt: function (secret: string): Promise { return encryptAES(secret, privateKey, name); }, - decrypt: function (encInfo: SecretEncryptedPayload): Promise { + decrypt: function (encInfo: AESEncryptedSecretStoragePayload): Promise { return decryptAES(encInfo, privateKey, name); }, }; diff --git a/src/types.ts b/src/types.ts index 24f1620427c..6266f63167f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -26,6 +26,7 @@ export * from "./@types/membership.ts"; export type * from "./@types/event.ts"; export type * from "./@types/events.ts"; export type * from "./@types/state_events.ts"; +export type * from "./@types/AESEncryptedSecretStoragePayload.ts"; /** The different methods for device and user verification */ export enum VerificationMethod { diff --git a/src/utils/calculateKeyCheck.ts b/src/utils/calculateKeyCheck.ts index a53132fd95b..0bdf3061ff4 100644 --- a/src/utils/calculateKeyCheck.ts +++ b/src/utils/calculateKeyCheck.ts @@ -15,7 +15,7 @@ */ import { encryptAES } from "./encryptAES.ts"; -import { SecretEncryptedPayload } from "./@types/SecretEncryptedPayload.ts"; +import { AESEncryptedSecretStoragePayload } from "../@types/AESEncryptedSecretStoragePayload.ts"; // 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"; @@ -27,6 +27,6 @@ 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 * 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 { +export function calculateKeyCheck(key: Uint8Array, iv?: string): Promise { return encryptAES(ZERO_STR, key, "", iv); } diff --git a/src/utils/decryptAES.ts b/src/utils/decryptAES.ts index 04284ce8a6c..2696c2bf250 100644 --- a/src/utils/decryptAES.ts +++ b/src/utils/decryptAES.ts @@ -16,7 +16,7 @@ import { decodeBase64 } from "../base64.ts"; import { deriveKeys } from "../rust-crypto/deriveKeys.ts"; -import { SecretEncryptedPayload } from "./@types/SecretEncryptedPayload.ts"; +import { AESEncryptedSecretStoragePayload } from "../@types/AESEncryptedSecretStoragePayload.ts"; /** * Decrypt an AES-encrypted string. @@ -27,7 +27,11 @@ import { SecretEncryptedPayload } from "./@types/SecretEncryptedPayload.ts"; * @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: SecretEncryptedPayload, key: Uint8Array, name: string): Promise { +export async function decryptAES( + data: AESEncryptedSecretStoragePayload, + key: Uint8Array, + name: string, +): Promise { const [aesKey, hmacKey] = await deriveKeys(key, name); const ciphertext = decodeBase64(data.ciphertext); diff --git a/src/utils/encryptAES.ts b/src/utils/encryptAES.ts index 75509775396..bdc4b734a7a 100644 --- a/src/utils/encryptAES.ts +++ b/src/utils/encryptAES.ts @@ -16,7 +16,7 @@ import { decodeBase64, encodeBase64 } from "../base64.ts"; import { deriveKeys } from "../rust-crypto/deriveKeys.ts"; -import { SecretEncryptedPayload } from "./@types/SecretEncryptedPayload.ts"; +import { AESEncryptedSecretStoragePayload } from "../@types/AESEncryptedSecretStoragePayload.ts"; /** * Encrypt a string using AES-CTR. @@ -36,7 +36,7 @@ export async function encryptAES( key: Uint8Array, name: string, ivStr?: string, -): Promise { +): Promise { let iv: Uint8Array; if (ivStr) { iv = decodeBase64(ivStr); From e06c0b721dc8a91d4314a140b4e936f9e41a6c5c Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Fri, 27 Sep 2024 13:52:37 +0200 Subject: [PATCH 10/17] Move `calculateKeyCheck` into `secret-storage.ts`. --- spec/unit/secret-storage.spec.ts | 2 +- src/crypto/aes.ts | 1 + src/crypto/backup.ts | 2 +- src/crypto/index.ts | 2 +- src/secret-storage.ts | 16 +++++++++++++++- src/utils/calculateKeyCheck.ts | 32 -------------------------------- 6 files changed, 19 insertions(+), 36 deletions(-) delete mode 100644 src/utils/calculateKeyCheck.ts diff --git a/spec/unit/secret-storage.spec.ts b/spec/unit/secret-storage.spec.ts index 1ddcebd9297..b2346d88e6c 100644 --- a/spec/unit/secret-storage.spec.ts +++ b/spec/unit/secret-storage.spec.ts @@ -18,6 +18,7 @@ import { Mocked } from "jest-mock"; import { AccountDataClient, + calculateKeyCheck, PassphraseInfo, SecretStorageCallbacks, SecretStorageKeyDescriptionAesV1, @@ -25,7 +26,6 @@ import { ServerSideSecretStorageImpl, trimTrailingEquals, } from "../../src/secret-storage"; -import { calculateKeyCheck } from "../../src/utils/calculateKeyCheck"; import { randomString } from "../../src/randomstring"; describe("ServerSideSecretStorageImpl", function () { diff --git a/src/crypto/aes.ts b/src/crypto/aes.ts index bc410a3fcb1..99da0d333bd 100644 --- a/src/crypto/aes.ts +++ b/src/crypto/aes.ts @@ -18,3 +18,4 @@ limitations under the License. export type { AESEncryptedSecretStoragePayload as IEncryptedPayload } from "../@types/AESEncryptedSecretStoragePayload.ts"; export { encryptAES } from "../utils/encryptAES.ts"; export { decryptAES } from "../utils/decryptAES.ts"; +export { calculateKeyCheck } from "../secret-storage.ts"; diff --git a/src/crypto/backup.ts b/src/crypto/backup.ts index edd6d707d5a..5a38c25f3d8 100644 --- a/src/crypto/backup.ts +++ b/src/crypto/backup.ts @@ -41,9 +41,9 @@ import { BackupTrustInfo } from "../crypto-api/keybackup.ts"; import { BackupDecryptor } from "../common-crypto/CryptoBackend.ts"; import { encodeRecoveryKey } from "../crypto-api/index.ts"; import { decryptAES } from "../utils/decryptAES.ts"; -import { calculateKeyCheck } from "../utils/calculateKeyCheck.ts"; import { encryptAES } from "../utils/encryptAES.ts"; import { AESEncryptedSecretStoragePayload } from "../@types/AESEncryptedSecretStoragePayload.ts"; +import { calculateKeyCheck } from "../secret-storage.ts"; const KEY_BACKUP_KEYS_PER_REQUEST = 200; const KEY_BACKUP_CHECK_RATE_LIMIT = 5000; // ms diff --git a/src/crypto/index.ts b/src/crypto/index.ts index f28eda1279a..9bc9eb9561f 100644 --- a/src/crypto/index.ts +++ b/src/crypto/index.ts @@ -75,6 +75,7 @@ import { MapWithDefault, recursiveMapToObject } from "../utils.ts"; import { AccountDataClient, AddSecretStorageKeyOpts, + calculateKeyCheck, SECRET_STORAGE_ALGORITHM_V1_AES, SecretStorageKeyDescription, SecretStorageKeyObject, @@ -106,7 +107,6 @@ import { deviceInfoToDevice } from "./device-converter.ts"; import { ClientPrefix, MatrixError, Method } from "../http-api/index.ts"; import { decodeBase64, encodeBase64 } from "../base64.ts"; import { KnownMembership } from "../@types/membership.ts"; -import { calculateKeyCheck } from "../utils/calculateKeyCheck.ts"; import { decryptAES } from "../utils/decryptAES.ts"; import { encryptAES } from "../utils/encryptAES.ts"; import { AESEncryptedSecretStoragePayload } from "../@types/AESEncryptedSecretStoragePayload.ts"; diff --git a/src/secret-storage.ts b/src/secret-storage.ts index d80c0bfa711..b22ea9c2412 100644 --- a/src/secret-storage.ts +++ b/src/secret-storage.ts @@ -25,7 +25,6 @@ import { ClientEvent, ClientEventHandlerMap } from "./client.ts"; import { MatrixEvent } from "./models/event.ts"; import { randomString } from "./randomstring.ts"; import { logger } from "./logger.ts"; -import { calculateKeyCheck } from "./utils/calculateKeyCheck.ts"; import { encryptAES } from "./utils/encryptAES.ts"; import { decryptAES } from "./utils/decryptAES.ts"; import { AESEncryptedSecretStoragePayload } from "./@types/AESEncryptedSecretStoragePayload.ts"; @@ -676,3 +675,18 @@ export function trimTrailingEquals(input: string): string { return input; } } + +// 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 { + return encryptAES(ZERO_STR, key, "", iv); +} diff --git a/src/utils/calculateKeyCheck.ts b/src/utils/calculateKeyCheck.ts deleted file mode 100644 index 0bdf3061ff4..00000000000 --- a/src/utils/calculateKeyCheck.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * 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 { encryptAES } from "./encryptAES.ts"; -import { AESEncryptedSecretStoragePayload } from "../@types/AESEncryptedSecretStoragePayload.ts"; - -// 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 { - return encryptAES(ZERO_STR, key, "", iv); -} From 6e7b7baa6311409ceb8e9279e32f9a94c5af0761 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Fri, 27 Sep 2024 13:58:41 +0200 Subject: [PATCH 11/17] Move `deriveKeys` into `src/utils/internal` folder. --- src/utils/decryptAES.ts | 2 +- src/utils/encryptAES.ts | 2 +- src/{rust-crypto => utils/internal}/deriveKeys.ts | 3 +++ 3 files changed, 5 insertions(+), 2 deletions(-) rename src/{rust-crypto => utils/internal}/deriveKeys.ts (92%) diff --git a/src/utils/decryptAES.ts b/src/utils/decryptAES.ts index 2696c2bf250..d9e63fb2104 100644 --- a/src/utils/decryptAES.ts +++ b/src/utils/decryptAES.ts @@ -15,7 +15,7 @@ */ import { decodeBase64 } from "../base64.ts"; -import { deriveKeys } from "../rust-crypto/deriveKeys.ts"; +import { deriveKeys } from "./internal/deriveKeys.ts"; import { AESEncryptedSecretStoragePayload } from "../@types/AESEncryptedSecretStoragePayload.ts"; /** diff --git a/src/utils/encryptAES.ts b/src/utils/encryptAES.ts index bdc4b734a7a..ed28e37becf 100644 --- a/src/utils/encryptAES.ts +++ b/src/utils/encryptAES.ts @@ -15,7 +15,7 @@ */ import { decodeBase64, encodeBase64 } from "../base64.ts"; -import { deriveKeys } from "../rust-crypto/deriveKeys.ts"; +import { deriveKeys } from "./internal/deriveKeys.ts"; import { AESEncryptedSecretStoragePayload } from "../@types/AESEncryptedSecretStoragePayload.ts"; /** diff --git a/src/rust-crypto/deriveKeys.ts b/src/utils/internal/deriveKeys.ts similarity index 92% rename from src/rust-crypto/deriveKeys.ts rename to src/utils/internal/deriveKeys.ts index 6a0503aaac2..13249f846a0 100644 --- a/src/rust-crypto/deriveKeys.ts +++ b/src/utils/internal/deriveKeys.ts @@ -19,6 +19,9 @@ 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 */ From 8b1dd09bea235b261f70b185f35a66d1d35d1d49 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Fri, 27 Sep 2024 14:10:03 +0200 Subject: [PATCH 12/17] - Rename `encryptAES` on `encryptAESSecretStorageItem` - Change named export by default export --- spec/integ/crypto/cross-signing.spec.ts | 8 ++++---- spec/unit/crypto/secrets.spec.ts | 4 ++-- spec/unit/rust-crypto/rust-crypto.spec.ts | 4 ++-- src/crypto/CrossSigning.ts | 4 ++-- src/crypto/aes.ts | 4 +++- src/crypto/backup.ts | 4 ++-- src/crypto/dehydration.ts | 4 ++-- src/crypto/index.ts | 4 ++-- src/secret-storage.ts | 6 +++--- .../{encryptAES.ts => encryptAESSecretStorageItem.ts} | 4 ++-- 10 files changed, 24 insertions(+), 22 deletions(-) rename src/utils/{encryptAES.ts => encryptAESSecretStorageItem.ts} (95%) diff --git a/spec/integ/crypto/cross-signing.spec.ts b/spec/integ/crypto/cross-signing.spec.ts index 2ef95393e53..95b0f756e0a 100644 --- a/spec/integ/crypto/cross-signing.spec.ts +++ b/spec/integ/crypto/cross-signing.spec.ts @@ -21,7 +21,7 @@ import { IDBFactory } from "fake-indexeddb"; import { CRYPTO_BACKENDS, InitCrypto, syncPromise } from "../../test-utils/test-utils"; import { AuthDict, createClient, CryptoEvent, MatrixClient } from "../../../src"; import { mockInitialApiRequests, mockSetupCrossSigningRequests } from "../../test-utils/mockEndpoints"; -import { encryptAES } from "../../../src/utils/encryptAES"; +import encryptAESSecretStorageItem from "../../../src/utils/encryptAESSecretStorageItem.ts"; import { CryptoCallbacks, CrossSigningKey } from "../../../src/crypto-api"; import { SECRET_STORAGE_ALGORITHM_V1_AES } from "../../../src/secret-storage"; 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()); // 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, encryptionKey, "m.cross_signing.master", ); - const selfSigningKey = await encryptAES( + const selfSigningKey = await encryptAESSecretStorageItem( SELF_CROSS_SIGNING_PRIVATE_KEY_BASE64, encryptionKey, "m.cross_signing.self_signing", ); - const userSigningKey = await encryptAES( + const userSigningKey = await encryptAESSecretStorageItem( USER_CROSS_SIGNING_PRIVATE_KEY_BASE64, encryptionKey, "m.cross_signing.user_signing", diff --git a/spec/unit/crypto/secrets.spec.ts b/spec/unit/crypto/secrets.spec.ts index 8c2ac5bcc6e..f09657c88b1 100644 --- a/spec/unit/crypto/secrets.spec.ts +++ b/spec/unit/crypto/secrets.spec.ts @@ -20,7 +20,7 @@ import { IObject } from "../../../src/crypto/olmlib"; import { MatrixEvent } from "../../../src/models/event"; import { TestClient } from "../../TestClient"; import { makeTestClients } from "./verification/util"; -import { encryptAES } from "../../../src/utils/encryptAES"; +import encryptAESSecretStorageItem from "../../../src/utils/encryptAESSecretStorageItem.ts"; import { createSecretStorageKey, resetCrossSigningKeys } from "./crypto-utils"; import { logger } from "../../../src/logger"; import { ClientEvent, ICreateClientOpts, MatrixClient } from "../../../src/client"; @@ -612,7 +612,7 @@ describe("Secrets", function () { type: "m.megolm_backup.v1", content: { 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", secretStorageKeys.key_id, "m.megolm_backup.v1", diff --git a/spec/unit/rust-crypto/rust-crypto.spec.ts b/spec/unit/rust-crypto/rust-crypto.spec.ts index 2de47122125..0b16ea1ee5c 100644 --- a/spec/unit/rust-crypto/rust-crypto.spec.ts +++ b/spec/unit/rust-crypto/rust-crypto.spec.ts @@ -69,7 +69,7 @@ import { logger } from "../../../src/logger"; import { OutgoingRequestsManager } from "../../../src/rust-crypto/OutgoingRequestsManager"; import { ClientEvent, ClientEventHandlerMap } from "../../../src/client"; import { Curve25519AuthData } from "../../../src/crypto-api/keybackup"; -import { encryptAES } from "../../../src/utils/encryptAES"; +import encryptAESSecretStorageItem from "../../../src/utils/encryptAESSecretStorageItem.ts"; import { CryptoStore, SecretStorePrivateKeys } from "../../../src/crypto/store/base"; const TEST_USER = "@alice:example.com"; @@ -425,7 +425,7 @@ describe("initRustCrypto", () => { }, 10000); 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); } diff --git a/src/crypto/CrossSigning.ts b/src/crypto/CrossSigning.ts index c429a4a8068..7f52418bb05 100644 --- a/src/crypto/CrossSigning.ts +++ b/src/crypto/CrossSigning.ts @@ -35,7 +35,7 @@ import { UserVerificationStatus as UserTrustLevel, } from "../crypto-api/index.ts"; import { decodeBase64, encodeBase64 } from "../base64.ts"; -import { encryptAES } from "../utils/encryptAES.ts"; +import encryptAESSecretStorageItem from "../utils/encryptAESSecretStorageItem.ts"; import { decryptAES } from "../utils/decryptAES.ts"; // backwards-compatibility re-exports @@ -677,7 +677,7 @@ export function createCryptoStoreCacheCallbacks(store: CryptoStore, olmDevice: O throw new Error(`storeCrossSigningKeyCache expects Uint8Array, got ${key}`); } 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) => { store.storeSecretStorePrivateKey(txn, type, encryptedKey); }); diff --git a/src/crypto/aes.ts b/src/crypto/aes.ts index 99da0d333bd..732b20bd710 100644 --- a/src/crypto/aes.ts +++ b/src/crypto/aes.ts @@ -14,8 +14,10 @@ See the License for the specific language governing permissions and limitations under the License. */ +import encryptAESSecretStorageItem from "../utils/encryptAESSecretStorageItem.ts"; + // Export for backwards compatibility export type { AESEncryptedSecretStoragePayload as IEncryptedPayload } from "../@types/AESEncryptedSecretStoragePayload.ts"; -export { encryptAES } from "../utils/encryptAES.ts"; +export { encryptAESSecretStorageItem }; export { decryptAES } from "../utils/decryptAES.ts"; export { calculateKeyCheck } from "../secret-storage.ts"; diff --git a/src/crypto/backup.ts b/src/crypto/backup.ts index 5a38c25f3d8..18595d7a017 100644 --- a/src/crypto/backup.ts +++ b/src/crypto/backup.ts @@ -41,7 +41,7 @@ import { BackupTrustInfo } from "../crypto-api/keybackup.ts"; import { BackupDecryptor } from "../common-crypto/CryptoBackend.ts"; import { encodeRecoveryKey } from "../crypto-api/index.ts"; import { decryptAES } from "../utils/decryptAES.ts"; -import { encryptAES } from "../utils/encryptAES.ts"; +import encryptAESSecretStorageItem from "../utils/encryptAESSecretStorageItem.ts"; import { AESEncryptedSecretStoragePayload } from "../@types/AESEncryptedSecretStoragePayload.ts"; import { calculateKeyCheck } from "../secret-storage.ts"; @@ -833,7 +833,7 @@ export class Aes256 implements BackupAlgorithm { delete plainText.session_id; delete plainText.room_id; 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( diff --git a/src/crypto/dehydration.ts b/src/crypto/dehydration.ts index 80890c29adc..b9786c7d684 100644 --- a/src/crypto/dehydration.ts +++ b/src/crypto/dehydration.ts @@ -24,7 +24,7 @@ import { Crypto } from "./index.ts"; import { Method } from "../http-api/index.ts"; import { SecretStorageKeyDescription } from "../secret-storage.ts"; import { decryptAES } from "../utils/decryptAES.ts"; -import { encryptAES } from "../utils/encryptAES.ts"; +import encryptAESSecretStorageItem from "../utils/encryptAESSecretStorageItem.ts"; export interface IDehydratedDevice { device_id: string; // eslint-disable-line camelcase @@ -142,7 +142,7 @@ export class DehydrationManager { const pickleKey = Buffer.from(this.crypto.olmDevice.pickleKey); // 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) => { this.crypto.cryptoStore.storeSecretStorePrivateKey(txn, "dehydration", { keyInfo: this.keyInfo, diff --git a/src/crypto/index.ts b/src/crypto/index.ts index 9bc9eb9561f..b325638ebc2 100644 --- a/src/crypto/index.ts +++ b/src/crypto/index.ts @@ -108,7 +108,7 @@ import { ClientPrefix, MatrixError, Method } from "../http-api/index.ts"; import { decodeBase64, encodeBase64 } from "../base64.ts"; import { KnownMembership } from "../@types/membership.ts"; import { decryptAES } from "../utils/decryptAES.ts"; -import { encryptAES } from "../utils/encryptAES.ts"; +import encryptAESSecretStorageItem from "../utils/encryptAESSecretStorageItem.ts"; import { AESEncryptedSecretStoragePayload } from "../@types/AESEncryptedSecretStoragePayload.ts"; /* re-exports for backwards compatibility */ @@ -1359,7 +1359,7 @@ export class Crypto extends TypedEventEmitter { this.cryptoStore.storeSecretStorePrivateKey(txn, "m.megolm_backup.v1", encryptedKey); }); diff --git a/src/secret-storage.ts b/src/secret-storage.ts index b22ea9c2412..8cb612886fc 100644 --- a/src/secret-storage.ts +++ b/src/secret-storage.ts @@ -25,7 +25,7 @@ import { ClientEvent, ClientEventHandlerMap } from "./client.ts"; import { MatrixEvent } from "./models/event.ts"; import { randomString } from "./randomstring.ts"; import { logger } from "./logger.ts"; -import { encryptAES } from "./utils/encryptAES.ts"; +import encryptAESSecretStorageItem from "./utils/encryptAESSecretStorageItem.ts"; import { decryptAES } from "./utils/decryptAES.ts"; import { AESEncryptedSecretStoragePayload } from "./@types/AESEncryptedSecretStoragePayload.ts"; @@ -641,7 +641,7 @@ export class ServerSideSecretStorageImpl implements ServerSideSecretStorage { if (keys[keyId].algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) { const decryption = { encrypt: function (secret: string): Promise { - return encryptAES(secret, privateKey, name); + return encryptAESSecretStorageItem(secret, privateKey, name); }, decrypt: function (encInfo: AESEncryptedSecretStoragePayload): Promise { return decryptAES(encInfo, privateKey, name); @@ -688,5 +688,5 @@ 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 * @returns An object that contains, `mac` and `iv` properties. */ export function calculateKeyCheck(key: Uint8Array, iv?: string): Promise { - return encryptAES(ZERO_STR, key, "", iv); + return encryptAESSecretStorageItem(ZERO_STR, key, "", iv); } diff --git a/src/utils/encryptAES.ts b/src/utils/encryptAESSecretStorageItem.ts similarity index 95% rename from src/utils/encryptAES.ts rename to src/utils/encryptAESSecretStorageItem.ts index ed28e37becf..064e914a125 100644 --- a/src/utils/encryptAES.ts +++ b/src/utils/encryptAESSecretStorageItem.ts @@ -19,7 +19,7 @@ import { deriveKeys } from "./internal/deriveKeys.ts"; import { AESEncryptedSecretStoragePayload } from "../@types/AESEncryptedSecretStoragePayload.ts"; /** - * Encrypt a string using AES-CTR. + * 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 @@ -31,7 +31,7 @@ import { AESEncryptedSecretStoragePayload } from "../@types/AESEncryptedSecretSt * @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( +export default async function encryptAESSecretStorageItem( data: string, key: Uint8Array, name: string, From c99cf954482206478033b6fb86fccb8afe50b11e Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Fri, 27 Sep 2024 14:28:53 +0200 Subject: [PATCH 13/17] - Rename `decryptAES` on `decryptAESSecretStorageItem` - Change named export by default export --- src/crypto/CrossSigning.ts | 4 ++-- src/crypto/aes.ts | 4 ++-- src/crypto/backup.ts | 6 ++++-- src/crypto/dehydration.ts | 4 ++-- src/crypto/index.ts | 4 ++-- src/rust-crypto/libolm_migration.ts | 4 ++-- src/secret-storage.ts | 4 ++-- src/utils/{decryptAES.ts => decryptAESSecretStorageItem.ts} | 4 ++-- 8 files changed, 18 insertions(+), 16 deletions(-) rename src/utils/{decryptAES.ts => decryptAESSecretStorageItem.ts} (94%) diff --git a/src/crypto/CrossSigning.ts b/src/crypto/CrossSigning.ts index 7f52418bb05..175b878b762 100644 --- a/src/crypto/CrossSigning.ts +++ b/src/crypto/CrossSigning.ts @@ -36,7 +36,7 @@ import { } from "../crypto-api/index.ts"; import { decodeBase64, encodeBase64 } from "../base64.ts"; import encryptAESSecretStorageItem from "../utils/encryptAESSecretStorageItem.ts"; -import { decryptAES } from "../utils/decryptAES.ts"; +import decryptAESSecretStorageItem from "../utils/decryptAESSecretStorageItem.ts"; // backwards-compatibility re-exports export { UserTrustLevel }; @@ -663,7 +663,7 @@ export function createCryptoStoreCacheCallbacks(store: CryptoStore, olmDevice: O if (key && key.ciphertext) { const pickleKey = Buffer.from(olmDevice.pickleKey); - const decrypted = await decryptAES(key, pickleKey, type); + const decrypted = await decryptAESSecretStorageItem(key, pickleKey, type); return decodeBase64(decrypted); } else { return key; diff --git a/src/crypto/aes.ts b/src/crypto/aes.ts index 732b20bd710..7ea79770095 100644 --- a/src/crypto/aes.ts +++ b/src/crypto/aes.ts @@ -15,9 +15,9 @@ limitations under the License. */ import encryptAESSecretStorageItem from "../utils/encryptAESSecretStorageItem.ts"; +import decryptAESSecretStorageItem from "../utils/decryptAESSecretStorageItem.ts"; // Export for backwards compatibility export type { AESEncryptedSecretStoragePayload as IEncryptedPayload } from "../@types/AESEncryptedSecretStoragePayload.ts"; -export { encryptAESSecretStorageItem }; -export { decryptAES } from "../utils/decryptAES.ts"; +export { encryptAESSecretStorageItem, decryptAESSecretStorageItem }; export { calculateKeyCheck } from "../secret-storage.ts"; diff --git a/src/crypto/backup.ts b/src/crypto/backup.ts index 18595d7a017..39bcaae6126 100644 --- a/src/crypto/backup.ts +++ b/src/crypto/backup.ts @@ -40,7 +40,7 @@ import { ClientPrefix, HTTPError, MatrixError, Method } from "../http-api/index. import { BackupTrustInfo } from "../crypto-api/keybackup.ts"; import { BackupDecryptor } from "../common-crypto/CryptoBackend.ts"; import { encodeRecoveryKey } from "../crypto-api/index.ts"; -import { decryptAES } from "../utils/decryptAES.ts"; +import decryptAESSecretStorageItem from "../utils/decryptAESSecretStorageItem.ts"; import encryptAESSecretStorageItem from "../utils/encryptAESSecretStorageItem.ts"; import { AESEncryptedSecretStoragePayload } from "../@types/AESEncryptedSecretStoragePayload.ts"; import { calculateKeyCheck } from "../secret-storage.ts"; @@ -843,7 +843,9 @@ export class Aes256 implements BackupAlgorithm { for (const [sessionId, sessionData] of Object.entries(sessions)) { 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; keys.push(decrypted); } catch (e) { diff --git a/src/crypto/dehydration.ts b/src/crypto/dehydration.ts index b9786c7d684..4cfc1193a0b 100644 --- a/src/crypto/dehydration.ts +++ b/src/crypto/dehydration.ts @@ -23,7 +23,7 @@ import { logger } from "../logger.ts"; import { Crypto } from "./index.ts"; import { Method } from "../http-api/index.ts"; import { SecretStorageKeyDescription } from "../secret-storage.ts"; -import { decryptAES } from "../utils/decryptAES.ts"; +import decryptAESSecretStorageItem from "../utils/decryptAESSecretStorageItem.ts"; import encryptAESSecretStorageItem from "../utils/encryptAESSecretStorageItem.ts"; export interface IDehydratedDevice { @@ -62,7 +62,7 @@ export class DehydrationManager { if (result) { const { key, keyInfo, deviceDisplayName, time } = result; 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.keyInfo = keyInfo; this.deviceDisplayName = deviceDisplayName; diff --git a/src/crypto/index.ts b/src/crypto/index.ts index b325638ebc2..9f4b3978187 100644 --- a/src/crypto/index.ts +++ b/src/crypto/index.ts @@ -107,7 +107,7 @@ import { deviceInfoToDevice } from "./device-converter.ts"; import { ClientPrefix, MatrixError, Method } from "../http-api/index.ts"; import { decodeBase64, encodeBase64 } from "../base64.ts"; import { KnownMembership } from "../@types/membership.ts"; -import { decryptAES } from "../utils/decryptAES.ts"; +import decryptAESSecretStorageItem from "../utils/decryptAESSecretStorageItem.ts"; import encryptAESSecretStorageItem from "../utils/encryptAESSecretStorageItem.ts"; import { AESEncryptedSecretStoragePayload } from "../@types/AESEncryptedSecretStoragePayload.ts"; @@ -1342,7 +1342,7 @@ export class Crypto extends TypedEventEmitter { - return decryptAES(encInfo, privateKey, name); + return decryptAESSecretStorageItem(encInfo, privateKey, name); }, }; return [keyId, decryption]; diff --git a/src/utils/decryptAES.ts b/src/utils/decryptAESSecretStorageItem.ts similarity index 94% rename from src/utils/decryptAES.ts rename to src/utils/decryptAESSecretStorageItem.ts index d9e63fb2104..eb783410d86 100644 --- a/src/utils/decryptAES.ts +++ b/src/utils/decryptAESSecretStorageItem.ts @@ -19,7 +19,7 @@ import { deriveKeys } from "./internal/deriveKeys.ts"; import { AESEncryptedSecretStoragePayload } from "../@types/AESEncryptedSecretStoragePayload.ts"; /** - * Decrypt an AES-encrypted string. + * Decrypt an AES-encrypted Secret Storage item. * * @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 @@ -27,7 +27,7 @@ import { AESEncryptedSecretStoragePayload } from "../@types/AESEncryptedSecretSt * @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( +export default async function decryptAESSecretStorageItem( data: AESEncryptedSecretStoragePayload, key: Uint8Array, name: string, From aeafe25ff6086bc5dfc03eabbde25b2ce302152c Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Fri, 27 Sep 2024 14:31:30 +0200 Subject: [PATCH 14/17] Update documentation --- README.md | 2 +- typedoc.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f0b4912fbf1..7a9146030c1 100644 --- a/README.md +++ b/README.md @@ -191,7 +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/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/utils` | Standalone functions and their types | +| `matrix-js-sdk/lib/utils/*.js` | A set of modules exporting standalone functions (and their types). | ## Examples diff --git a/typedoc.json b/typedoc.json index 69426dc7a3e..75b85b15bf8 100644 --- a/typedoc.json +++ b/typedoc.json @@ -2,7 +2,7 @@ "$schema": "https://typedoc.org/schema.json", "plugin": ["typedoc-plugin-mdn-links", "typedoc-plugin-missing-exports", "typedoc-plugin-coverage"], "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, "out": "_docs" } From 28f60befc2ad3e4f53e96600c3145627269c522a Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Fri, 27 Sep 2024 14:33:35 +0200 Subject: [PATCH 15/17] Update `decryptAESSecretStorageItem` doc --- src/utils/decryptAESSecretStorageItem.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/utils/decryptAESSecretStorageItem.ts b/src/utils/decryptAESSecretStorageItem.ts index eb783410d86..b48c16ee062 100644 --- a/src/utils/decryptAESSecretStorageItem.ts +++ b/src/utils/decryptAESSecretStorageItem.ts @@ -21,11 +21,11 @@ import { AESEncryptedSecretStoragePayload } from "../@types/AESEncryptedSecretSt /** * Decrypt an AES-encrypted Secret Storage item. * - * @param data - the encrypted data, returned by {@link encryptAES}. + * @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 encryptAES}. + * 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 encryptAES}. + * key, so again must be the same as provided to {@link utils/encryptAESSecretStorageItem.default | encryptAESSecretStorageItem}. */ export default async function decryptAESSecretStorageItem( data: AESEncryptedSecretStoragePayload, From 6fe09cfde8da8ec509e0f34040c4049cb6c06bb3 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Mon, 30 Sep 2024 18:31:31 +0200 Subject: [PATCH 16/17] Add lnk to spec for `calculateKeyCheck` --- src/secret-storage.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/secret-storage.ts b/src/secret-storage.ts index e23ff2710b4..13e8bdc625b 100644 --- a/src/secret-storage.ts +++ b/src/secret-storage.ts @@ -681,6 +681,7 @@ 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 /** * 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. From 25a34c16b34175b2220a64817f22b25276125638 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Tue, 1 Oct 2024 10:17:30 +0200 Subject: [PATCH 17/17] Fix downstream tests --- spec/unit/secret-storage.spec.ts | 2 +- src/calculateKeyCheck.ts | 34 ++++++++++++++++++++++++++++++++ src/crypto/aes.ts | 9 ++++++--- src/crypto/backup.ts | 2 +- src/crypto/index.ts | 2 +- src/secret-storage.ts | 17 +--------------- 6 files changed, 44 insertions(+), 22 deletions(-) create mode 100644 src/calculateKeyCheck.ts diff --git a/spec/unit/secret-storage.spec.ts b/spec/unit/secret-storage.spec.ts index b2346d88e6c..b71da45e2f8 100644 --- a/spec/unit/secret-storage.spec.ts +++ b/spec/unit/secret-storage.spec.ts @@ -18,7 +18,6 @@ import { Mocked } from "jest-mock"; import { AccountDataClient, - calculateKeyCheck, PassphraseInfo, SecretStorageCallbacks, SecretStorageKeyDescriptionAesV1, @@ -27,6 +26,7 @@ import { trimTrailingEquals, } from "../../src/secret-storage"; import { randomString } from "../../src/randomstring"; +import { calculateKeyCheck } from "../../src/calculateKeyCheck.ts"; describe("ServerSideSecretStorageImpl", function () { describe(".addKey", function () { diff --git a/src/calculateKeyCheck.ts b/src/calculateKeyCheck.ts new file mode 100644 index 00000000000..7e979a71263 --- /dev/null +++ b/src/calculateKeyCheck.ts @@ -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 { + return encryptAESSecretStorageItem(ZERO_STR, key, "", iv); +} diff --git a/src/crypto/aes.ts b/src/crypto/aes.ts index 7ea79770095..f435d8c15e5 100644 --- a/src/crypto/aes.ts +++ b/src/crypto/aes.ts @@ -16,8 +16,11 @@ limitations under the License. import encryptAESSecretStorageItem from "../utils/encryptAESSecretStorageItem.ts"; import decryptAESSecretStorageItem from "../utils/decryptAESSecretStorageItem.ts"; +import { AESEncryptedSecretStoragePayload } from "../@types/AESEncryptedSecretStoragePayload.ts"; // Export for backwards compatibility -export type { AESEncryptedSecretStoragePayload as IEncryptedPayload } from "../@types/AESEncryptedSecretStoragePayload.ts"; -export { encryptAESSecretStorageItem, decryptAESSecretStorageItem }; -export { calculateKeyCheck } from "../secret-storage.ts"; +export type { AESEncryptedSecretStoragePayload as IEncryptedPayload }; +// Export with new names instead of using `as` to not break react-sdk tests +export const encryptAES = encryptAESSecretStorageItem; +export const decryptAES = decryptAESSecretStorageItem; +export { calculateKeyCheck } from "../calculateKeyCheck.ts"; diff --git a/src/crypto/backup.ts b/src/crypto/backup.ts index 39bcaae6126..7caf069ab4a 100644 --- a/src/crypto/backup.ts +++ b/src/crypto/backup.ts @@ -43,7 +43,7 @@ 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 "../secret-storage.ts"; +import { calculateKeyCheck } from "../calculateKeyCheck.ts"; const KEY_BACKUP_KEYS_PER_REQUEST = 200; const KEY_BACKUP_CHECK_RATE_LIMIT = 5000; // ms diff --git a/src/crypto/index.ts b/src/crypto/index.ts index 9f4b3978187..932ce3adf48 100644 --- a/src/crypto/index.ts +++ b/src/crypto/index.ts @@ -75,7 +75,6 @@ import { MapWithDefault, recursiveMapToObject } from "../utils.ts"; import { AccountDataClient, AddSecretStorageKeyOpts, - calculateKeyCheck, SECRET_STORAGE_ALGORITHM_V1_AES, SecretStorageKeyDescription, SecretStorageKeyObject, @@ -110,6 +109,7 @@ 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 */ export type { diff --git a/src/secret-storage.ts b/src/secret-storage.ts index 13e8bdc625b..f192ddfd7f5 100644 --- a/src/secret-storage.ts +++ b/src/secret-storage.ts @@ -28,6 +28,7 @@ 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"; @@ -675,19 +676,3 @@ export function trimTrailingEquals(input: string): string { return input; } } - -// 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. - * 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 { - return encryptAESSecretStorageItem(ZERO_STR, key, "", iv); -}