You've already forked matrix-js-sdk
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:
committed by
GitHub
parent
6af3b114e1
commit
30570bcce6
@@ -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();
|
||||
|
@@ -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');
|
||||
}
|
||||
|
@@ -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,
|
||||
|
@@ -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}`,
|
||||
|
@@ -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
|
||||
|
@@ -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";
|
||||
|
||||
|
@@ -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);
|
||||
return buf;
|
||||
}
|
||||
throw new Error("No usable crypto implementation");
|
||||
const buf = new Uint8Array(size);
|
||||
crypto.getRandomValues(buf);
|
||||
return buf;
|
||||
}
|
||||
|
||||
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
31
src/crypto/crypto.ts
Normal 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 */
|
@@ -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');
|
||||
}
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
|
@@ -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}`);
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
|
||||
|
@@ -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);
|
||||
}
|
||||
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
|
10
src/index.ts
10
src/index.ts
@@ -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;
|
||||
|
||||
|
15
src/utils.ts
15
src/utils.ts
@@ -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
|
||||
|
Reference in New Issue
Block a user