Skip to content

Commit 5f3b899

Browse files
authored
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
1 parent 866fd6f commit 5f3b899

22 files changed

+329
-198
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@ As well as the primary entry point (`matrix-js-sdk`), there are several other en
191191
| `matrix-js-sdk/lib/crypto-api` | Cryptography functionality. |
192192
| `matrix-js-sdk/lib/types` | Low-level types, reflecting data structures defined in the Matrix spec. |
193193
| `matrix-js-sdk/lib/testing` | Test utilities, which may be useful in test code but should not be used in production code. |
194+
| `matrix-js-sdk/lib/utils/*.js` | A set of modules exporting standalone functions (and their types). |
194195

195196
## Examples
196197

spec/integ/crypto/cross-signing.spec.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import { IDBFactory } from "fake-indexeddb";
2121
import { CRYPTO_BACKENDS, InitCrypto, syncPromise } from "../../test-utils/test-utils";
2222
import { AuthDict, createClient, CryptoEvent, MatrixClient } from "../../../src";
2323
import { mockInitialApiRequests, mockSetupCrossSigningRequests } from "../../test-utils/mockEndpoints";
24-
import { encryptAES } from "../../../src/crypto/aes";
24+
import encryptAESSecretStorageItem from "../../../src/utils/encryptAESSecretStorageItem.ts";
2525
import { CryptoCallbacks, CrossSigningKey } from "../../../src/crypto-api";
2626
import { SECRET_STORAGE_ALGORITHM_V1_AES } from "../../../src/secret-storage";
2727
import { ISyncResponder, SyncResponder } from "../../test-utils/SyncResponder";
@@ -169,17 +169,17 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("cross-signing (%s)", (backend: s
169169
mockInitialApiRequests(aliceClient.getHomeserverUrl());
170170

171171
// Encrypt the private keys and return them in the /sync response as if they are in Secret Storage
172-
const masterKey = await encryptAES(
172+
const masterKey = await encryptAESSecretStorageItem(
173173
MASTER_CROSS_SIGNING_PRIVATE_KEY_BASE64,
174174
encryptionKey,
175175
"m.cross_signing.master",
176176
);
177-
const selfSigningKey = await encryptAES(
177+
const selfSigningKey = await encryptAESSecretStorageItem(
178178
SELF_CROSS_SIGNING_PRIVATE_KEY_BASE64,
179179
encryptionKey,
180180
"m.cross_signing.self_signing",
181181
);
182-
const userSigningKey = await encryptAES(
182+
const userSigningKey = await encryptAESSecretStorageItem(
183183
USER_CROSS_SIGNING_PRIVATE_KEY_BASE64,
184184
encryptionKey,
185185
"m.cross_signing.user_signing",

spec/unit/crypto/secrets.spec.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import { IObject } from "../../../src/crypto/olmlib";
2020
import { MatrixEvent } from "../../../src/models/event";
2121
import { TestClient } from "../../TestClient";
2222
import { makeTestClients } from "./verification/util";
23-
import { encryptAES } from "../../../src/crypto/aes";
23+
import encryptAESSecretStorageItem from "../../../src/utils/encryptAESSecretStorageItem.ts";
2424
import { createSecretStorageKey, resetCrossSigningKeys } from "./crypto-utils";
2525
import { logger } from "../../../src/logger";
2626
import { ClientEvent, ICreateClientOpts, MatrixClient } from "../../../src/client";
@@ -612,7 +612,7 @@ describe("Secrets", function () {
612612
type: "m.megolm_backup.v1",
613613
content: {
614614
encrypted: {
615-
key_id: await encryptAES(
615+
key_id: await encryptAESSecretStorageItem(
616616
"123,45,6,7,89,1,234,56,78,90,12,34,5,67,8,90",
617617
secretStorageKeys.key_id,
618618
"m.megolm_backup.v1",

spec/unit/rust-crypto/rust-crypto.spec.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ import { logger } from "../../../src/logger";
6969
import { OutgoingRequestsManager } from "../../../src/rust-crypto/OutgoingRequestsManager";
7070
import { ClientEvent, ClientEventHandlerMap } from "../../../src/client";
7171
import { Curve25519AuthData } from "../../../src/crypto-api/keybackup";
72-
import { encryptAES } from "../../../src/crypto/aes";
72+
import encryptAESSecretStorageItem from "../../../src/utils/encryptAESSecretStorageItem.ts";
7373
import { CryptoStore, SecretStorePrivateKeys } from "../../../src/crypto/store/base";
7474

7575
const TEST_USER = "@alice:example.com";
@@ -425,7 +425,7 @@ describe("initRustCrypto", () => {
425425
}, 10000);
426426

427427
async function encryptAndStoreSecretKey(type: string, key: Uint8Array, pickleKey: string, store: CryptoStore) {
428-
const encryptedKey = await encryptAES(encodeBase64(key), Buffer.from(pickleKey), type);
428+
const encryptedKey = await encryptAESSecretStorageItem(encodeBase64(key), Buffer.from(pickleKey), type);
429429
store.storeSecretStorePrivateKey(undefined, type as keyof SecretStorePrivateKeys, encryptedKey);
430430
}
431431

spec/unit/secret-storage.spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ import {
2525
ServerSideSecretStorageImpl,
2626
trimTrailingEquals,
2727
} from "../../src/secret-storage";
28-
import { calculateKeyCheck } from "../../src/crypto/aes";
2928
import { randomString } from "../../src/randomstring";
29+
import { calculateKeyCheck } from "../../src/calculateKeyCheck.ts";
3030

3131
describe("ServerSideSecretStorageImpl", function () {
3232
describe(".addKey", function () {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright 2024 The Matrix.org Foundation C.I.C.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
/**
18+
* An AES-encrypted secret storage payload.
19+
* See https://spec.matrix.org/v1.11/client-server-api/#msecret_storagev1aes-hmac-sha2-1
20+
*/
21+
export interface AESEncryptedSecretStoragePayload {
22+
[key: string]: any; // extensible
23+
/** the initialization vector in base64 */
24+
iv: string;
25+
/** the ciphertext in base64 */
26+
ciphertext: string;
27+
/** the HMAC in base64 */
28+
mac: string;
29+
}

src/calculateKeyCheck.ts

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright 2024 The Matrix.org Foundation C.I.C.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
// string of zeroes, for calculating the key check
18+
import encryptAESSecretStorageItem from "./utils/encryptAESSecretStorageItem.ts";
19+
import { AESEncryptedSecretStoragePayload } from "./@types/AESEncryptedSecretStoragePayload.ts";
20+
21+
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";
22+
23+
/**
24+
* Calculate the MAC for checking the key.
25+
* See https://spec.matrix.org/v1.11/client-server-api/#msecret_storagev1aes-hmac-sha2, steps 3 and 4.
26+
*
27+
* @param key - the key to use
28+
* @param iv - The initialization vector as a base64-encoded string.
29+
* If omitted, a random initialization vector will be created.
30+
* @returns An object that contains, `mac` and `iv` properties.
31+
*/
32+
export function calculateKeyCheck(key: Uint8Array, iv?: string): Promise<AESEncryptedSecretStoragePayload> {
33+
return encryptAESSecretStorageItem(ZERO_STR, key, "", iv);
34+
}

src/crypto-api/keybackup.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ limitations under the License.
1515
*/
1616

1717
import { ISigned } from "../@types/signed.ts";
18-
import { IEncryptedPayload } from "../crypto/aes.ts";
18+
import { AESEncryptedSecretStoragePayload } from "../@types/AESEncryptedSecretStoragePayload.ts";
1919

2020
export interface Curve25519AuthData {
2121
public_key: string;
@@ -77,7 +77,7 @@ export interface Curve25519SessionData {
7777
}
7878

7979
/* eslint-disable camelcase */
80-
export interface KeyBackupSession<T = Curve25519SessionData | IEncryptedPayload> {
80+
export interface KeyBackupSession<T = Curve25519SessionData | AESEncryptedSecretStoragePayload> {
8181
first_message_index: number;
8282
forwarded_count: number;
8383
is_verified: boolean;

src/crypto/CrossSigning.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import type { PkSigning } from "@matrix-org/olm";
2222
import { IObject, pkSign, pkVerify } from "./olmlib.ts";
2323
import { logger } from "../logger.ts";
2424
import { IndexedDBCryptoStore } from "../crypto/store/indexeddb-crypto-store.ts";
25-
import { decryptAES, encryptAES } from "./aes.ts";
2625
import { DeviceInfo } from "./deviceinfo.ts";
2726
import { ISignedKey, MatrixClient } from "../client.ts";
2827
import { OlmDevice } from "./OlmDevice.ts";
@@ -36,6 +35,8 @@ import {
3635
UserVerificationStatus as UserTrustLevel,
3736
} from "../crypto-api/index.ts";
3837
import { decodeBase64, encodeBase64 } from "../base64.ts";
38+
import encryptAESSecretStorageItem from "../utils/encryptAESSecretStorageItem.ts";
39+
import decryptAESSecretStorageItem from "../utils/decryptAESSecretStorageItem.ts";
3940

4041
// backwards-compatibility re-exports
4142
export { UserTrustLevel };
@@ -662,7 +663,7 @@ export function createCryptoStoreCacheCallbacks(store: CryptoStore, olmDevice: O
662663

663664
if (key && key.ciphertext) {
664665
const pickleKey = Buffer.from(olmDevice.pickleKey);
665-
const decrypted = await decryptAES(key, pickleKey, type);
666+
const decrypted = await decryptAESSecretStorageItem(key, pickleKey, type);
666667
return decodeBase64(decrypted);
667668
} else {
668669
return key;
@@ -676,7 +677,7 @@ export function createCryptoStoreCacheCallbacks(store: CryptoStore, olmDevice: O
676677
throw new Error(`storeCrossSigningKeyCache expects Uint8Array, got ${key}`);
677678
}
678679
const pickleKey = Buffer.from(olmDevice.pickleKey);
679-
const encryptedKey = await encryptAES(encodeBase64(key), pickleKey, type);
680+
const encryptedKey = await encryptAESSecretStorageItem(encodeBase64(key), pickleKey, type);
680681
return store.doTxn("readwrite", [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
681682
store.storeSecretStorePrivateKey(txn, type, encryptedKey);
682683
});

src/crypto/aes.ts

+10-150
Original file line numberDiff line numberDiff line change
@@ -14,153 +14,13 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17-
import { decodeBase64, encodeBase64 } from "../base64.ts";
18-
19-
// salt for HKDF, with 8 bytes of zeros
20-
const zeroSalt = new Uint8Array(8);
21-
22-
export interface IEncryptedPayload {
23-
[key: string]: any; // extensible
24-
/** the initialization vector in base64 */
25-
iv: string;
26-
/** the ciphertext in base64 */
27-
ciphertext: string;
28-
/** the HMAC in base64 */
29-
mac: string;
30-
}
31-
32-
/**
33-
* Encrypt a string using AES-CTR.
34-
*
35-
* @param data - the plaintext to encrypt
36-
* @param key - the encryption key to use as an input to the HKDF function which is used to derive the AES key for
37-
* encryption. Obviously, the same key must be provided when decrypting.
38-
* @param name - the name of the secret. Used as an input to the HKDF operation which is used to derive the AES key,
39-
* so again the same value must be provided when decrypting.
40-
* @param ivStr - the base64-encoded initialization vector to use. If not supplied, a random one will be generated.
41-
*
42-
* @returns The encrypted result, including the ciphertext itself, the initialization vector (as supplied in `ivStr`,
43-
* or generated), and an HMAC on the ciphertext — all base64-encoded.
44-
*/
45-
export async function encryptAES(
46-
data: string,
47-
key: Uint8Array,
48-
name: string,
49-
ivStr?: string,
50-
): Promise<IEncryptedPayload> {
51-
let iv: Uint8Array;
52-
if (ivStr) {
53-
iv = decodeBase64(ivStr);
54-
} else {
55-
iv = new Uint8Array(16);
56-
globalThis.crypto.getRandomValues(iv);
57-
58-
// clear bit 63 of the IV to stop us hitting the 64-bit counter boundary
59-
// (which would mean we wouldn't be able to decrypt on Android). The loss
60-
// of a single bit of iv is a price we have to pay.
61-
iv[8] &= 0x7f;
62-
}
63-
64-
const [aesKey, hmacKey] = await deriveKeys(key, name);
65-
const encodedData = new TextEncoder().encode(data);
66-
67-
const ciphertext = await globalThis.crypto.subtle.encrypt(
68-
{
69-
name: "AES-CTR",
70-
counter: iv,
71-
length: 64,
72-
},
73-
aesKey,
74-
encodedData,
75-
);
76-
77-
const hmac = await globalThis.crypto.subtle.sign({ name: "HMAC" }, hmacKey, ciphertext);
78-
79-
return {
80-
iv: encodeBase64(iv),
81-
ciphertext: encodeBase64(ciphertext),
82-
mac: encodeBase64(hmac),
83-
};
84-
}
85-
86-
/**
87-
* Decrypt an AES-encrypted string.
88-
*
89-
* @param data - the encrypted data, returned by {@link encryptAES}.
90-
* @param key - the encryption key to use as an input to the HKDF function which is used to derive the AES key. Must
91-
* be the same as provided to {@link encryptAES}.
92-
* @param name - the name of the secret. Also used as an input to the HKDF operation which is used to derive the AES
93-
* key, so again must be the same as provided to {@link encryptAES}.
94-
*/
95-
export async function decryptAES(data: IEncryptedPayload, key: Uint8Array, name: string): Promise<string> {
96-
const [aesKey, hmacKey] = await deriveKeys(key, name);
97-
98-
const ciphertext = decodeBase64(data.ciphertext);
99-
100-
if (!(await globalThis.crypto.subtle.verify({ name: "HMAC" }, hmacKey, decodeBase64(data.mac), ciphertext))) {
101-
throw new Error(`Error decrypting secret ${name}: bad MAC`);
102-
}
103-
104-
const plaintext = await globalThis.crypto.subtle.decrypt(
105-
{
106-
name: "AES-CTR",
107-
counter: decodeBase64(data.iv),
108-
length: 64,
109-
},
110-
aesKey,
111-
ciphertext,
112-
);
113-
114-
return new TextDecoder().decode(new Uint8Array(plaintext));
115-
}
116-
117-
async function deriveKeys(key: Uint8Array, name: string): Promise<[CryptoKey, CryptoKey]> {
118-
const hkdfkey = await globalThis.crypto.subtle.importKey("raw", key, { name: "HKDF" }, false, ["deriveBits"]);
119-
const keybits = await globalThis.crypto.subtle.deriveBits(
120-
{
121-
name: "HKDF",
122-
salt: zeroSalt,
123-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
124-
// @ts-ignore: https://github.com/microsoft/TypeScript-DOM-lib-generator/pull/879
125-
info: new TextEncoder().encode(name),
126-
hash: "SHA-256",
127-
},
128-
hkdfkey,
129-
512,
130-
);
131-
132-
const aesKey = keybits.slice(0, 32);
133-
const hmacKey = keybits.slice(32);
134-
135-
const aesProm = globalThis.crypto.subtle.importKey("raw", aesKey, { name: "AES-CTR" }, false, [
136-
"encrypt",
137-
"decrypt",
138-
]);
139-
140-
const hmacProm = globalThis.crypto.subtle.importKey(
141-
"raw",
142-
hmacKey,
143-
{
144-
name: "HMAC",
145-
hash: { name: "SHA-256" },
146-
},
147-
false,
148-
["sign", "verify"],
149-
);
150-
151-
return Promise.all([aesProm, hmacProm]);
152-
}
153-
154-
// string of zeroes, for calculating the key check
155-
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";
156-
157-
/** Calculate the MAC for checking the key.
158-
*
159-
* @param key - the key to use
160-
* @param iv - The initialization vector as a base64-encoded string.
161-
* If omitted, a random initialization vector will be created.
162-
* @returns An object that contains, `mac` and `iv` properties.
163-
*/
164-
export function calculateKeyCheck(key: Uint8Array, iv?: string): Promise<IEncryptedPayload> {
165-
return encryptAES(ZERO_STR, key, "", iv);
166-
}
17+
import encryptAESSecretStorageItem from "../utils/encryptAESSecretStorageItem.ts";
18+
import decryptAESSecretStorageItem from "../utils/decryptAESSecretStorageItem.ts";
19+
import { AESEncryptedSecretStoragePayload } from "../@types/AESEncryptedSecretStoragePayload.ts";
20+
21+
// Export for backwards compatibility
22+
export type { AESEncryptedSecretStoragePayload as IEncryptedPayload };
23+
// Export with new names instead of using `as` to not break react-sdk tests
24+
export const encryptAES = encryptAESSecretStorageItem;
25+
export const decryptAES = decryptAESSecretStorageItem;
26+
export { calculateKeyCheck } from "../calculateKeyCheck.ts";

0 commit comments

Comments
 (0)