Skip to content

Commit 30570bc

Browse files
authored
Remove node-specific crypto bits, use Node 16's WebCrypto (#2762)
1 parent 6af3b11 commit 30570bc

17 files changed

+177
-265
lines changed

spec/integ/megolm-integ.spec.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -1133,10 +1133,10 @@ describe("megolm", () => {
11331133
'readonly',
11341134
[IndexedDBCryptoStore.STORE_ACCOUNT],
11351135
(txn) => {
1136-
beccaTestClient.client.crypto!.cryptoStore.getAccount(txn, (pickledAccount: string) => {
1136+
beccaTestClient.client.crypto!.cryptoStore.getAccount(txn, (pickledAccount: string | null) => {
11371137
const account = new global.Olm.Account();
11381138
try {
1139-
account.unpickle(beccaTestClient.client.crypto!.olmDevice.pickleKey, pickledAccount);
1139+
account.unpickle(beccaTestClient.client.crypto!.olmDevice.pickleKey, pickledAccount!);
11401140
p2pSession.create_outbound(account, aliceTestClient.getDeviceKey(), aliceOtk.key);
11411141
} finally {
11421142
account.free();
@@ -1271,10 +1271,10 @@ describe("megolm", () => {
12711271
'readonly',
12721272
[IndexedDBCryptoStore.STORE_ACCOUNT],
12731273
(txn) => {
1274-
beccaTestClient.client.crypto!.cryptoStore.getAccount(txn, (pickledAccount: string) => {
1274+
beccaTestClient.client.crypto!.cryptoStore.getAccount(txn, (pickledAccount: string | null) => {
12751275
const account = new global.Olm.Account();
12761276
try {
1277-
account.unpickle(beccaTestClient.client.crypto!.olmDevice.pickleKey, pickledAccount);
1277+
account.unpickle(beccaTestClient.client.crypto!.olmDevice.pickleKey, pickledAccount!);
12781278
p2pSession.create_outbound(account, aliceTestClient.getDeviceKey(), aliceOtk.key);
12791279
} finally {
12801280
account.free();

spec/olm-loader.ts

-10
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ limitations under the License.
1616
*/
1717

1818
import { logger } from '../src/logger';
19-
import * as utils from "../src/utils";
2019

2120
// try to load the olm library.
2221
try {
@@ -26,12 +25,3 @@ try {
2625
} catch (e) {
2726
logger.warn("unable to run crypto tests: libolm not available");
2827
}
29-
30-
// also try to set node crypto
31-
try {
32-
// eslint-disable-next-line @typescript-eslint/no-var-requires
33-
const crypto = require('crypto');
34-
utils.setCrypto(crypto);
35-
} catch (err) {
36-
logger.log('nodejs was compiled without crypto support: some tests will fail');
37-
}

spec/unit/crypto/secrets.spec.ts

-9
Original file line numberDiff line numberDiff line change
@@ -23,19 +23,10 @@ import { makeTestClients } from './verification/util';
2323
import { encryptAES } from "../../../src/crypto/aes";
2424
import { resetCrossSigningKeys, createSecretStorageKey } from "./crypto-utils";
2525
import { logger } from '../../../src/logger';
26-
import * as utils from "../../../src/utils";
2726
import { ICreateClientOpts } from '../../../src/client';
2827
import { ISecretStorageKeyInfo } from '../../../src/crypto/api';
2928
import { DeviceInfo } from '../../../src/crypto/deviceinfo';
3029

31-
try {
32-
// eslint-disable-next-line @typescript-eslint/no-var-requires
33-
const crypto = require('crypto');
34-
utils.setCrypto(crypto);
35-
} catch (err) {
36-
logger.log('nodejs was compiled without crypto support');
37-
}
38-
3930
async function makeTestClient(userInfo: { userId: string, deviceId: string}, options: Partial<ICreateClientOpts> = {}) {
4031
const client = (new TestClient(
4132
userInfo.userId, userInfo.deviceId, undefined, undefined, options,

src/crypto/CrossSigning.ts

+9-3
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import { ICrossSigningKey, ISignedKey, MatrixClient } from "../client";
3131
import { OlmDevice } from "./OlmDevice";
3232
import { ICryptoCallbacks } from "../matrix";
3333
import { ISignatures } from "../@types/signed";
34-
import { CryptoStore } from "./store/base";
34+
import { CryptoStore, SecretStorePrivateKeys } from "./store/base";
3535
import { ISecretStorageKeyInfo } from "./api";
3636

3737
const KEY_REQUEST_TIMEOUT_MS = 1000 * 60;
@@ -699,7 +699,10 @@ export class DeviceTrustLevel {
699699

700700
export function createCryptoStoreCacheCallbacks(store: CryptoStore, olmDevice: OlmDevice): ICacheCallbacks {
701701
return {
702-
getCrossSigningKeyCache: async function(type: string, _expectedPublicKey: string): Promise<Uint8Array> {
702+
getCrossSigningKeyCache: async function(
703+
type: keyof SecretStorePrivateKeys,
704+
_expectedPublicKey: string,
705+
): Promise<Uint8Array> {
703706
const key = await new Promise<any>((resolve) => {
704707
return store.doTxn(
705708
'readonly',
@@ -718,7 +721,10 @@ export function createCryptoStoreCacheCallbacks(store: CryptoStore, olmDevice: O
718721
return key;
719722
}
720723
},
721-
storeCrossSigningKeyCache: async function(type: string, key: Uint8Array): Promise<void> {
724+
storeCrossSigningKeyCache: async function(
725+
type: keyof SecretStorePrivateKeys,
726+
key: Uint8Array,
727+
): Promise<void> {
722728
if (!(key instanceof Uint8Array)) {
723729
throw new Error(
724730
`storeCrossSigningKeyCache expects Uint8Array, got ${key}`,

src/crypto/OlmDevice.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -308,10 +308,10 @@ export class OlmDevice {
308308
* @private
309309
*/
310310
private getAccount(txn: unknown, func: (account: Account) => void): void {
311-
this.cryptoStore.getAccount(txn, (pickledAccount: string) => {
311+
this.cryptoStore.getAccount(txn, (pickledAccount: string | null) => {
312312
const account = new global.Olm.Account();
313313
try {
314-
account.unpickle(this.pickleKey, pickledAccount);
314+
account.unpickle(this.pickleKey, pickledAccount!);
315315
func(account);
316316
} finally {
317317
account.free();
@@ -350,8 +350,8 @@ export class OlmDevice {
350350
IndexedDBCryptoStore.STORE_SESSIONS,
351351
],
352352
(txn) => {
353-
this.cryptoStore.getAccount(txn, (pickledAccount: string) => {
354-
result.pickledAccount = pickledAccount;
353+
this.cryptoStore.getAccount(txn, (pickledAccount: string | null) => {
354+
result.pickledAccount = pickledAccount!;
355355
});
356356
result.sessions = [];
357357
// Note that the pickledSession object we get in the callback

src/crypto/aes.ts

+18-116
Original file line numberDiff line numberDiff line change
@@ -14,137 +14,47 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17-
import type { BinaryLike } from "crypto";
18-
import { getCrypto } from '../utils';
1917
import { decodeBase64, encodeBase64 } from './olmlib';
20-
21-
const subtleCrypto = (typeof window !== "undefined" && window.crypto) ?
22-
(window.crypto.subtle || window.crypto.webkitSubtle) : null;
18+
import { subtleCrypto, crypto, TextEncoder } from "./crypto";
2319

2420
// salt for HKDF, with 8 bytes of zeros
2521
const zeroSalt = new Uint8Array(8);
2622

2723
export interface IEncryptedPayload {
2824
[key: string]: any; // extensible
29-
iv?: string;
30-
ciphertext?: string;
31-
mac?: string;
25+
iv: string;
26+
ciphertext: string;
27+
mac: string;
3228
}
3329

3430
/**
35-
* encrypt a string in Node.js
31+
* encrypt a string
3632
*
3733
* @param {string} data the plaintext to encrypt
3834
* @param {Uint8Array} key the encryption key to use
3935
* @param {string} name the name of the secret
4036
* @param {string} ivStr the initialization vector to use
4137
*/
42-
async function encryptNode(data: string, key: Uint8Array, name: string, ivStr?: string): Promise<IEncryptedPayload> {
43-
const crypto = getCrypto();
44-
if (!crypto) {
45-
throw new Error("No usable crypto implementation");
46-
}
47-
48-
let iv;
49-
if (ivStr) {
50-
iv = decodeBase64(ivStr);
51-
} else {
52-
iv = crypto.randomBytes(16);
53-
54-
// clear bit 63 of the IV to stop us hitting the 64-bit counter boundary
55-
// (which would mean we wouldn't be able to decrypt on Android). The loss
56-
// of a single bit of iv is a price we have to pay.
57-
iv[8] &= 0x7f;
58-
}
59-
60-
const [aesKey, hmacKey] = deriveKeysNode(key, name);
61-
62-
const cipher = crypto.createCipheriv("aes-256-ctr", aesKey, iv);
63-
const ciphertext = Buffer.concat([
64-
cipher.update(data, "utf8"),
65-
cipher.final(),
66-
]);
67-
68-
const hmac = crypto.createHmac("sha256", hmacKey)
69-
.update(ciphertext).digest("base64");
70-
71-
return {
72-
iv: encodeBase64(iv),
73-
ciphertext: ciphertext.toString("base64"),
74-
mac: hmac,
75-
};
76-
}
77-
78-
/**
79-
* decrypt a string in Node.js
80-
*
81-
* @param {object} data the encrypted data
82-
* @param {string} data.ciphertext the ciphertext in base64
83-
* @param {string} data.iv the initialization vector in base64
84-
* @param {string} data.mac the HMAC in base64
85-
* @param {Uint8Array} key the encryption key to use
86-
* @param {string} name the name of the secret
87-
*/
88-
async function decryptNode(data: IEncryptedPayload, key: Uint8Array, name: string): Promise<string> {
89-
const crypto = getCrypto();
90-
if (!crypto) {
91-
throw new Error("No usable crypto implementation");
92-
}
93-
94-
const [aesKey, hmacKey] = deriveKeysNode(key, name);
95-
96-
const hmac = crypto.createHmac("sha256", hmacKey)
97-
.update(Buffer.from(data.ciphertext, "base64"))
98-
.digest("base64").replace(/=+$/g, '');
99-
100-
if (hmac !== data.mac.replace(/=+$/g, '')) {
101-
throw new Error(`Error decrypting secret ${name}: bad MAC`);
102-
}
103-
104-
const decipher = crypto.createDecipheriv(
105-
"aes-256-ctr", aesKey, decodeBase64(data.iv),
106-
);
107-
return decipher.update(data.ciphertext, "base64", "utf8")
108-
+ decipher.final("utf8");
109-
}
110-
111-
function deriveKeysNode(key: BinaryLike, name: string): [Buffer, Buffer] {
112-
const crypto = getCrypto();
113-
const prk = crypto.createHmac("sha256", zeroSalt).update(key).digest();
114-
115-
const b = Buffer.alloc(1, 1);
116-
const aesKey = crypto.createHmac("sha256", prk)
117-
.update(name, "utf8").update(b).digest();
118-
b[0] = 2;
119-
const hmacKey = crypto.createHmac("sha256", prk)
120-
.update(aesKey).update(name, "utf8").update(b).digest();
121-
122-
return [aesKey, hmacKey];
123-
}
124-
125-
/**
126-
* encrypt a string in Node.js
127-
*
128-
* @param {string} data the plaintext to encrypt
129-
* @param {Uint8Array} key the encryption key to use
130-
* @param {string} name the name of the secret
131-
* @param {string} ivStr the initialization vector to use
132-
*/
133-
async function encryptBrowser(data: string, key: Uint8Array, name: string, ivStr?: string): Promise<IEncryptedPayload> {
134-
let iv;
38+
export async function encryptAES(
39+
data: string,
40+
key: Uint8Array,
41+
name: string,
42+
ivStr?: string,
43+
): Promise<IEncryptedPayload> {
44+
let iv: Uint8Array;
13545
if (ivStr) {
13646
iv = decodeBase64(ivStr);
13747
} else {
13848
iv = new Uint8Array(16);
139-
window.crypto.getRandomValues(iv);
49+
crypto.getRandomValues(iv);
14050

14151
// clear bit 63 of the IV to stop us hitting the 64-bit counter boundary
14252
// (which would mean we wouldn't be able to decrypt on Android). The loss
14353
// of a single bit of iv is a price we have to pay.
14454
iv[8] &= 0x7f;
14555
}
14656

147-
const [aesKey, hmacKey] = await deriveKeysBrowser(key, name);
57+
const [aesKey, hmacKey] = await deriveKeys(key, name);
14858
const encodedData = new TextEncoder().encode(data);
14959

15060
const ciphertext = await subtleCrypto.encrypt(
@@ -171,7 +81,7 @@ async function encryptBrowser(data: string, key: Uint8Array, name: string, ivStr
17181
}
17282

17383
/**
174-
* decrypt a string in the browser
84+
* decrypt a string
17585
*
17686
* @param {object} data the encrypted data
17787
* @param {string} data.ciphertext the ciphertext in base64
@@ -180,8 +90,8 @@ async function encryptBrowser(data: string, key: Uint8Array, name: string, ivStr
18090
* @param {Uint8Array} key the encryption key to use
18191
* @param {string} name the name of the secret
18292
*/
183-
async function decryptBrowser(data: IEncryptedPayload, key: Uint8Array, name: string): Promise<string> {
184-
const [aesKey, hmacKey] = await deriveKeysBrowser(key, name);
93+
export async function decryptAES(data: IEncryptedPayload, key: Uint8Array, name: string): Promise<string> {
94+
const [aesKey, hmacKey] = await deriveKeys(key, name);
18595

18696
const ciphertext = decodeBase64(data.ciphertext);
18797

@@ -207,7 +117,7 @@ async function decryptBrowser(data: IEncryptedPayload, key: Uint8Array, name: st
207117
return new TextDecoder().decode(new Uint8Array(plaintext));
208118
}
209119

210-
async function deriveKeysBrowser(key: Uint8Array, name: string): Promise<[CryptoKey, CryptoKey]> {
120+
async function deriveKeys(key: Uint8Array, name: string): Promise<[CryptoKey, CryptoKey]> {
211121
const hkdfkey = await subtleCrypto.importKey(
212122
'raw',
213123
key,
@@ -253,14 +163,6 @@ async function deriveKeysBrowser(key: Uint8Array, name: string): Promise<[Crypto
253163
return Promise.all([aesProm, hmacProm]);
254164
}
255165

256-
export function encryptAES(data: string, key: Uint8Array, name: string, ivStr?: string): Promise<IEncryptedPayload> {
257-
return subtleCrypto ? encryptBrowser(data, key, name, ivStr) : encryptNode(data, key, name, ivStr);
258-
}
259-
260-
export function decryptAES(data: IEncryptedPayload, key: Uint8Array, name: string): Promise<string> {
261-
return subtleCrypto ? decryptBrowser(data, key, name) : decryptNode(data, key, name);
262-
}
263-
264166
// string of zeroes, for calculating the key check
265167
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";
266168

src/crypto/backup.ts

+20-18
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,20 @@ import { MEGOLM_ALGORITHM, verifySignature } from "./olmlib";
2626
import { DeviceInfo } from "./deviceinfo";
2727
import { DeviceTrustLevel } from './CrossSigning';
2828
import { keyFromPassphrase } from './key_passphrase';
29-
import { getCrypto, sleep } from "../utils";
29+
import { sleep } from "../utils";
3030
import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
3131
import { encodeRecoveryKey } from './recoverykey';
32-
import { calculateKeyCheck, decryptAES, encryptAES } from './aes';
33-
import { IAes256AuthData, ICurve25519AuthData, IKeyBackupInfo, IKeyBackupSession } from "./keybackup";
32+
import { calculateKeyCheck, decryptAES, encryptAES, IEncryptedPayload } from './aes';
33+
import {
34+
Curve25519SessionData,
35+
IAes256AuthData,
36+
ICurve25519AuthData,
37+
IKeyBackupInfo,
38+
IKeyBackupSession,
39+
} from "./keybackup";
3440
import { UnstableValue } from "../NamespacedValue";
3541
import { CryptoEvent, IMegolmSessionData } from "./index";
42+
import { crypto } from "./crypto";
3643

3744
const KEY_BACKUP_KEYS_PER_REQUEST = 200;
3845
const KEY_BACKUP_CHECK_RATE_LIMIT = 5000; // ms
@@ -677,7 +684,9 @@ export class Curve25519 implements BackupAlgorithm {
677684
return this.publicKey.encrypt(JSON.stringify(plainText));
678685
}
679686

680-
public async decryptSessions(sessions: Record<string, IKeyBackupSession>): Promise<IMegolmSessionData[]> {
687+
public async decryptSessions(
688+
sessions: Record<string, IKeyBackupSession<Curve25519SessionData>>,
689+
): Promise<IMegolmSessionData[]> {
681690
const privKey = await this.getKey();
682691
const decryption = new global.Olm.PkDecryption();
683692
try {
@@ -711,7 +720,7 @@ export class Curve25519 implements BackupAlgorithm {
711720

712721
public async keyMatches(key: Uint8Array): Promise<boolean> {
713722
const decryption = new global.Olm.PkDecryption();
714-
let pubKey;
723+
let pubKey: string;
715724
try {
716725
pubKey = decryption.init_with_private_key(key);
717726
} finally {
@@ -727,18 +736,9 @@ export class Curve25519 implements BackupAlgorithm {
727736
}
728737

729738
function randomBytes(size: number): Uint8Array {
730-
const crypto: {randomBytes: (n: number) => Uint8Array} | undefined = getCrypto() as any;
731-
if (crypto) {
732-
// nodejs version
733-
return crypto.randomBytes(size);
734-
}
735-
if (window?.crypto) {
736-
// browser version
737-
const buf = new Uint8Array(size);
738-
window.crypto.getRandomValues(buf);
739-
return buf;
740-
}
741-
throw new Error("No usable crypto implementation");
739+
const buf = new Uint8Array(size);
740+
crypto.getRandomValues(buf);
741+
return buf;
742742
}
743743

744744
const UNSTABLE_MSC3270_NAME = new UnstableValue(null, "org.matrix.msc3270.v1.aes-hmac-sha2");
@@ -807,7 +807,9 @@ export class Aes256 implements BackupAlgorithm {
807807
return encryptAES(JSON.stringify(plainText), this.key, data.session_id);
808808
}
809809

810-
public async decryptSessions(sessions: Record<string, IKeyBackupSession>): Promise<IMegolmSessionData[]> {
810+
public async decryptSessions(
811+
sessions: Record<string, IKeyBackupSession<IEncryptedPayload>>,
812+
): Promise<IMegolmSessionData[]> {
811813
const keys: IMegolmSessionData[] = [];
812814

813815
for (const [sessionId, sessionData] of Object.entries(sessions)) {

0 commit comments

Comments
 (0)