1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-08-06 12:02:40 +03:00

Remove node-specific crypto bits, use Node 16's WebCrypto (#2762)

This commit is contained in:
Michael Telatynski
2022-10-17 17:54:54 +01:00
committed by GitHub
parent 6af3b114e1
commit 30570bcce6
17 changed files with 177 additions and 265 deletions

View File

@@ -1133,10 +1133,10 @@ describe("megolm", () => {
'readonly',
[IndexedDBCryptoStore.STORE_ACCOUNT],
(txn) => {
beccaTestClient.client.crypto!.cryptoStore.getAccount(txn, (pickledAccount: string) => {
beccaTestClient.client.crypto!.cryptoStore.getAccount(txn, (pickledAccount: string | null) => {
const account = new global.Olm.Account();
try {
account.unpickle(beccaTestClient.client.crypto!.olmDevice.pickleKey, pickledAccount);
account.unpickle(beccaTestClient.client.crypto!.olmDevice.pickleKey, pickledAccount!);
p2pSession.create_outbound(account, aliceTestClient.getDeviceKey(), aliceOtk.key);
} finally {
account.free();
@@ -1271,10 +1271,10 @@ describe("megolm", () => {
'readonly',
[IndexedDBCryptoStore.STORE_ACCOUNT],
(txn) => {
beccaTestClient.client.crypto!.cryptoStore.getAccount(txn, (pickledAccount: string) => {
beccaTestClient.client.crypto!.cryptoStore.getAccount(txn, (pickledAccount: string | null) => {
const account = new global.Olm.Account();
try {
account.unpickle(beccaTestClient.client.crypto!.olmDevice.pickleKey, pickledAccount);
account.unpickle(beccaTestClient.client.crypto!.olmDevice.pickleKey, pickledAccount!);
p2pSession.create_outbound(account, aliceTestClient.getDeviceKey(), aliceOtk.key);
} finally {
account.free();

View File

@@ -16,7 +16,6 @@ limitations under the License.
*/
import { logger } from '../src/logger';
import * as utils from "../src/utils";
// try to load the olm library.
try {
@@ -26,12 +25,3 @@ try {
} catch (e) {
logger.warn("unable to run crypto tests: libolm not available");
}
// also try to set node crypto
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const crypto = require('crypto');
utils.setCrypto(crypto);
} catch (err) {
logger.log('nodejs was compiled without crypto support: some tests will fail');
}

View File

@@ -23,19 +23,10 @@ import { makeTestClients } from './verification/util';
import { encryptAES } from "../../../src/crypto/aes";
import { resetCrossSigningKeys, createSecretStorageKey } from "./crypto-utils";
import { logger } from '../../../src/logger';
import * as utils from "../../../src/utils";
import { ICreateClientOpts } from '../../../src/client';
import { ISecretStorageKeyInfo } from '../../../src/crypto/api';
import { DeviceInfo } from '../../../src/crypto/deviceinfo';
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const crypto = require('crypto');
utils.setCrypto(crypto);
} catch (err) {
logger.log('nodejs was compiled without crypto support');
}
async function makeTestClient(userInfo: { userId: string, deviceId: string}, options: Partial<ICreateClientOpts> = {}) {
const client = (new TestClient(
userInfo.userId, userInfo.deviceId, undefined, undefined, options,

View File

@@ -31,7 +31,7 @@ import { ICrossSigningKey, ISignedKey, MatrixClient } from "../client";
import { OlmDevice } from "./OlmDevice";
import { ICryptoCallbacks } from "../matrix";
import { ISignatures } from "../@types/signed";
import { CryptoStore } from "./store/base";
import { CryptoStore, SecretStorePrivateKeys } from "./store/base";
import { ISecretStorageKeyInfo } from "./api";
const KEY_REQUEST_TIMEOUT_MS = 1000 * 60;
@@ -699,7 +699,10 @@ export class DeviceTrustLevel {
export function createCryptoStoreCacheCallbacks(store: CryptoStore, olmDevice: OlmDevice): ICacheCallbacks {
return {
getCrossSigningKeyCache: async function(type: string, _expectedPublicKey: string): Promise<Uint8Array> {
getCrossSigningKeyCache: async function(
type: keyof SecretStorePrivateKeys,
_expectedPublicKey: string,
): Promise<Uint8Array> {
const key = await new Promise<any>((resolve) => {
return store.doTxn(
'readonly',
@@ -718,7 +721,10 @@ export function createCryptoStoreCacheCallbacks(store: CryptoStore, olmDevice: O
return key;
}
},
storeCrossSigningKeyCache: async function(type: string, key: Uint8Array): Promise<void> {
storeCrossSigningKeyCache: async function(
type: keyof SecretStorePrivateKeys,
key: Uint8Array,
): Promise<void> {
if (!(key instanceof Uint8Array)) {
throw new Error(
`storeCrossSigningKeyCache expects Uint8Array, got ${key}`,

View File

@@ -308,10 +308,10 @@ export class OlmDevice {
* @private
*/
private getAccount(txn: unknown, func: (account: Account) => void): void {
this.cryptoStore.getAccount(txn, (pickledAccount: string) => {
this.cryptoStore.getAccount(txn, (pickledAccount: string | null) => {
const account = new global.Olm.Account();
try {
account.unpickle(this.pickleKey, pickledAccount);
account.unpickle(this.pickleKey, pickledAccount!);
func(account);
} finally {
account.free();
@@ -350,8 +350,8 @@ export class OlmDevice {
IndexedDBCryptoStore.STORE_SESSIONS,
],
(txn) => {
this.cryptoStore.getAccount(txn, (pickledAccount: string) => {
result.pickledAccount = pickledAccount;
this.cryptoStore.getAccount(txn, (pickledAccount: string | null) => {
result.pickledAccount = pickledAccount!;
});
result.sessions = [];
// Note that the pickledSession object we get in the callback

View File

@@ -14,129 +14,39 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import type { BinaryLike } from "crypto";
import { getCrypto } from '../utils';
import { decodeBase64, encodeBase64 } from './olmlib';
const subtleCrypto = (typeof window !== "undefined" && window.crypto) ?
(window.crypto.subtle || window.crypto.webkitSubtle) : null;
import { subtleCrypto, crypto, TextEncoder } from "./crypto";
// salt for HKDF, with 8 bytes of zeros
const zeroSalt = new Uint8Array(8);
export interface IEncryptedPayload {
[key: string]: any; // extensible
iv?: string;
ciphertext?: string;
mac?: string;
iv: string;
ciphertext: string;
mac: string;
}
/**
* encrypt a string in Node.js
* encrypt a string
*
* @param {string} data the plaintext to encrypt
* @param {Uint8Array} key the encryption key to use
* @param {string} name the name of the secret
* @param {string} ivStr the initialization vector to use
*/
async function encryptNode(data: string, key: Uint8Array, name: string, ivStr?: string): Promise<IEncryptedPayload> {
const crypto = getCrypto();
if (!crypto) {
throw new Error("No usable crypto implementation");
}
let iv;
if (ivStr) {
iv = decodeBase64(ivStr);
} else {
iv = crypto.randomBytes(16);
// 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] = deriveKeysNode(key, name);
const cipher = crypto.createCipheriv("aes-256-ctr", aesKey, iv);
const ciphertext = Buffer.concat([
cipher.update(data, "utf8"),
cipher.final(),
]);
const hmac = crypto.createHmac("sha256", hmacKey)
.update(ciphertext).digest("base64");
return {
iv: encodeBase64(iv),
ciphertext: ciphertext.toString("base64"),
mac: hmac,
};
}
/**
* decrypt a string in Node.js
*
* @param {object} data the encrypted data
* @param {string} data.ciphertext the ciphertext in base64
* @param {string} data.iv the initialization vector in base64
* @param {string} data.mac the HMAC in base64
* @param {Uint8Array} key the encryption key to use
* @param {string} name the name of the secret
*/
async function decryptNode(data: IEncryptedPayload, key: Uint8Array, name: string): Promise<string> {
const crypto = getCrypto();
if (!crypto) {
throw new Error("No usable crypto implementation");
}
const [aesKey, hmacKey] = deriveKeysNode(key, name);
const hmac = crypto.createHmac("sha256", hmacKey)
.update(Buffer.from(data.ciphertext, "base64"))
.digest("base64").replace(/=+$/g, '');
if (hmac !== data.mac.replace(/=+$/g, '')) {
throw new Error(`Error decrypting secret ${name}: bad MAC`);
}
const decipher = crypto.createDecipheriv(
"aes-256-ctr", aesKey, decodeBase64(data.iv),
);
return decipher.update(data.ciphertext, "base64", "utf8")
+ decipher.final("utf8");
}
function deriveKeysNode(key: BinaryLike, name: string): [Buffer, Buffer] {
const crypto = getCrypto();
const prk = crypto.createHmac("sha256", zeroSalt).update(key).digest();
const b = Buffer.alloc(1, 1);
const aesKey = crypto.createHmac("sha256", prk)
.update(name, "utf8").update(b).digest();
b[0] = 2;
const hmacKey = crypto.createHmac("sha256", prk)
.update(aesKey).update(name, "utf8").update(b).digest();
return [aesKey, hmacKey];
}
/**
* encrypt a string in Node.js
*
* @param {string} data the plaintext to encrypt
* @param {Uint8Array} key the encryption key to use
* @param {string} name the name of the secret
* @param {string} ivStr the initialization vector to use
*/
async function encryptBrowser(data: string, key: Uint8Array, name: string, ivStr?: string): Promise<IEncryptedPayload> {
let iv;
export async function encryptAES(
data: string,
key: Uint8Array,
name: string,
ivStr?: string,
): Promise<IEncryptedPayload> {
let iv: Uint8Array;
if (ivStr) {
iv = decodeBase64(ivStr);
} else {
iv = new Uint8Array(16);
window.crypto.getRandomValues(iv);
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
@@ -144,7 +54,7 @@ async function encryptBrowser(data: string, key: Uint8Array, name: string, ivStr
iv[8] &= 0x7f;
}
const [aesKey, hmacKey] = await deriveKeysBrowser(key, name);
const [aesKey, hmacKey] = await deriveKeys(key, name);
const encodedData = new TextEncoder().encode(data);
const ciphertext = await subtleCrypto.encrypt(
@@ -171,7 +81,7 @@ async function encryptBrowser(data: string, key: Uint8Array, name: string, ivStr
}
/**
* decrypt a string in the browser
* decrypt a string
*
* @param {object} data the encrypted data
* @param {string} data.ciphertext the ciphertext in base64
@@ -180,8 +90,8 @@ async function encryptBrowser(data: string, key: Uint8Array, name: string, ivStr
* @param {Uint8Array} key the encryption key to use
* @param {string} name the name of the secret
*/
async function decryptBrowser(data: IEncryptedPayload, key: Uint8Array, name: string): Promise<string> {
const [aesKey, hmacKey] = await deriveKeysBrowser(key, name);
export async function decryptAES(data: IEncryptedPayload, key: Uint8Array, name: string): Promise<string> {
const [aesKey, hmacKey] = await deriveKeys(key, name);
const ciphertext = decodeBase64(data.ciphertext);
@@ -207,7 +117,7 @@ async function decryptBrowser(data: IEncryptedPayload, key: Uint8Array, name: st
return new TextDecoder().decode(new Uint8Array(plaintext));
}
async function deriveKeysBrowser(key: Uint8Array, name: string): Promise<[CryptoKey, CryptoKey]> {
async function deriveKeys(key: Uint8Array, name: string): Promise<[CryptoKey, CryptoKey]> {
const hkdfkey = await subtleCrypto.importKey(
'raw',
key,
@@ -253,14 +163,6 @@ async function deriveKeysBrowser(key: Uint8Array, name: string): Promise<[Crypto
return Promise.all([aesProm, hmacProm]);
}
export function encryptAES(data: string, key: Uint8Array, name: string, ivStr?: string): Promise<IEncryptedPayload> {
return subtleCrypto ? encryptBrowser(data, key, name, ivStr) : encryptNode(data, key, name, ivStr);
}
export function decryptAES(data: IEncryptedPayload, key: Uint8Array, name: string): Promise<string> {
return subtleCrypto ? decryptBrowser(data, key, name) : decryptNode(data, key, name);
}
// 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";

View File

@@ -26,13 +26,20 @@ import { MEGOLM_ALGORITHM, verifySignature } from "./olmlib";
import { DeviceInfo } from "./deviceinfo";
import { DeviceTrustLevel } from './CrossSigning';
import { keyFromPassphrase } from './key_passphrase';
import { getCrypto, sleep } from "../utils";
import { sleep } from "../utils";
import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
import { encodeRecoveryKey } from './recoverykey';
import { calculateKeyCheck, decryptAES, encryptAES } from './aes';
import { IAes256AuthData, ICurve25519AuthData, IKeyBackupInfo, IKeyBackupSession } from "./keybackup";
import { calculateKeyCheck, decryptAES, encryptAES, IEncryptedPayload } from './aes';
import {
Curve25519SessionData,
IAes256AuthData,
ICurve25519AuthData,
IKeyBackupInfo,
IKeyBackupSession,
} from "./keybackup";
import { UnstableValue } from "../NamespacedValue";
import { CryptoEvent, IMegolmSessionData } from "./index";
import { crypto } from "./crypto";
const KEY_BACKUP_KEYS_PER_REQUEST = 200;
const KEY_BACKUP_CHECK_RATE_LIMIT = 5000; // ms
@@ -677,7 +684,9 @@ export class Curve25519 implements BackupAlgorithm {
return this.publicKey.encrypt(JSON.stringify(plainText));
}
public async decryptSessions(sessions: Record<string, IKeyBackupSession>): Promise<IMegolmSessionData[]> {
public async decryptSessions(
sessions: Record<string, IKeyBackupSession<Curve25519SessionData>>,
): Promise<IMegolmSessionData[]> {
const privKey = await this.getKey();
const decryption = new global.Olm.PkDecryption();
try {
@@ -711,7 +720,7 @@ export class Curve25519 implements BackupAlgorithm {
public async keyMatches(key: Uint8Array): Promise<boolean> {
const decryption = new global.Olm.PkDecryption();
let pubKey;
let pubKey: string;
try {
pubKey = decryption.init_with_private_key(key);
} finally {
@@ -727,18 +736,9 @@ export class Curve25519 implements BackupAlgorithm {
}
function randomBytes(size: number): Uint8Array {
const crypto: {randomBytes: (n: number) => Uint8Array} | undefined = getCrypto() as any;
if (crypto) {
// nodejs version
return crypto.randomBytes(size);
}
if (window?.crypto) {
// browser version
const buf = new Uint8Array(size);
window.crypto.getRandomValues(buf);
crypto.getRandomValues(buf);
return buf;
}
throw new Error("No usable crypto implementation");
}
const UNSTABLE_MSC3270_NAME = new UnstableValue(null, "org.matrix.msc3270.v1.aes-hmac-sha2");
@@ -807,7 +807,9 @@ export class Aes256 implements BackupAlgorithm {
return encryptAES(JSON.stringify(plainText), this.key, data.session_id);
}
public async decryptSessions(sessions: Record<string, IKeyBackupSession>): Promise<IMegolmSessionData[]> {
public async decryptSessions(
sessions: Record<string, IKeyBackupSession<IEncryptedPayload>>,
): Promise<IMegolmSessionData[]> {
const keys: IMegolmSessionData[] = [];
for (const [sessionId, sessionData] of Object.entries(sessions)) {

31
src/crypto/crypto.ts Normal file
View File

@@ -0,0 +1,31 @@
/*
Copyright 2022 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.
*/
export let crypto = global.window?.crypto;
export let subtleCrypto = global.window?.crypto?.subtle ?? global.window?.crypto?.webkitSubtle;
export let TextEncoder = global.window?.TextEncoder;
/* eslint-disable @typescript-eslint/no-var-requires */
if (!crypto) {
crypto = require("crypto").webcrypto;
}
if (!subtleCrypto) {
subtleCrypto = crypto?.subtle;
}
if (!TextEncoder) {
TextEncoder = require("util").TextEncoder;
}
/* eslint-enable @typescript-eslint/no-var-requires */

View File

@@ -15,10 +15,7 @@ limitations under the License.
*/
import { randomString } from '../randomstring';
import { getCrypto } from '../utils';
const subtleCrypto = (typeof window !== "undefined" && window.crypto) ?
(window.crypto.subtle || window.crypto.webkitSubtle) : null;
import { subtleCrypto, TextEncoder } from "./crypto";
const DEFAULT_ITERATIONS = 500000;
@@ -75,21 +72,8 @@ export async function deriveKey(
iterations: number,
numBits = DEFAULT_BITSIZE,
): Promise<Uint8Array> {
return subtleCrypto
? deriveKeyBrowser(password, salt, iterations, numBits)
: deriveKeyNode(password, salt, iterations, numBits);
}
async function deriveKeyBrowser(
password: string,
salt: string,
iterations: number,
numBits: number,
): Promise<Uint8Array> {
const subtleCrypto = global.crypto.subtle;
const TextEncoder = global.TextEncoder;
if (!subtleCrypto || !TextEncoder) {
throw new Error("Password-based backup is not avaiable on this platform");
throw new Error("Password-based backup is not available on this platform");
}
const key = await subtleCrypto.importKey(
@@ -113,17 +97,3 @@ async function deriveKeyBrowser(
return new Uint8Array(keybits);
}
async function deriveKeyNode(
password: string,
salt: string,
iterations: number,
numBits: number,
): Promise<Uint8Array> {
const crypto = getCrypto();
if (!crypto) {
throw new Error("No usable crypto implementation");
}
return crypto.pbkdf2Sync(password, Buffer.from(salt, 'binary'), iterations, numBits, 'sha512');
}

View File

@@ -23,18 +23,18 @@ export interface Curve25519SessionData {
mac: string;
}
export interface IKeyBackupSession {
first_message_index: number; // eslint-disable-line camelcase
forwarded_count: number; // eslint-disable-line camelcase
is_verified: boolean; // eslint-disable-line camelcase
session_data: Curve25519SessionData | IEncryptedPayload; // eslint-disable-line camelcase
/* eslint-disable camelcase */
export interface IKeyBackupSession<T = Curve25519SessionData | IEncryptedPayload> {
first_message_index: number;
forwarded_count: number;
is_verified: boolean;
session_data: T;
}
export interface IKeyBackupRoomSessions {
[sessionId: string]: IKeyBackupSession;
}
/* eslint-disable camelcase */
export interface ICurve25519AuthData {
public_key: string;
private_key_salt?: string;

View File

@@ -24,8 +24,9 @@ import { IDevice } from "../deviceinfo";
import { ICrossSigningInfo } from "../CrossSigning";
import { PrefixedLogger } from "../../logger";
import { InboundGroupSessionData } from "../OlmDevice";
import { IEncryptedPayload } from "../aes";
import { MatrixEvent } from "../../models/event";
import { DehydrationManager } from "../dehydration";
import { IEncryptedPayload } from "../aes";
/**
* Internal module. Definitions for storage for the crypto module
@@ -33,6 +34,16 @@ import { MatrixEvent } from "../../models/event";
* @module
*/
export interface SecretStorePrivateKeys {
dehydration: {
keyInfo: DehydrationManager["keyInfo"];
key: IEncryptedPayload;
deviceDisplayName: string;
time: number;
} | null;
"m.megolm_backup.v1": IEncryptedPayload;
}
/**
* Abstraction of things that can store data required for end-to-end encryption
*
@@ -58,12 +69,20 @@ export interface CryptoStore {
deleteOutgoingRoomKeyRequest(requestId: string, expectedState: number): Promise<OutgoingRoomKeyRequest | null>;
// Olm Account
getAccount(txn: unknown, func: (accountPickle: string) => void);
getAccount(txn: unknown, func: (accountPickle: string | null) => void);
storeAccount(txn: unknown, accountPickle: string): void;
getCrossSigningKeys(txn: unknown, func: (keys: Record<string, ICrossSigningKey>) => void): void;
getSecretStorePrivateKey(txn: unknown, func: (key: IEncryptedPayload | null) => void, type: string): void;
getCrossSigningKeys(txn: unknown, func: (keys: Record<string, ICrossSigningKey> | null) => void): void;
getSecretStorePrivateKey<K extends keyof SecretStorePrivateKeys>(
txn: unknown,
func: (key: SecretStorePrivateKeys[K] | null) => void,
type: K,
): void;
storeCrossSigningKeys(txn: unknown, keys: Record<string, ICrossSigningKey>): void;
storeSecretStorePrivateKey(txn: unknown, type: string, key: IEncryptedPayload): void;
storeSecretStorePrivateKey<K extends keyof SecretStorePrivateKeys>(
txn: unknown,
type: K,
key: SecretStorePrivateKeys[K],
): void;
// Olm Sessions
countEndToEndSessions(txn: unknown, func: (count: number) => void): void;

View File

@@ -25,14 +25,13 @@ import {
IWithheld,
Mode,
OutgoingRoomKeyRequest,
ParkedSharedHistory,
ParkedSharedHistory, SecretStorePrivateKeys,
} from "./base";
import { IRoomKeyRequestBody, IRoomKeyRequestRecipient } from "../index";
import { ICrossSigningKey } from "../../client";
import { IOlmDevice } from "../algorithms/megolm";
import { IRoomEncryption } from "../RoomList";
import { InboundGroupSessionData } from "../OlmDevice";
import { IEncryptedPayload } from "../aes";
const PROFILE_TRANSACTIONS = false;
@@ -369,7 +368,7 @@ export class Backend implements CryptoStore {
// Olm Account
public getAccount(txn: IDBTransaction, func: (accountPickle: string) => void): void {
public getAccount(txn: IDBTransaction, func: (accountPickle: string | null) => void): void {
const objectStore = txn.objectStore("account");
const getReq = objectStore.get("-");
getReq.onsuccess = function() {
@@ -386,7 +385,10 @@ export class Backend implements CryptoStore {
objectStore.put(accountPickle, "-");
}
public getCrossSigningKeys(txn: IDBTransaction, func: (keys: Record<string, ICrossSigningKey>) => void): void {
public getCrossSigningKeys(
txn: IDBTransaction,
func: (keys: Record<string, ICrossSigningKey> | null) => void,
): void {
const objectStore = txn.objectStore("account");
const getReq = objectStore.get("crossSigningKeys");
getReq.onsuccess = function() {
@@ -398,10 +400,10 @@ export class Backend implements CryptoStore {
};
}
public getSecretStorePrivateKey(
public getSecretStorePrivateKey<K extends keyof SecretStorePrivateKeys>(
txn: IDBTransaction,
func: (key: IEncryptedPayload | null) => void,
type: string,
func: (key: SecretStorePrivateKeys[K] | null) => void,
type: K,
): void {
const objectStore = txn.objectStore("account");
const getReq = objectStore.get(`ssss_cache:${type}`);
@@ -419,7 +421,11 @@ export class Backend implements CryptoStore {
objectStore.put(keys, "crossSigningKeys");
}
public storeSecretStorePrivateKey(txn: IDBTransaction, type: string, key: IEncryptedPayload): void {
public storeSecretStorePrivateKey<K extends keyof SecretStorePrivateKeys>(
txn: IDBTransaction,
type: K,
key: SecretStorePrivateKeys[K],
): void {
const objectStore = txn.objectStore("account");
objectStore.put(key, `ssss_cache:${type}`);
}

View File

@@ -29,14 +29,13 @@ import {
IWithheld,
Mode,
OutgoingRoomKeyRequest,
ParkedSharedHistory,
ParkedSharedHistory, SecretStorePrivateKeys,
} from "./base";
import { IRoomKeyRequestBody } from "../index";
import { ICrossSigningKey } from "../../client";
import { IOlmDevice } from "../algorithms/megolm";
import { IRoomEncryption } from "../RoomList";
import { InboundGroupSessionData } from "../OlmDevice";
import { IEncryptedPayload } from "../aes";
/**
* Internal module. indexeddb storage for e2e.
@@ -323,7 +322,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
* @param {*} txn An active transaction. See doTxn().
* @param {function(string)} func Called with the account pickle
*/
public getAccount(txn: IDBTransaction, func: (accountPickle: string) => void) {
public getAccount(txn: IDBTransaction, func: (accountPickle: string | null) => void) {
this.backend.getAccount(txn, func);
}
@@ -346,7 +345,10 @@ export class IndexedDBCryptoStore implements CryptoStore {
* @param {function(string)} func Called with the account keys object:
* { key_type: base64 encoded seed } where key type = user_signing_key_seed or self_signing_key_seed
*/
public getCrossSigningKeys(txn: IDBTransaction, func: (keys: Record<string, ICrossSigningKey>) => void): void {
public getCrossSigningKeys(
txn: IDBTransaction,
func: (keys: Record<string, ICrossSigningKey> | null) => void,
): void {
this.backend.getCrossSigningKeys(txn, func);
}
@@ -355,10 +357,10 @@ export class IndexedDBCryptoStore implements CryptoStore {
* @param {function(string)} func Called with the private key
* @param {string} type A key type
*/
public getSecretStorePrivateKey(
public getSecretStorePrivateKey<K extends keyof SecretStorePrivateKeys>(
txn: IDBTransaction,
func: (key: IEncryptedPayload | null) => void,
type: string,
func: (key: SecretStorePrivateKeys[K] | null) => void,
type: K,
): void {
this.backend.getSecretStorePrivateKey(txn, func, type);
}
@@ -380,7 +382,11 @@ export class IndexedDBCryptoStore implements CryptoStore {
* @param {string} type The type of cross-signing private key to store
* @param {string} key keys object as getCrossSigningKeys()
*/
public storeSecretStorePrivateKey(txn: IDBTransaction, type: string, key: IEncryptedPayload): void {
public storeSecretStorePrivateKey<K extends keyof SecretStorePrivateKeys>(
txn: IDBTransaction,
type: K,
key: SecretStorePrivateKeys[K],
): void {
this.backend.storeSecretStorePrivateKey(txn, type, key);
}

View File

@@ -16,12 +16,11 @@ limitations under the License.
import { logger } from '../../logger';
import { MemoryCryptoStore } from './memory-crypto-store';
import { IDeviceData, IProblem, ISession, ISessionInfo, IWithheld, Mode } from "./base";
import { IDeviceData, IProblem, ISession, ISessionInfo, IWithheld, Mode, SecretStorePrivateKeys } from "./base";
import { IOlmDevice } from "../algorithms/megolm";
import { IRoomEncryption } from "../RoomList";
import { ICrossSigningKey } from "../../client";
import { InboundGroupSessionData } from "../OlmDevice";
import { IEncryptedPayload } from "../aes";
/**
* Internal module. Partial localStorage backed storage for e2e.
@@ -374,7 +373,7 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
// Olm account
public getAccount(txn: unknown, func: (accountPickle: string) => void): void {
public getAccount(txn: unknown, func: (accountPickle: string | null) => void): void {
const accountPickle = getJsonItem<string>(this.store, KEY_END_TO_END_ACCOUNT);
func(accountPickle);
}
@@ -383,13 +382,17 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
setJsonItem(this.store, KEY_END_TO_END_ACCOUNT, accountPickle);
}
public getCrossSigningKeys(txn: unknown, func: (keys: Record<string, ICrossSigningKey>) => void): void {
public getCrossSigningKeys(txn: unknown, func: (keys: Record<string, ICrossSigningKey> | null) => void): void {
const keys = getJsonItem<Record<string, ICrossSigningKey>>(this.store, KEY_CROSS_SIGNING_KEYS);
func(keys);
}
public getSecretStorePrivateKey(txn: unknown, func: (key: IEncryptedPayload | null) => void, type: string): void {
const key = getJsonItem<IEncryptedPayload>(this.store, E2E_PREFIX + `ssss_cache.${type}`);
public getSecretStorePrivateKey<K extends keyof SecretStorePrivateKeys>(
txn: unknown,
func: (key: SecretStorePrivateKeys[K] | null) => void,
type: K,
): void {
const key = getJsonItem<SecretStorePrivateKeys[K]>(this.store, E2E_PREFIX + `ssss_cache.${type}`);
func(key);
}
@@ -397,7 +400,11 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
setJsonItem(this.store, KEY_CROSS_SIGNING_KEYS, keys);
}
public storeSecretStorePrivateKey(txn: unknown, type: string, key: IEncryptedPayload): void {
public storeSecretStorePrivateKey<K extends keyof SecretStorePrivateKeys>(
txn: unknown,
type: K,
key: SecretStorePrivateKeys[K],
): void {
setJsonItem(this.store, E2E_PREFIX + `ssss_cache.${type}`, key);
}

View File

@@ -25,14 +25,13 @@ import {
IWithheld,
Mode,
OutgoingRoomKeyRequest,
ParkedSharedHistory,
ParkedSharedHistory, SecretStorePrivateKeys,
} from "./base";
import { IRoomKeyRequestBody } from "../index";
import { ICrossSigningKey } from "../../client";
import { IOlmDevice } from "../algorithms/megolm";
import { IRoomEncryption } from "../RoomList";
import { InboundGroupSessionData } from "../OlmDevice";
import { IEncryptedPayload } from "../aes";
/**
* Internal module. in-memory storage for e2e.
@@ -45,9 +44,9 @@ import { IEncryptedPayload } from "../aes";
*/
export class MemoryCryptoStore implements CryptoStore {
private outgoingRoomKeyRequests: OutgoingRoomKeyRequest[] = [];
private account: string = null;
private crossSigningKeys: Record<string, ICrossSigningKey> = null;
private privateKeys: Record<string, IEncryptedPayload> = {};
private account: string | null = null;
private crossSigningKeys: Record<string, ICrossSigningKey> | null = null;
private privateKeys: Partial<SecretStorePrivateKeys> = {};
private sessions: { [deviceKey: string]: { [sessionId: string]: ISessionInfo } } = {};
private sessionProblems: { [deviceKey: string]: IProblem[] } = {};
@@ -280,7 +279,7 @@ export class MemoryCryptoStore implements CryptoStore {
// Olm Account
public getAccount(txn: unknown, func: (accountPickle: string) => void) {
public getAccount(txn: unknown, func: (accountPickle: string | null) => void) {
func(this.account);
}
@@ -288,12 +287,16 @@ export class MemoryCryptoStore implements CryptoStore {
this.account = accountPickle;
}
public getCrossSigningKeys(txn: unknown, func: (keys: Record<string, ICrossSigningKey>) => void): void {
public getCrossSigningKeys(txn: unknown, func: (keys: Record<string, ICrossSigningKey> | null) => void): void {
func(this.crossSigningKeys);
}
public getSecretStorePrivateKey(txn: unknown, func: (key: IEncryptedPayload | null) => void, type: string): void {
const result = this.privateKeys[type];
public getSecretStorePrivateKey<K extends keyof SecretStorePrivateKeys>(
txn: unknown,
func: (key: SecretStorePrivateKeys[K] | null) => void,
type: K,
): void {
const result = this.privateKeys[type] as SecretStorePrivateKeys[K] | undefined;
func(result || null);
}
@@ -301,7 +304,11 @@ export class MemoryCryptoStore implements CryptoStore {
this.crossSigningKeys = keys;
}
public storeSecretStorePrivateKey(txn: unknown, type: string, key: IEncryptedPayload): void {
public storeSecretStorePrivateKey<K extends keyof SecretStorePrivateKeys>(
txn: unknown,
type: K,
key: SecretStorePrivateKeys[K],
): void {
this.privateKeys[type] = key;
}

View File

@@ -15,22 +15,12 @@ limitations under the License.
*/
import * as matrixcs from "./matrix";
import * as utils from "./utils";
import { logger } from './logger';
if (global.__js_sdk_entrypoint) {
throw new Error("Multiple matrix-js-sdk entrypoints detected!");
}
global.__js_sdk_entrypoint = true;
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const crypto = require('crypto');
utils.setCrypto(crypto);
} catch (err) {
logger.log('nodejs was compiled without crypto support');
}
export * from "./matrix";
export default matrixcs;

View File

@@ -23,7 +23,6 @@ limitations under the License.
import unhomoglyph from "unhomoglyph";
import promiseRetry from "p-retry";
import type * as NodeCrypto from "crypto";
import { MatrixEvent } from "./models/event";
import { M_TIMESTAMP } from "./@types/location";
import { ReceiptType } from "./@types/read_receipts";
@@ -453,20 +452,6 @@ export function simpleRetryOperation<T>(promiseFn: (attempt: number) => Promise<
});
}
// We need to be able to access the Node.js crypto library from within the
// Matrix SDK without needing to `require("crypto")`, which will fail in
// browsers. So `index.ts` will call `setCrypto` to store it, and when we need
// it, we can call `getCrypto`.
let crypto: typeof NodeCrypto;
export function setCrypto(c: typeof NodeCrypto) {
crypto = c;
}
export function getCrypto(): typeof NodeCrypto {
return crypto;
}
// String averaging inspired by https://stackoverflow.com/a/2510816
// Dev note: We make the alphabet a string because it's easier to write syntactically
// than arrays. Thankfully, strings implement the useful parts of the Array interface