From 5d52053caadf28d394c6ac223f9e2c78a53edfe0 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Mon, 24 Feb 2020 17:38:53 -0500 Subject: [PATCH 01/21] use symmetric encryption for SSSS --- spec/unit/crypto/secrets.spec.js | 42 +++- src/crypto/SecretStorage.js | 394 ++++++++++++++++++++++++------- src/crypto/index.js | 6 +- src/index.ts | 7 + src/utils.ts | 14 ++ 5 files changed, 364 insertions(+), 99 deletions(-) diff --git a/spec/unit/crypto/secrets.spec.js b/spec/unit/crypto/secrets.spec.js index 5084263e6..80e5bf454 100644 --- a/spec/unit/crypto/secrets.spec.js +++ b/spec/unit/crypto/secrets.spec.js @@ -16,11 +16,20 @@ limitations under the License. import '../../olm-loader'; import * as olmlib from "../../../src/crypto/olmlib"; -import {SECRET_STORAGE_ALGORITHM_V1} from "../../../src/crypto/SecretStorage"; +import {SECRET_STORAGE_ALGORITHM_V1_AES} from "../../../src/crypto/SecretStorage"; import {MatrixEvent} from "../../../src/models/event"; import {TestClient} from '../../TestClient'; import {makeTestClients} from './verification/util'; +import * as utils from "../../../src/utils"; + +try { + const crypto = require('crypto'); + utils.setCrypto(crypto); +} catch (err) { + console.log('nodejs was compiled without crypto support'); +} + async function makeTestClient(userInfo, options) { const client = (new TestClient( userInfo.userId, userInfo.deviceId, undefined, undefined, options, @@ -51,9 +60,8 @@ describe("Secrets", function() { }); it("should store and retrieve a secret", async function() { - const decryption = new global.Olm.PkDecryption(); - const pubkey = decryption.generate_key(); - const privkey = decryption.get_private_key(); + const key = new Uint8Array(16); + for (let i = 0; i < 16; i++) key[i] = i; const signing = new global.Olm.PkSigning(); const signingKey = signing.generate_seed(); @@ -69,7 +77,7 @@ describe("Secrets", function() { const getKey = jest.fn(e => { expect(Object.keys(e.keys)).toEqual(["abc"]); - return ['abc', privkey]; + return ['abc', key]; }); const alice = await makeTestClient( @@ -100,8 +108,7 @@ describe("Secrets", function() { }; const keyAccountData = { - algorithm: SECRET_STORAGE_ALGORITHM_V1, - pubkey: pubkey, + algorithm: SECRET_STORAGE_ALGORITHM_V1_AES, }; await alice._crypto._crossSigningInfo.signObject(keyAccountData, 'master'); @@ -149,6 +156,13 @@ describe("Secrets", function() { }); it("should encrypt with default key if keys is null", async function() { + const key = new Uint8Array(16); + for (let i = 0; i < 16; i++) key[i] = i; + const getKey = jest.fn(e => { + expect(Object.keys(e.keys)).toEqual([newKeyId]); + return [newKeyId, key]; + }); + let keys = {}; const alice = await makeTestClient( {userId: "@alice:example.com", deviceId: "Osborne2"}, @@ -156,6 +170,7 @@ describe("Secrets", function() { cryptoCallbacks: { getCrossSigningKey: t => keys[t], saveCrossSigningKeys: k => keys = k, + getSecretStorageKey: getKey, }, }, ); @@ -170,7 +185,7 @@ describe("Secrets", function() { alice.resetCrossSigningKeys(); const newKeyId = await alice.addSecretStorageKey( - SECRET_STORAGE_ALGORITHM_V1, + SECRET_STORAGE_ALGORITHM_V1_AES, ); // we don't await on this because it waits for the event to come down the sync // which won't happen in the test setup @@ -252,11 +267,22 @@ describe("Secrets", function() { }); it("bootstraps when no storage or cross-signing keys locally", async function() { + const key = new Uint8Array(16); + for (let i = 0; i < 16; i++) key[i] = i; + const getKey = jest.fn(e => { + return [Object.keys(e.keys)[0], key]; + }); + const bob = await makeTestClient( { userId: "@bob:example.com", deviceId: "bob1", }, + { + cryptoCallbacks: { + getSecretStorageKey: getKey, + }, + }, ); bob.uploadDeviceSigningKeys = async () => {}; bob.uploadKeySignatures = async () => {}; diff --git a/src/crypto/SecretStorage.js b/src/crypto/SecretStorage.js index 799cf8d1d..46bf93052 100644 --- a/src/crypto/SecretStorage.js +++ b/src/crypto/SecretStorage.js @@ -1,5 +1,5 @@ /* -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2019, 2020 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. @@ -19,8 +19,225 @@ import {logger} from '../logger'; import * as olmlib from './olmlib'; import {pkVerify} from './olmlib'; import {randomString} from '../randomstring'; +import {decodeBase64, encodeBase64} from './olmlib'; +import {getCrypto} from '../utils'; -export const SECRET_STORAGE_ALGORITHM_V1 = "m.secret_storage.v1.curve25519-aes-sha2"; +export const SECRET_STORAGE_ALGORITHM_V1_AES + = "m.secret_storage.v1.aes-hmac-sha2"; +// don't use curve25519 for writing data. +export const SECRET_STORAGE_ALGORITHM_V1_CURVE25519 + = "m.secret_storage.v1.curve25519-aes-sha2"; + +const subtleCrypto = typeof window === "undefined" ? null : + (window.crypto.subtle || window.crypto.webkitSubtle); + +// salt for HKDF, with 8 bytes of zeros +const zerosalt = new Uint8Array(8); + +/** 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 + */ +async function encryptNode(data, key, name) { + const crypto = getCrypto(); + if (!crypto) { + throw new Error("No usable crypto implementation"); + } + + const 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 = cipher.update(data, "utf-8", "base64") + + cipher.final("base64"); + + const hmac = crypto.createHmac("sha256", hmacKey) + .update(ciphertext, "base64").digest("base64"); + + return { + iv: encodeBase64(iv), + ciphertext: ciphertext, + 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, key, name) { + 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(data.ciphertext, "base64").digest("base64"); + + if (hmac !== data.mac) { + 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", "utf-8") + + decipher.final("utf-8"); +} + +function deriveKeysNode(key, name) { + 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, "utf-8").update(b).digest(); + b[0] = 2; + const hmacKey = crypto.createHmac("sha256", prk) + .update(aesKey).update(name, "utf-8").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 + */ +async function encryptBrowser(data, key, name) { + const iv = new Uint8Array(16); + window.crypto.getRandomValues(iv); + + // clear bit 63 of the IV to stop us hitting the 64-bit counter boundary + // (which would mean we wouldn't be able to decrypt on Android). The loss + // of a single bit of iv is a price we have to pay. + iv[8] &= 0x7f; + + const [aesKey, hmacKey] = await deriveKeysBrowser(key, name); + const encodedData = new TextEncoder().encode(data); + + const ciphertext = await subtleCrypto.encrypt( + { + name: "AES-CTR", + counter: iv, + length: 64, + }, + aesKey, + encodedData, + ); + + const hmac = await subtleCrypto.sign( + {name: 'HMAC'}, + hmacKey, + ciphertext, + ); + + return { + iv: encodeBase64(iv), + ciphertext: encodeBase64(ciphertext), + mac: encodeBase64(hmac), + }; +} + +/** decrypt a string in the browser + * + * @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 decryptBrowser(data, key, name) { + const [aesKey, hmacKey] = await deriveKeysBrowser(key, name); + + const ciphertext = decodeBase64(data.ciphertext); + + if (!await subtleCrypto.verify( + {name: "HMAC"}, + hmacKey, + decodeBase64(data.mac), + ciphertext, + )) { + throw new Error(`Error decrypting secret ${name}: bad MAC`); + } + + const plaintext = await subtleCrypto.decrypt( + { + name: "AES-CTR", + counter: decodeBase64(data.iv), + length: 64, + }, + aesKey, + ciphertext, + ); + + return new TextDecoder().decode(new Uint8Array(plaintext)); +} + +async function deriveKeysBrowser(key, name) { + const hkdfkey = await subtleCrypto.importKey( + 'raw', + key, + {name: "HKDF"}, + false, + ["deriveBits"], + ); + const keybits = await subtleCrypto.deriveBits( + { + name: "HKDF", + salt: zerosalt, + info: (new TextEncoder().encode(name)), + hash: "SHA-256", + }, + hkdfkey, + 512, + ); + + const aesKey = keybits.slice(0, 32); + const hmacKey = keybits.slice(32); + + const aesProm = await subtleCrypto.importKey( + 'raw', + aesKey, + {name: 'AES-CTR'}, + false, + ['encrypt', 'decrypt'], + ); + + const hmacProm = await subtleCrypto.importKey( + 'raw', + hmacKey, + { + name: 'HMAC', + hash: {name: 'SHA-256'}, + }, + false, + ['sign', 'verify'], + ); + + return await Promise.all([aesProm, hmacProm]); +} + +const [encryptAES, decryptAES] = (typeof window === "undefined") ? + [encryptNode, decryptNode] : [encryptBrowser, decryptBrowser]; /** * Implements Secure Secret Storage and Sharing (MSC1946) @@ -85,20 +302,12 @@ export class SecretStorage extends EventEmitter { } switch (algorithm) { - case SECRET_STORAGE_ALGORITHM_V1: + case SECRET_STORAGE_ALGORITHM_V1_AES: { const decryption = new global.Olm.PkDecryption(); try { - const { passphrase, pubkey } = opts; - // Copies in public key details of the form generated by - // the Crypto module's `createRecoveryKeyFromPassphrase`. - if (passphrase && pubkey) { - keyData.passphrase = passphrase; - keyData.pubkey = pubkey; - } else if (pubkey) { - keyData.pubkey = pubkey; - } else { - keyData.pubkey = decryption.generate_key(); + if (opts.passphrase) { + keyData.passphrase = opts.passphrase; } } finally { decryption.free(); @@ -207,24 +416,13 @@ export class SecretStorage extends EventEmitter { throw new Error("Unknown key: " + keyId); } - // check signature of key info - pkVerify( - keyInfo, - this._crossSigningInfo.getId('master'), - this._crossSigningInfo.userId, - ); - // encrypt secret, based on the algorithm switch (keyInfo.algorithm) { - case SECRET_STORAGE_ALGORITHM_V1: + case SECRET_STORAGE_ALGORITHM_V1_AES: { - const encryption = new global.Olm.PkEncryption(); - try { - encryption.set_recipient_key(keyInfo.pubkey); - encrypted[keyId] = encryption.encrypt(secret); - } finally { - encryption.free(); - } + const keys = {[keyId]: keyInfo}; + const [, encryption] = await this._getSecretStorageKey(keys, name); + encrypted[keyId] = await encryption.encrypt(secret); break; } default: @@ -238,29 +436,6 @@ export class SecretStorage extends EventEmitter { await this._baseApis.setAccountData(name, {encrypted}); } - /** - * Store a secret defined to be the same as the given key. - * No secret information will be stored, instead the secret will - * be stored with a marker to say that the contents of the secret is - * the value of the given key. - * This is useful for migration from systems that predate SSSS such as - * key backup. - * - * @param {string} name The name of the secret - * @param {string} keyId The ID of the key whose value will be the - * value of the secret - * @returns {Promise} resolved when account data is saved - */ - storePassthrough(name, keyId) { - return this._baseApis.setAccountData(name, { - encrypted: { - [keyId]: { - passthrough: true, - }, - }, - }); - } - /** * Temporary method to fix up existing accounts where secrets * are incorrectly stored without the 'encrypted' level @@ -317,7 +492,12 @@ export class SecretStorage extends EventEmitter { ); const encInfo = secretInfo.encrypted[keyId]; switch (keyInfo.algorithm) { - case SECRET_STORAGE_ALGORITHM_V1: + case SECRET_STORAGE_ALGORITHM_V1_AES: + if (encInfo.iv && encInfo.ciphertext && encInfo.mac) { + keys[keyId] = keyInfo; + } + break; + case SECRET_STORAGE_ALGORITHM_V1_CURVE25519: if ( keyInfo.pubkey && ( (encInfo.ciphertext && encInfo.mac && encInfo.ephemeral) || @@ -344,15 +524,9 @@ export class SecretStorage extends EventEmitter { // since we just want to return the key itself. if (encInfo.passthrough) return decryption.get_private_key(); - // decrypt secret - switch (keys[keyId].algorithm) { - case SECRET_STORAGE_ALGORITHM_V1: - return decryption.decrypt( - encInfo.ephemeral, encInfo.mac, encInfo.ciphertext, - ); - } + return await decryption.decrypt(encInfo); } finally { - if (decryption) decryption.free(); + if (decryption && decryption.free) decryption.free(); } } @@ -387,22 +561,44 @@ export class SecretStorage extends EventEmitter { ); if (!keyInfo) return false; const encInfo = secretInfo.encrypted[keyId]; - if (checkKey) { - pkVerify( - keyInfo, - this._crossSigningInfo.getId('master'), - this._crossSigningInfo.userId, - ); - } // We don't actually need the decryption object if it's a passthrough // since we just want to return the key itself. - if (encInfo.passthrough) return true; + if (encInfo.passthrough) { + try { + pkVerify( + keyInfo, + this._crossSigningInfo.getId('master'), + this._crossSigningInfo.userId, + ); + } catch (e) { + // not trusted, so move on to the next key + continue; + } + return true; + } switch (keyInfo.algorithm) { - case SECRET_STORAGE_ALGORITHM_V1: + case SECRET_STORAGE_ALGORITHM_V1_AES: + if (encInfo.iv && encInfo.ciphertext && encInfo.mac) { + return true; + } + break; + case SECRET_STORAGE_ALGORITHM_V1_CURVE25519: if (keyInfo.pubkey && encInfo.ciphertext && encInfo.mac && encInfo.ephemeral) { + if (checkKey) { + try { + pkVerify( + keyInfo, + this._crossSigningInfo.getId('master'), + this._crossSigningInfo.userId, + ); + } catch (e) { + // not trusted, so move on to the next key + continue; + } + } return true; } break; @@ -607,26 +803,48 @@ export class SecretStorage extends EventEmitter { } switch (keys[keyId].algorithm) { - case SECRET_STORAGE_ALGORITHM_V1: - { - const decryption = new global.Olm.PkDecryption(); - let pubkey; - try { - pubkey = decryption.init_with_private_key(privateKey); - } catch (e) { - decryption.free(); - throw new Error("getSecretStorageKey callback returned invalid key"); - } - if (pubkey !== keys[keyId].pubkey) { - decryption.free(); - throw new Error( - "getSecretStorageKey callback returned incorrect key", - ); - } - return [keyId, decryption]; + case SECRET_STORAGE_ALGORITHM_V1_AES: + { + const decryption = { + encrypt: async function(secret) { + return await encryptAES(secret, privateKey, name); + }, + decrypt: async function(encInfo) { + return await decryptAES(encInfo, privateKey, name); + }, + }; + return [keyId, decryption]; + } + case SECRET_STORAGE_ALGORITHM_V1_CURVE25519: + { + const pkDecryption = new global.Olm.PkDecryption(); + let pubkey; + try { + pubkey = pkDecryption.init_with_private_key(privateKey); + } catch (e) { + pkDecryption.free(); + throw new Error("getSecretStorageKey callback returned invalid key"); } - default: - throw new Error("Unknown key type: " + keys[keyId].algorithm); + if (pubkey !== keys[keyId].pubkey) { + pkDecryption.free(); + throw new Error( + "getSecretStorageKey callback returned incorrect key", + ); + } + const decryption = { + free: pkDecryption.free().bind(pkDecryption), + decrypt: async function(encInfo) { + return pkDecryption.decrypt( + encInfo.ephemeral, encInfo.mac, encInfo.ciphertext, + ); + }, + // needed for passthrough + get_private_key: pkDecryption.get_private_key.bind(pkDecryption), + }; + return [keyId, decryption]; + } + default: + throw new Error("Unknown key type: " + keys[keyId].algorithm); } } } diff --git a/src/crypto/index.js b/src/crypto/index.js index b59cdf895..887c49605 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -38,7 +38,7 @@ import { DeviceTrustLevel, UserTrustLevel, } from './CrossSigning'; -import {SECRET_STORAGE_ALGORITHM_V1, SecretStorage} from './SecretStorage'; +import {SECRET_STORAGE_ALGORITHM_V1_AES, SecretStorage} from './SecretStorage'; import {OutgoingRoomKeyRequestManager} from './OutgoingRoomKeyRequestManager'; import {IndexedDBCryptoStore} from './store/indexeddb-crypto-store'; import { @@ -439,7 +439,7 @@ Crypto.prototype.bootstrapSecretStorage = async function({ } newKeyId = await this.addSecretStorageKey( - SECRET_STORAGE_ALGORITHM_V1, opts, + SECRET_STORAGE_ALGORITHM_V1_AES, opts, ); // Add an entry for the backup key in SSSS as a 'passthrough' key @@ -468,7 +468,7 @@ Crypto.prototype.bootstrapSecretStorage = async function({ logger.log("Secret storage default key not found, creating new key"); const keyOptions = await createSecretStorageKey(); newKeyId = await this.addSecretStorageKey( - SECRET_STORAGE_ALGORITHM_V1, + SECRET_STORAGE_ALGORITHM_V1_AES, keyOptions, ); } diff --git a/src/index.ts b/src/index.ts index 18fdba545..6d7c2839b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -21,5 +21,12 @@ import request from "request"; matrixcs.request(request); utils.runPolyfills(); +try { + const crypto = require('crypto'); + utils.setCrypto(crypto); +} catch (err) { + console.log('nodejs was compiled without crypto support'); +} + export * from "./matrix"; export default matrixcs; diff --git a/src/utils.ts b/src/utils.ts index ec2113a91..1e31dff88 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -734,3 +734,17 @@ export async function promiseMapSeries( export function promiseTry(fn: () => T): Promise { return new Promise((resolve) => resolve(fn())); } + +// 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: Object; + +export function setCrypto(c: Object) { + crypto = c; +} + +export function getCrypto(): Object { + return crypto; +} From ed223d1d765676662bddc10ed6e93e0adb726dfb Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Thu, 27 Feb 2020 22:54:43 -0500 Subject: [PATCH 02/21] remove unnecessary awaits --- src/crypto/SecretStorage.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/crypto/SecretStorage.js b/src/crypto/SecretStorage.js index 46bf93052..7ba28fa84 100644 --- a/src/crypto/SecretStorage.js +++ b/src/crypto/SecretStorage.js @@ -214,7 +214,7 @@ async function deriveKeysBrowser(key, name) { const aesKey = keybits.slice(0, 32); const hmacKey = keybits.slice(32); - const aesProm = await subtleCrypto.importKey( + const aesProm = subtleCrypto.importKey( 'raw', aesKey, {name: 'AES-CTR'}, @@ -222,7 +222,7 @@ async function deriveKeysBrowser(key, name) { ['encrypt', 'decrypt'], ); - const hmacProm = await subtleCrypto.importKey( + const hmacProm = subtleCrypto.importKey( 'raw', hmacKey, { From 1151bdc6db6bd8208762f45bb0f49a0cff577225 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Thu, 27 Feb 2020 22:56:34 -0500 Subject: [PATCH 03/21] initial work in migrating ssss to symmetric --- spec/unit/crypto/secrets.spec.js | 4 +- src/crypto/CrossSigning.js | 21 ++++++-- src/crypto/SecretStorage.js | 54 ++++++++++++------- src/crypto/index.js | 92 +++++++++++++++++++++++++------- src/crypto/key_passphrase.js | 9 ++-- 5 files changed, 134 insertions(+), 46 deletions(-) diff --git a/spec/unit/crypto/secrets.spec.js b/spec/unit/crypto/secrets.spec.js index 80e5bf454..02e4b1a5e 100644 --- a/spec/unit/crypto/secrets.spec.js +++ b/spec/unit/crypto/secrets.spec.js @@ -119,11 +119,11 @@ describe("Secrets", function() { }), ]); - expect(await secretStorage.isStored("foo")).toBe(false); + expect(await secretStorage.isStored("foo")).toBeFalsy(); await secretStorage.store("foo", "bar", ["abc"]); - expect(await secretStorage.isStored("foo")).toBe(true); + expect(await secretStorage.isStored("foo")).toBeTruthy(); expect(await secretStorage.get("foo")).toBe("bar"); expect(getKey).toHaveBeenCalled(); diff --git a/src/crypto/CrossSigning.js b/src/crypto/CrossSigning.js index 2bc9d0f3f..cd4a48845 100644 --- a/src/crypto/CrossSigning.js +++ b/src/crypto/CrossSigning.js @@ -111,14 +111,25 @@ export class CrossSigningInfo extends EventEmitter { * want to know this anyway... * * @param {SecretStorage} secretStorage The secret store using account data - * @returns {boolean} Whether all private keys were found in storage + * @returns {object} map of key name to key info the secret is encrypted + * with, or null if it is not present or not encrypted with a trusted + * key */ async isStoredInSecretStorage(secretStorage) { - let stored = true; - for (const type of ["master", "self_signing", "user_signing"]) { - stored &= await secretStorage.isStored(`m.cross_signing.${type}`, false); + const stored = await secretStorage.isStored("m.cross_signing.master", false) | {}; + function intersect(s) { + for (const k of Object.keys(stored)) { + if (!s[k]) { + delete stored[k]; + } + } } - return stored; + for (const type of ["self_signing", "user_signing"]) { + intersect( + await secretStorage.isStored(`m.cross_signing.${type}`, false) || {}, + ); + } + return Object.keys(stored).length ? stored : null; } /** diff --git a/src/crypto/SecretStorage.js b/src/crypto/SecretStorage.js index 7ba28fa84..b8a45694d 100644 --- a/src/crypto/SecretStorage.js +++ b/src/crypto/SecretStorage.js @@ -364,6 +364,28 @@ export class SecretStorage extends EventEmitter { ); } + /** + * Get the key information for a given ID. + * + * @param {string} [keyId = default key's ID] The ID of the key to check + * for. Defaults to the default key ID if not provided. + * @returns {Array?} If the key was found, the return value is an array of + * the form [keyId, keyInfo]. Otherwise, null is returned. + */ + async getKey(keyId) { + if (!keyId) { + keyId = await this.getDefaultKeyId(); + } + if (!keyId) { + return null; + } + + const keyInfo = this._baseApis.getAccountDataFromServer( + "m.secret_storage.key." + keyId, + ); + return keyInfo ? [keyId, keyInfo] : null; + } + /** * Check whether we have a key with a given ID. * @@ -372,16 +394,7 @@ export class SecretStorage extends EventEmitter { * @return {boolean} Whether we have the key. */ async hasKey(keyId) { - if (!keyId) { - keyId = await this.getDefaultKeyId(); - } - if (!keyId) { - return false; - } - - return !!this._baseApis.getAccountDataFromServer( - "m.secret_storage.key." + keyId, - ); + return !!(await this.getKey(keyId)); } /** @@ -536,22 +549,26 @@ export class SecretStorage extends EventEmitter { * @param {string} name the name of the secret * @param {boolean} checkKey check if the secret is encrypted by a trusted key * - * @return {boolean} whether or not the secret is stored + * @return {object?} map of key name to key info the secret is encrypted + * with, or null if it is not present or not encrypted with a trusted + * key */ async isStored(name, checkKey) { // check if secret exists let secretInfo = await this._baseApis.getAccountDataFromServer(name); - if (!secretInfo) return false; + if (!secretInfo) return null; if (!secretInfo.encrypted) { // try to fix it up secretInfo = await this._fixupStoredSecret(name, secretInfo); if (!secretInfo || !secretInfo.encrypted) { - return false; + return null; } } if (checkKey === undefined) checkKey = true; + const ret = {}; + // check if secret is encrypted by a known/trusted secret and // encryption looks sane for (const keyId of Object.keys(secretInfo.encrypted)) { @@ -559,7 +576,7 @@ export class SecretStorage extends EventEmitter { const keyInfo = await this._baseApis.getAccountDataFromServer( "m.secret_storage.key." + keyId, ); - if (!keyInfo) return false; + if (!keyInfo) continue; const encInfo = secretInfo.encrypted[keyId]; // We don't actually need the decryption object if it's a passthrough @@ -575,13 +592,14 @@ export class SecretStorage extends EventEmitter { // not trusted, so move on to the next key continue; } - return true; + ret[keyId] = keyInfo; + continue; } switch (keyInfo.algorithm) { case SECRET_STORAGE_ALGORITHM_V1_AES: if (encInfo.iv && encInfo.ciphertext && encInfo.mac) { - return true; + ret[keyId] = keyInfo; } break; case SECRET_STORAGE_ALGORITHM_V1_CURVE25519: @@ -599,14 +617,14 @@ export class SecretStorage extends EventEmitter { continue; } } - return true; + ret[keyId] = keyInfo; } break; default: // do nothing if we don't understand the encryption algorithm } } - return false; + return Object.keys(ret).length ? ret : null; } /** diff --git a/src/crypto/index.js b/src/crypto/index.js index 887c49605..aa7f89fc8 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -388,10 +388,36 @@ Crypto.prototype.bootstrapSecretStorage = async function({ // use temporary callbacks to weave them through the various APIs. const appCallbacks = Object.assign({}, this._baseApis._cryptoCallbacks); + // the ID of the new SSSS key, if we create one + let newKeyId = null; + + // cache SSSS keys so that we don't need to constantly pester the user about it + const ssssKeys = {}; + + this._baseApis._cryptoCallbacks.getSecretStorageKey = + async (keys, name) => { + // if we already have a key that works, return it + for (const keyId of Object.keys(keys)) { + if (ssssKeys[keyId]) { + return [keyId, ssssKeys[keyId]]; + } + } + + // otherwise, prompt the user and cache it + const key = await appCallbacks.getSecretStorageKey(keys, name); + if (key) { + const [keyId, keyData] = key; + ssssKeys[keyId] = keyData; + } + return key; + }; + try { - const inStorage = !setupNewSecretStorage && - await this._crossSigningInfo.isStoredInSecretStorage(this._secretStorage); + const decryptionKeys = + await this._crossSigningInfo.isStoredInSecretStorage(this._secretStorage); + const inStorage = !setupNewSecretStorage && decryptionKeys; if (!this._crossSigningInfo.getId() || !inStorage) { + // create new cross-signing keys if necessary. logger.log( "Cross-signing public and/or private keys not found, " + "checking secret storage for private keys", @@ -413,19 +439,42 @@ Crypto.prototype.bootstrapSecretStorage = async function({ { authUploadDeviceSigningKeys }, ); } + } else if (!(Object.values(decryptionKeys).some( + info => info.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES, + ))) { + // we already have cross-signing keys, but they're encrypted using + // the old algorithm + logger.log("Switching to symmetric"); + // create new symmetric key and set it as default + newKeyId = this.addSecretStorageKey(); + this.setDefaultSecretStorageKeyId(newKeyId); + // re-encrypt all the keys with the new key + for (const type of ["master", "self_signing", "user_signing"]) { + const secretName = `m.cross_signing.${type}`; + const secret = this.getSecret(secretName); + this.storeSecret(secretName, secret, [newKeyId]); + } } else { logger.log("Cross signing keys are present in secret storage"); } - // Check if Secure Secret Storage has a default key. If we don't have one, create - // the default key (which will also be signed by the cross-signing master key). - if (setupNewSecretStorage || !await this.hasSecretStorageKey()) { - let newKeyId; + // Check if we need to create a new secret storage key + // - we're resetting secret storage + // - we don't have a default secret storage key yet + // - our default secret storage key is using an older algorithm + // We will also run this part if we created a new secret storage key + // above, so that we can (re-)encrypt the backup with it. + const defaultSSSSKey = await this.getSecretStorageKey(); + if (setupNewSecretStorage || newKeyId || !defaultSSSSKey + || defaultSSSSKey[1].algorithm !== SECRET_STORAGE_ALGORITHM_V1_AES) { if (keyBackupInfo) { + // if we already have a backup key, use the same key as the + // secret storage key logger.log("Secret storage default key not found, using key backup key"); - const opts = { - pubkey: keyBackupInfo.auth_data.public_key, - }; + const opts = {}; + + // FIXME: ask for recovery passphrase/key + const backupKey = "foobar"; if ( keyBackupInfo.auth_data.private_key_salt && @@ -435,16 +484,18 @@ Crypto.prototype.bootstrapSecretStorage = async function({ algorithm: "m.pbkdf2", iterations: keyBackupInfo.auth_data.private_key_iterations, salt: keyBackupInfo.auth_data.private_key_salt, + bits: 256, }; } - newKeyId = await this.addSecretStorageKey( - SECRET_STORAGE_ALGORITHM_V1_AES, opts, - ); - - // Add an entry for the backup key in SSSS as a 'passthrough' key - // (ie. the secret is the key itself). - this._secretStorage.storePassthrough('m.megolm_backup.v1', newKeyId); + if (!newKeyId) { + newKeyId = await this.addSecretStorageKey( + SECRET_STORAGE_ALGORITHM_V1_AES, opts, + ); + this.setDefaultSecretStorageKeyId(newKeyId); + // use the backup key as the new ssss key + ssssKeys[newKeyId] = backupKey; + } // if this key backup is trusted, sign it with the cross signing key // so the key backup can be trusted via cross-signing. @@ -459,20 +510,21 @@ Crypto.prototype.bootstrapSecretStorage = async function({ undefined, keyBackupInfo, {prefix: httpApi.PREFIX_UNSTABLE}, ); + await this.storeSecret("m.megolm_backup.v1", backupKey, [newKeyId]); } else { console.log( "Key backup is NOT TRUSTED: NOT adding cross signing signature", ); } - } else { + } else if (!newKeyId) { logger.log("Secret storage default key not found, creating new key"); const keyOptions = await createSecretStorageKey(); newKeyId = await this.addSecretStorageKey( SECRET_STORAGE_ALGORITHM_V1_AES, keyOptions, ); + await this.setDefaultSecretStorageKeyId(newKeyId); } - await this.setDefaultSecretStorageKeyId(newKeyId); } else { logger.log("Have secret storage key"); } @@ -528,6 +580,10 @@ Crypto.prototype.hasSecretStorageKey = function(keyID) { return this._secretStorage.hasKey(keyID); }; +Crypto.prototype.getSecretStorageKey = function(keyID) { + return this._secretStorage.getKey(keyID); +}; + Crypto.prototype.storeSecret = function(name, secret, keys) { return this._secretStorage.store(name, secret, keys); }; diff --git a/src/crypto/key_passphrase.js b/src/crypto/key_passphrase.js index 3e8455472..f033583fb 100644 --- a/src/crypto/key_passphrase.js +++ b/src/crypto/key_passphrase.js @@ -19,6 +19,8 @@ import {randomString} from '../randomstring'; const DEFAULT_ITERATIONS = 500000; +const DEFAULT_BITSIZE = 256; + export async function keyFromAuthData(authData, password) { if (!global.Olm) { throw new Error("Olm is not available"); @@ -34,6 +36,7 @@ export async function keyFromAuthData(authData, password) { return await deriveKey( password, authData.private_key_salt, authData.private_key_iterations, + authData.private_key_bits || DEFAULT_BITSIZE, ); } @@ -44,12 +47,12 @@ export async function keyFromPassphrase(password) { const salt = randomString(32); - const key = await deriveKey(password, salt, DEFAULT_ITERATIONS); + const key = await deriveKey(password, salt, DEFAULT_ITERATIONS, DEFAULT_BITSIZE); return { key, salt, iterations: DEFAULT_ITERATIONS }; } -export async function deriveKey(password, salt, iterations) { +export async function deriveKey(password, salt, iterations, numBits) { const subtleCrypto = global.crypto.subtle; const TextEncoder = global.TextEncoder; if (!subtleCrypto || !TextEncoder) { @@ -73,7 +76,7 @@ export async function deriveKey(password, salt, iterations) { hash: 'SHA-512', }, key, - global.Olm.PRIVATE_KEY_LENGTH * 8, + numBits, ); return new Uint8Array(keybits); From c6b5936f8a1cd9fb84e6030b554fab9069849eb9 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Fri, 28 Feb 2020 16:09:24 -0500 Subject: [PATCH 04/21] use the right operator --- src/crypto/CrossSigning.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/crypto/CrossSigning.js b/src/crypto/CrossSigning.js index cd4a48845..9ed639f31 100644 --- a/src/crypto/CrossSigning.js +++ b/src/crypto/CrossSigning.js @@ -116,7 +116,8 @@ export class CrossSigningInfo extends EventEmitter { * key */ async isStoredInSecretStorage(secretStorage) { - const stored = await secretStorage.isStored("m.cross_signing.master", false) | {}; + const stored = + await secretStorage.isStored("m.cross_signing.master", false) || {}; function intersect(s) { for (const k of Object.keys(stored)) { if (!s[k]) { From e3735082115b2d73bb82a0441691b72759f57f07 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Thu, 12 Mar 2020 18:08:54 -0400 Subject: [PATCH 05/21] some fixes in SSSS migration --- src/client.js | 6 ++-- src/crypto/SecretStorage.js | 4 +-- src/crypto/index.js | 69 +++++++++++++++++++++++++----------- src/crypto/key_passphrase.js | 2 +- 4 files changed, 56 insertions(+), 25 deletions(-) diff --git a/src/client.js b/src/client.js index 954ec1971..56a717411 100644 --- a/src/client.js +++ b/src/client.js @@ -217,8 +217,10 @@ function keyFromRecoverySession(session, decryptionKey) { * Args: * {object} keys Information about the keys: * { - * : { - * pubkey: {UInt8Array} + * keys: { + * : { + * pubkey: {UInt8Array} + * }, ... * } * } * {string} name the name of the value we want to read out of SSSS, for UI purposes. diff --git a/src/crypto/SecretStorage.js b/src/crypto/SecretStorage.js index b8a45694d..d2374b4de 100644 --- a/src/crypto/SecretStorage.js +++ b/src/crypto/SecretStorage.js @@ -380,7 +380,7 @@ export class SecretStorage extends EventEmitter { return null; } - const keyInfo = this._baseApis.getAccountDataFromServer( + const keyInfo = await this._baseApis.getAccountDataFromServer( "m.secret_storage.key." + keyId, ); return keyInfo ? [keyId, keyInfo] : null; @@ -850,7 +850,7 @@ export class SecretStorage extends EventEmitter { ); } const decryption = { - free: pkDecryption.free().bind(pkDecryption), + free: pkDecryption.free.bind(pkDecryption), decrypt: async function(encInfo) { return pkDecryption.decrypt( encInfo.ephemeral, encInfo.mac, encInfo.ciphertext, diff --git a/src/crypto/index.js b/src/crypto/index.js index aa7f89fc8..86924f354 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -395,7 +395,7 @@ Crypto.prototype.bootstrapSecretStorage = async function({ const ssssKeys = {}; this._baseApis._cryptoCallbacks.getSecretStorageKey = - async (keys, name) => { + async ({keys}, name) => { // if we already have a key that works, return it for (const keyId of Object.keys(keys)) { if (ssssKeys[keyId]) { @@ -404,7 +404,7 @@ Crypto.prototype.bootstrapSecretStorage = async function({ } // otherwise, prompt the user and cache it - const key = await appCallbacks.getSecretStorageKey(keys, name); + const key = await appCallbacks.getSecretStorageKey({keys}, name); if (key) { const [keyId, keyData] = key; ssssKeys[keyId] = keyData; @@ -422,7 +422,51 @@ Crypto.prototype.bootstrapSecretStorage = async function({ "Cross-signing public and/or private keys not found, " + "checking secret storage for private keys", ); - if (inStorage) { + if (!(Object.values(decryptionKeys).some( + info => info.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES, + ))) { + // we already have cross-signing keys, but they're encrypted using + // the old algorithm + logger.log("Switching to symmetric"); + const keys = {}; + // fetch the cross-signing private keys (needed to sign the + // new SSSS key) + this._baseApis._cryptoCallbacks.getCrossSigningKey = + name => crossSigningPrivateKeys[name]; + for (const type of ["master", "self_signing", "user_signing"]) { + const secretName = `m.cross_signing.${type}`; + const secret = await this.getSecret(secretName); + keys[type] = secret; + crossSigningPrivateKeys[type] = olmlib.decodeBase64(secret); + } + await this.checkOwnCrossSigningTrust(); + const opts = {}; + let oldKeyId = null; + for (const [keyId, keyInfo] of Object.entries(decryptionKeys)) { + // See if the old key was generated from a passphrase. If + // yes, use the same settings. + if (keyId in ssssKeys) { + oldKeyId = keyId; + if (keyInfo.passphrase) { + opts.passphrase = keyInfo.passphrase; + } + break; + } + } + // create new symmetric SSSS key and set it as default + newKeyId = await this.addSecretStorageKey( + SECRET_STORAGE_ALGORITHM_V1_AES, opts, + ); + if (oldKeyId) { + ssssKeys[newKeyId] = ssssKeys[oldKeyId]; + } + await this.setDefaultSecretStorageKeyId(newKeyId); + // re-encrypt all the keys with the new key + for (const type of ["master", "self_signing", "user_signing"]) { + const secretName = `m.cross_signing.${type}`; + await this.storeSecret(secretName, keys[type], [newKeyId]); + } + } else if (inStorage) { logger.log("Cross-signing private keys found in secret storage"); await this.checkOwnCrossSigningTrust(); } else { @@ -439,21 +483,6 @@ Crypto.prototype.bootstrapSecretStorage = async function({ { authUploadDeviceSigningKeys }, ); } - } else if (!(Object.values(decryptionKeys).some( - info => info.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES, - ))) { - // we already have cross-signing keys, but they're encrypted using - // the old algorithm - logger.log("Switching to symmetric"); - // create new symmetric key and set it as default - newKeyId = this.addSecretStorageKey(); - this.setDefaultSecretStorageKeyId(newKeyId); - // re-encrypt all the keys with the new key - for (const type of ["master", "self_signing", "user_signing"]) { - const secretName = `m.cross_signing.${type}`; - const secret = this.getSecret(secretName); - this.storeSecret(secretName, secret, [newKeyId]); - } } else { logger.log("Cross signing keys are present in secret storage"); } @@ -501,7 +530,7 @@ Crypto.prototype.bootstrapSecretStorage = async function({ // so the key backup can be trusted via cross-signing. const backupSigStatus = await this.checkKeyBackup(keyBackupInfo); if (backupSigStatus.trustInfo.usable) { - console.log("Adding cross signing signature to key backup"); + logger.log("Adding cross signing signature to key backup"); await this._crossSigningInfo.signObject( keyBackupInfo.auth_data, "master", ); @@ -512,7 +541,7 @@ Crypto.prototype.bootstrapSecretStorage = async function({ ); await this.storeSecret("m.megolm_backup.v1", backupKey, [newKeyId]); } else { - console.log( + logger.log( "Key backup is NOT TRUSTED: NOT adding cross signing signature", ); } diff --git a/src/crypto/key_passphrase.js b/src/crypto/key_passphrase.js index f033583fb..47c964726 100644 --- a/src/crypto/key_passphrase.js +++ b/src/crypto/key_passphrase.js @@ -52,7 +52,7 @@ export async function keyFromPassphrase(password) { return { key, salt, iterations: DEFAULT_ITERATIONS }; } -export async function deriveKey(password, salt, iterations, numBits) { +export async function deriveKey(password, salt, iterations, numBits = DEFAULT_BITSIZE) { const subtleCrypto = global.crypto.subtle; const TextEncoder = global.TextEncoder; if (!subtleCrypto || !TextEncoder) { From 45a88f0517e948fedc2a5ba1e08e4416be5de71f Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Mon, 16 Mar 2020 11:00:11 -0400 Subject: [PATCH 06/21] add function to check that secret storage needs upgrading --- src/client.js | 1 + src/crypto/SecretStorage.js | 9 +++++++++ src/crypto/index.js | 4 ++++ 3 files changed, 14 insertions(+) diff --git a/src/client.js b/src/client.js index 56a717411..4ff5bc784 100644 --- a/src/client.js +++ b/src/client.js @@ -1280,6 +1280,7 @@ wrapCryptoFuncs(MatrixClient, [ "bootstrapSecretStorage", "addSecretStorageKey", "hasSecretStorageKey", + "secretStorageKeyNeedsUpgrade", "storeSecret", "getSecret", "isSecretStored", diff --git a/src/crypto/SecretStorage.js b/src/crypto/SecretStorage.js index d2374b4de..d4d45e7c7 100644 --- a/src/crypto/SecretStorage.js +++ b/src/crypto/SecretStorage.js @@ -397,6 +397,15 @@ export class SecretStorage extends EventEmitter { return !!(await this.getKey(keyId)); } + async keyNeedsUpgrade(keyId) { + const keyInfo = await this.getKey(keyId); + if (keyInfo && keyInfo[1].algorithm === SECRET_STORAGE_ALGORITHM_V1_CURVE25519) { + return true; + } else { + return false; + } + } + /** * Store an encrypted secret on the server * diff --git a/src/crypto/index.js b/src/crypto/index.js index 86924f354..b6dbe99a8 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -609,6 +609,10 @@ Crypto.prototype.hasSecretStorageKey = function(keyID) { return this._secretStorage.hasKey(keyID); }; +Crypto.prototype.secretStorageKeyNeedsUpgrade = function(keyID) { + return this._secretStorage.keyNeedsUpgrade(keyID); +}; + Crypto.prototype.getSecretStorageKey = function(keyID) { return this._secretStorage.getKey(keyID); }; From c8c6444f6a2779467c35e70f62ba84441986e2ac Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Mon, 16 Mar 2020 11:05:07 -0400 Subject: [PATCH 07/21] migrate backup key from asymmetric SSSS to symmetric SSSS --- src/crypto/index.js | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/src/crypto/index.js b/src/crypto/index.js index b6dbe99a8..87621debc 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -545,14 +545,23 @@ Crypto.prototype.bootstrapSecretStorage = async function({ "Key backup is NOT TRUSTED: NOT adding cross signing signature", ); } - } else if (!newKeyId) { - logger.log("Secret storage default key not found, creating new key"); - const keyOptions = await createSecretStorageKey(); - newKeyId = await this.addSecretStorageKey( - SECRET_STORAGE_ALGORITHM_V1_AES, - keyOptions, - ); - await this.setDefaultSecretStorageKeyId(newKeyId); + } else { + if (!newKeyId) { + logger.log("Secret storage default key not found, creating new key"); + const keyOptions = await createSecretStorageKey(); + newKeyId = await this.addSecretStorageKey( + SECRET_STORAGE_ALGORITHM_V1_AES, + keyOptions, + ); + await this.setDefaultSecretStorageKeyId(newKeyId); + } + if (await this.isSecretStored("m.megolm_backup.v1")) { + // we created a new SSSS, and we previously encrypted the + // backup key with the old SSSS key, so re-encrypt with the + // new key + const backupKey = await this.getSecret("m.megolm_backup.v1"); + await this.storeSecret("m.megolm_backup.v1", backupKey, [newKeyId]); + } } } else { logger.log("Have secret storage key"); From 1b24d55b24d15b3562184c75dbcf0cb0a44ffe7f Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Mon, 16 Mar 2020 17:20:54 -0400 Subject: [PATCH 08/21] misc fixes and cleanups --- src/crypto/index.js | 125 +++++++++++++++++++++++--------------------- 1 file changed, 64 insertions(+), 61 deletions(-) diff --git a/src/crypto/index.js b/src/crypto/index.js index 87621debc..ef8fc167d 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -416,57 +416,57 @@ Crypto.prototype.bootstrapSecretStorage = async function({ const decryptionKeys = await this._crossSigningInfo.isStoredInSecretStorage(this._secretStorage); const inStorage = !setupNewSecretStorage && decryptionKeys; - if (!this._crossSigningInfo.getId() || !inStorage) { + if (!(Object.values(decryptionKeys).some( + info => info.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES, + ))) { + // we already have cross-signing keys, but they're encrypted using + // the old algorithm + logger.log("Switching to symmetric"); + const keys = {}; + // fetch the cross-signing private keys (needed to sign the + // new SSSS key) + this._baseApis._cryptoCallbacks.getCrossSigningKey = + name => crossSigningPrivateKeys[name]; + for (const type of ["master", "self_signing", "user_signing"]) { + const secretName = `m.cross_signing.${type}`; + const secret = await this.getSecret(secretName); + keys[type] = secret; + crossSigningPrivateKeys[type] = olmlib.decodeBase64(secret); + } + await this.checkOwnCrossSigningTrust(); + const opts = {}; + let oldKeyId = null; + for (const [keyId, keyInfo] of Object.entries(decryptionKeys)) { + // See if the old key was generated from a passphrase. If + // yes, use the same settings. + if (keyId in ssssKeys) { + oldKeyId = keyId; + if (keyInfo.passphrase) { + opts.passphrase = keyInfo.passphrase; + } + break; + } + } + // create new symmetric SSSS key and set it as default + newKeyId = await this.addSecretStorageKey( + SECRET_STORAGE_ALGORITHM_V1_AES, opts, + ); + if (oldKeyId) { + ssssKeys[newKeyId] = ssssKeys[oldKeyId]; + } + await this.setDefaultSecretStorageKeyId(newKeyId); + // re-encrypt all the keys with the new key + for (const type of ["master", "self_signing", "user_signing"]) { + const secretName = `m.cross_signing.${type}`; + await this.storeSecret(secretName, keys[type], [newKeyId]); + } + } else if (!this._crossSigningInfo.getId() || !inStorage) { // create new cross-signing keys if necessary. logger.log( "Cross-signing public and/or private keys not found, " + "checking secret storage for private keys", ); - if (!(Object.values(decryptionKeys).some( - info => info.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES, - ))) { - // we already have cross-signing keys, but they're encrypted using - // the old algorithm - logger.log("Switching to symmetric"); - const keys = {}; - // fetch the cross-signing private keys (needed to sign the - // new SSSS key) - this._baseApis._cryptoCallbacks.getCrossSigningKey = - name => crossSigningPrivateKeys[name]; - for (const type of ["master", "self_signing", "user_signing"]) { - const secretName = `m.cross_signing.${type}`; - const secret = await this.getSecret(secretName); - keys[type] = secret; - crossSigningPrivateKeys[type] = olmlib.decodeBase64(secret); - } - await this.checkOwnCrossSigningTrust(); - const opts = {}; - let oldKeyId = null; - for (const [keyId, keyInfo] of Object.entries(decryptionKeys)) { - // See if the old key was generated from a passphrase. If - // yes, use the same settings. - if (keyId in ssssKeys) { - oldKeyId = keyId; - if (keyInfo.passphrase) { - opts.passphrase = keyInfo.passphrase; - } - break; - } - } - // create new symmetric SSSS key and set it as default - newKeyId = await this.addSecretStorageKey( - SECRET_STORAGE_ALGORITHM_V1_AES, opts, - ); - if (oldKeyId) { - ssssKeys[newKeyId] = ssssKeys[oldKeyId]; - } - await this.setDefaultSecretStorageKeyId(newKeyId); - // re-encrypt all the keys with the new key - for (const type of ["master", "self_signing", "user_signing"]) { - const secretName = `m.cross_signing.${type}`; - await this.storeSecret(secretName, keys[type], [newKeyId]); - } - } else if (inStorage) { + if (inStorage) { logger.log("Cross-signing private keys found in secret storage"); await this.checkOwnCrossSigningTrust(); } else { @@ -500,24 +500,25 @@ Crypto.prototype.bootstrapSecretStorage = async function({ // if we already have a backup key, use the same key as the // secret storage key logger.log("Secret storage default key not found, using key backup key"); - const opts = {}; // FIXME: ask for recovery passphrase/key - const backupKey = "foobar"; - - if ( - keyBackupInfo.auth_data.private_key_salt && - keyBackupInfo.auth_data.private_key_iterations - ) { - opts.passphrase = { - algorithm: "m.pbkdf2", - iterations: keyBackupInfo.auth_data.private_key_iterations, - salt: keyBackupInfo.auth_data.private_key_salt, - bits: 256, - }; - } + const backupKey = Buffer.from("XrmITOOdBhw6yY5Bh7trb/bgp1FRdIGyCUxxMP873R0", "base64"); if (!newKeyId) { + const opts = {}; + + if ( + keyBackupInfo.auth_data.private_key_salt && + keyBackupInfo.auth_data.private_key_iterations + ) { + opts.passphrase = { + algorithm: "m.pbkdf2", + iterations: keyBackupInfo.auth_data.private_key_iterations, + salt: keyBackupInfo.auth_data.private_key_salt, + bits: 256, + }; + } + newKeyId = await this.addSecretStorageKey( SECRET_STORAGE_ALGORITHM_V1_AES, opts, ); @@ -539,7 +540,9 @@ Crypto.prototype.bootstrapSecretStorage = async function({ undefined, keyBackupInfo, {prefix: httpApi.PREFIX_UNSTABLE}, ); - await this.storeSecret("m.megolm_backup.v1", backupKey, [newKeyId]); + await this.storeSecret( + "m.megolm_backup.v1", olmlib.encodeBase64(backupKey), [newKeyId], + ); } else { logger.log( "Key backup is NOT TRUSTED: NOT adding cross signing signature", From 3b06b0ffc120f228b0141aadf01e2b3dfe41ed3a Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Mon, 16 Mar 2020 17:22:12 -0400 Subject: [PATCH 09/21] fix lint --- src/crypto/index.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/crypto/index.js b/src/crypto/index.js index ef8fc167d..da02e4cdb 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -502,7 +502,9 @@ Crypto.prototype.bootstrapSecretStorage = async function({ logger.log("Secret storage default key not found, using key backup key"); // FIXME: ask for recovery passphrase/key - const backupKey = Buffer.from("XrmITOOdBhw6yY5Bh7trb/bgp1FRdIGyCUxxMP873R0", "base64"); + const backupKey = Buffer.from( + "XrmITOOdBhw6yY5Bh7trb/bgp1FRdIGyCUxxMP873R0", "base64", + ); if (!newKeyId) { const opts = {}; From 0434bf5a485ec63c97e1321b913f5311636389e7 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 19 Mar 2020 20:34:57 +0000 Subject: [PATCH 10/21] Add functions to get the raw key backup key --- src/client.js | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/client.js b/src/client.js index f0e8c44f8..def870edc 100644 --- a/src/client.js +++ b/src/client.js @@ -1730,6 +1730,35 @@ MatrixClient.prototype.isValidRecoveryKey = function(recoveryKey) { } }; +/** + * Get the raw key for a key backup from the password + * Used when migrating key backups into SSSS + * + * The cross-signing API is currently UNSTABLE and may change without notice. + * + * @param {string} password Passphrase + * @param {object} backupInfo Backup metadata from `checkKeyBackup` + * @return {Promise} key backup key + */ +MatrixClient.prototype.keyBackupKeyFromPassword = function( + password, backupInfo, +) { + return keyFromAuthData(backupInfo.auth_data, password); +}; + +/** + * Get the raw key for a key backup from the recovery key + * Used when migrating key backups into SSSS + * + * The cross-signing API is currently UNSTABLE and may change without notice. + * + * @param {string} recoveryKey The recovery key + * @return {Buffer} key backup key + */ +MatrixClient.prototype.keyBackupKeyFromRecoveryKey = function(recoveryKey) { + return decodeRecoveryKey(recoveryKey); +}; + MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY = 'RESTORE_BACKUP_ERROR_BAD_KEY'; /** From db285af0b5a624ed31f59e9d63c29bdc7cbd4ea8 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 19 Mar 2020 20:36:00 +0000 Subject: [PATCH 11/21] Add callback to get the user's current key backup passphrase And also add a null check --- src/crypto/index.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/crypto/index.js b/src/crypto/index.js index e50512fd9..55e6bcfac 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -371,6 +371,9 @@ Crypto.prototype.createRecoveryKeyFromPassphrase = async function(password) { * created and the private key stored in the new SSSS store. Ignored if keyBackupInfo * is supplied. * @param {bool} [opts.setupNewSecretStorage] Optional. Reset even if keys already exist. + * @param {func} [opts.getKeyBackupPassphrase] Optional. Function called to get the user's + * current key backup passphrase. Should return a promise that resolves with a Buffer + * containing the key, or rejects if the key cannot be obtained. * Returns: * {Promise} A promise which resolves to key creation data for * SecretStorage#addKey: an object with `passphrase` and/or `pubkey` fields. @@ -381,6 +384,7 @@ Crypto.prototype.bootstrapSecretStorage = async function({ keyBackupInfo, setupNewKeyBackup, setupNewSecretStorage, + getKeyBackupPassphrase, } = {}) { logger.log("Bootstrapping Secure Secret Storage"); @@ -423,7 +427,7 @@ Crypto.prototype.bootstrapSecretStorage = async function({ const decryptionKeys = await this._crossSigningInfo.isStoredInSecretStorage(this._secretStorage); const inStorage = !setupNewSecretStorage && decryptionKeys; - if (!(Object.values(decryptionKeys).some( + if (decryptionKeys && !(Object.values(decryptionKeys).some( info => info.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES, ))) { // we already have cross-signing keys, but they're encrypted using @@ -508,10 +512,7 @@ Crypto.prototype.bootstrapSecretStorage = async function({ // secret storage key logger.log("Secret storage default key not found, using key backup key"); - // FIXME: ask for recovery passphrase/key - const backupKey = Buffer.from( - "XrmITOOdBhw6yY5Bh7trb/bgp1FRdIGyCUxxMP873R0", "base64", - ); + const backupKey = await getKeyBackupPassphrase(); if (!newKeyId) { const opts = {}; From f1317e824b9879ba9dfac30731c2c11bbb64ac58 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 19 Mar 2020 21:04:36 +0000 Subject: [PATCH 12/21] Don't assume subtleCrypto exists if there's a window Jest has a window object but doesn't have subtleCrypto --- src/crypto/SecretStorage.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/crypto/SecretStorage.js b/src/crypto/SecretStorage.js index d4d45e7c7..920afb1ad 100644 --- a/src/crypto/SecretStorage.js +++ b/src/crypto/SecretStorage.js @@ -28,8 +28,8 @@ export const SECRET_STORAGE_ALGORITHM_V1_AES export const SECRET_STORAGE_ALGORITHM_V1_CURVE25519 = "m.secret_storage.v1.curve25519-aes-sha2"; -const subtleCrypto = typeof window === "undefined" ? null : - (window.crypto.subtle || window.crypto.webkitSubtle); +const subtleCrypto = (window && window.crypto) ? + (window.crypto.subtle || window.crypto.webkitSubtle) : null; // salt for HKDF, with 8 bytes of zeros const zerosalt = new Uint8Array(8); From ddce14b20b0e501d389d4a0d8aebe9b95ea499c2 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 19 Mar 2020 21:12:57 +0000 Subject: [PATCH 13/21] Use the typeof test to avoid undefined --- src/crypto/SecretStorage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/crypto/SecretStorage.js b/src/crypto/SecretStorage.js index 920afb1ad..a56939e96 100644 --- a/src/crypto/SecretStorage.js +++ b/src/crypto/SecretStorage.js @@ -28,7 +28,7 @@ export const SECRET_STORAGE_ALGORITHM_V1_AES export const SECRET_STORAGE_ALGORITHM_V1_CURVE25519 = "m.secret_storage.v1.curve25519-aes-sha2"; -const subtleCrypto = (window && window.crypto) ? +const subtleCrypto = (typeof window !== "undefined" && window.crypto) ? (window.crypto.subtle || window.crypto.webkitSubtle) : null; // salt for HKDF, with 8 bytes of zeros From 89bf9ff65b6b818823d677678e315a6704a5162c Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 23 Mar 2020 18:40:53 +0000 Subject: [PATCH 14/21] doc style fix --- src/crypto/SecretStorage.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/crypto/SecretStorage.js b/src/crypto/SecretStorage.js index a56939e96..6006ad199 100644 --- a/src/crypto/SecretStorage.js +++ b/src/crypto/SecretStorage.js @@ -34,7 +34,8 @@ const subtleCrypto = (typeof window !== "undefined" && window.crypto) ? // salt for HKDF, with 8 bytes of zeros const zerosalt = new Uint8Array(8); -/** encrypt a string in Node.js +/** + * encrypt a string in Node.js * * @param {string} data the plaintext to encrypt * @param {Uint8Array} key the encryption key to use From 0a7b9109f034e3a0033ae1509d4997aa90078bd1 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 23 Mar 2020 18:56:32 +0000 Subject: [PATCH 15/21] Move aes functions to their own file --- src/crypto/SecretStorage.js | 215 +------------------------------- src/crypto/aes.js | 236 ++++++++++++++++++++++++++++++++++++ 2 files changed, 237 insertions(+), 214 deletions(-) create mode 100644 src/crypto/aes.js diff --git a/src/crypto/SecretStorage.js b/src/crypto/SecretStorage.js index 6006ad199..0c6de6236 100644 --- a/src/crypto/SecretStorage.js +++ b/src/crypto/SecretStorage.js @@ -19,8 +19,7 @@ import {logger} from '../logger'; import * as olmlib from './olmlib'; import {pkVerify} from './olmlib'; import {randomString} from '../randomstring'; -import {decodeBase64, encodeBase64} from './olmlib'; -import {getCrypto} from '../utils'; +import {encryptAES, decryptAES} from './aes'; export const SECRET_STORAGE_ALGORITHM_V1_AES = "m.secret_storage.v1.aes-hmac-sha2"; @@ -28,218 +27,6 @@ export const SECRET_STORAGE_ALGORITHM_V1_AES export const SECRET_STORAGE_ALGORITHM_V1_CURVE25519 = "m.secret_storage.v1.curve25519-aes-sha2"; -const subtleCrypto = (typeof window !== "undefined" && window.crypto) ? - (window.crypto.subtle || window.crypto.webkitSubtle) : null; - -// salt for HKDF, with 8 bytes of zeros -const zerosalt = new Uint8Array(8); - -/** - * 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 - */ -async function encryptNode(data, key, name) { - const crypto = getCrypto(); - if (!crypto) { - throw new Error("No usable crypto implementation"); - } - - const 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 = cipher.update(data, "utf-8", "base64") - + cipher.final("base64"); - - const hmac = crypto.createHmac("sha256", hmacKey) - .update(ciphertext, "base64").digest("base64"); - - return { - iv: encodeBase64(iv), - ciphertext: ciphertext, - 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, key, name) { - 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(data.ciphertext, "base64").digest("base64"); - - if (hmac !== data.mac) { - 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", "utf-8") - + decipher.final("utf-8"); -} - -function deriveKeysNode(key, name) { - 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, "utf-8").update(b).digest(); - b[0] = 2; - const hmacKey = crypto.createHmac("sha256", prk) - .update(aesKey).update(name, "utf-8").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 - */ -async function encryptBrowser(data, key, name) { - const iv = new Uint8Array(16); - window.crypto.getRandomValues(iv); - - // clear bit 63 of the IV to stop us hitting the 64-bit counter boundary - // (which would mean we wouldn't be able to decrypt on Android). The loss - // of a single bit of iv is a price we have to pay. - iv[8] &= 0x7f; - - const [aesKey, hmacKey] = await deriveKeysBrowser(key, name); - const encodedData = new TextEncoder().encode(data); - - const ciphertext = await subtleCrypto.encrypt( - { - name: "AES-CTR", - counter: iv, - length: 64, - }, - aesKey, - encodedData, - ); - - const hmac = await subtleCrypto.sign( - {name: 'HMAC'}, - hmacKey, - ciphertext, - ); - - return { - iv: encodeBase64(iv), - ciphertext: encodeBase64(ciphertext), - mac: encodeBase64(hmac), - }; -} - -/** decrypt a string in the browser - * - * @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 decryptBrowser(data, key, name) { - const [aesKey, hmacKey] = await deriveKeysBrowser(key, name); - - const ciphertext = decodeBase64(data.ciphertext); - - if (!await subtleCrypto.verify( - {name: "HMAC"}, - hmacKey, - decodeBase64(data.mac), - ciphertext, - )) { - throw new Error(`Error decrypting secret ${name}: bad MAC`); - } - - const plaintext = await subtleCrypto.decrypt( - { - name: "AES-CTR", - counter: decodeBase64(data.iv), - length: 64, - }, - aesKey, - ciphertext, - ); - - return new TextDecoder().decode(new Uint8Array(plaintext)); -} - -async function deriveKeysBrowser(key, name) { - const hkdfkey = await subtleCrypto.importKey( - 'raw', - key, - {name: "HKDF"}, - false, - ["deriveBits"], - ); - const keybits = await subtleCrypto.deriveBits( - { - name: "HKDF", - salt: zerosalt, - info: (new TextEncoder().encode(name)), - hash: "SHA-256", - }, - hkdfkey, - 512, - ); - - const aesKey = keybits.slice(0, 32); - const hmacKey = keybits.slice(32); - - const aesProm = subtleCrypto.importKey( - 'raw', - aesKey, - {name: 'AES-CTR'}, - false, - ['encrypt', 'decrypt'], - ); - - const hmacProm = subtleCrypto.importKey( - 'raw', - hmacKey, - { - name: 'HMAC', - hash: {name: 'SHA-256'}, - }, - false, - ['sign', 'verify'], - ); - - return await Promise.all([aesProm, hmacProm]); -} - -const [encryptAES, decryptAES] = (typeof window === "undefined") ? - [encryptNode, decryptNode] : [encryptBrowser, decryptBrowser]; - /** * Implements Secure Secret Storage and Sharing (MSC1946) * @module crypto/SecretStorage diff --git a/src/crypto/aes.js b/src/crypto/aes.js new file mode 100644 index 000000000..d248a98bb --- /dev/null +++ b/src/crypto/aes.js @@ -0,0 +1,236 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import {getCrypto} from '../utils'; +import {decodeBase64, encodeBase64} from './olmlib'; + +const subtleCrypto = (typeof window !== "undefined" && window.crypto) ? + (window.crypto.subtle || window.crypto.webkitSubtle) : null; + +// salt for HKDF, with 8 bytes of zeros +const zerosalt = new Uint8Array(8); + +/** + * 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 + */ +async function encryptNode(data, key, name) { + const crypto = getCrypto(); + if (!crypto) { + throw new Error("No usable crypto implementation"); + } + + const 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 = cipher.update(data, "utf-8", "base64") + + cipher.final("base64"); + + const hmac = crypto.createHmac("sha256", hmacKey) + .update(ciphertext, "base64").digest("base64"); + + return { + iv: encodeBase64(iv), + ciphertext: ciphertext, + 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, key, name) { + 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(data.ciphertext, "base64").digest("base64"); + + if (hmac !== data.mac) { + 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", "utf-8") + + decipher.final("utf-8"); +} + +function deriveKeysNode(key, name) { + 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, "utf-8").update(b).digest(); + b[0] = 2; + const hmacKey = crypto.createHmac("sha256", prk) + .update(aesKey).update(name, "utf-8").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 + */ +async function encryptBrowser(data, key, name) { + const iv = new Uint8Array(16); + window.crypto.getRandomValues(iv); + + // clear bit 63 of the IV to stop us hitting the 64-bit counter boundary + // (which would mean we wouldn't be able to decrypt on Android). The loss + // of a single bit of iv is a price we have to pay. + iv[8] &= 0x7f; + + const [aesKey, hmacKey] = await deriveKeysBrowser(key, name); + const encodedData = new TextEncoder().encode(data); + + const ciphertext = await subtleCrypto.encrypt( + { + name: "AES-CTR", + counter: iv, + length: 64, + }, + aesKey, + encodedData, + ); + + const hmac = await subtleCrypto.sign( + {name: 'HMAC'}, + hmacKey, + ciphertext, + ); + + return { + iv: encodeBase64(iv), + ciphertext: encodeBase64(ciphertext), + mac: encodeBase64(hmac), + }; +} + +/** decrypt a string in the browser + * + * @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 decryptBrowser(data, key, name) { + const [aesKey, hmacKey] = await deriveKeysBrowser(key, name); + + const ciphertext = decodeBase64(data.ciphertext); + + if (!await subtleCrypto.verify( + {name: "HMAC"}, + hmacKey, + decodeBase64(data.mac), + ciphertext, + )) { + throw new Error(`Error decrypting secret ${name}: bad MAC`); + } + + const plaintext = await subtleCrypto.decrypt( + { + name: "AES-CTR", + counter: decodeBase64(data.iv), + length: 64, + }, + aesKey, + ciphertext, + ); + + return new TextDecoder().decode(new Uint8Array(plaintext)); +} + +async function deriveKeysBrowser(key, name) { + const hkdfkey = await subtleCrypto.importKey( + 'raw', + key, + {name: "HKDF"}, + false, + ["deriveBits"], + ); + const keybits = await subtleCrypto.deriveBits( + { + name: "HKDF", + salt: zerosalt, + info: (new TextEncoder().encode(name)), + hash: "SHA-256", + }, + hkdfkey, + 512, + ); + + const aesKey = keybits.slice(0, 32); + const hmacKey = keybits.slice(32); + + const aesProm = subtleCrypto.importKey( + 'raw', + aesKey, + {name: 'AES-CTR'}, + false, + ['encrypt', 'decrypt'], + ); + + const hmacProm = subtleCrypto.importKey( + 'raw', + hmacKey, + { + name: 'HMAC', + hash: {name: 'SHA-256'}, + }, + false, + ['sign', 'verify'], + ); + + return await Promise.all([aesProm, hmacProm]); +} + +export function encryptAES(...args) { + return subtleCrypto ? encryptBrowser(...args) : encryptNode(...args); +} + +export function decryptAES(...args) { + return subtleCrypto ? decryptBrowser(...args) : decryptNode(...args); +} + From d9796e3becf95d61d05e26dd18e1cebbb3d73586 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 23 Mar 2020 19:00:02 +0000 Subject: [PATCH 16/21] Fix indenting --- src/crypto/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/crypto/index.js b/src/crypto/index.js index 55e6bcfac..e863a8eff 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -519,7 +519,7 @@ Crypto.prototype.bootstrapSecretStorage = async function({ if ( keyBackupInfo.auth_data.private_key_salt && - keyBackupInfo.auth_data.private_key_iterations + keyBackupInfo.auth_data.private_key_iterations ) { opts.passphrase = { algorithm: "m.pbkdf2", From 944d39c8367350cf62248b597b2ca99d7325a30b Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Mon, 23 Mar 2020 16:51:44 -0400 Subject: [PATCH 17/21] add some comments --- src/client.js | 8 ++++++-- src/crypto/CrossSigning.js | 2 ++ src/crypto/index.js | 6 ++++-- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/client.js b/src/client.js index def870edc..a15778899 100644 --- a/src/client.js +++ b/src/client.js @@ -1242,7 +1242,9 @@ MatrixClient.prototype.checkEventSenderTrust = async function(event) { * @param {boolean} checkKey check if the secret is encrypted by a trusted * key * - * @return {boolean} whether or not the secret is stored + * @return {object?} map of key name to key info the secret is encrypted + * with, or null if it is not present or not encrypted with a trusted + * key */ /** @@ -1577,7 +1579,9 @@ MatrixClient.prototype.prepareKeyBackupVersion = async function( /** * Check whether the key backup private key is stored in secret storage. - * @return {Promise} Whether the backup key is stored. + * @return {Promise} map of key name to key info the secret is + * encrypted with, or null if it is not present or not encrypted with a + * trusted key */ MatrixClient.prototype.isKeyBackupKeyStored = async function() { return this.isSecretStored("m.megolm_backup.v1", false /* checkKey */); diff --git a/src/crypto/CrossSigning.js b/src/crypto/CrossSigning.js index f1287f308..2ce7a1989 100644 --- a/src/crypto/CrossSigning.js +++ b/src/crypto/CrossSigning.js @@ -146,8 +146,10 @@ export class CrossSigningInfo extends EventEmitter { * key */ async isStoredInSecretStorage(secretStorage) { + // check what SSSS keys have encrypted the master key (if any) const stored = await secretStorage.isStored("m.cross_signing.master", false) || {}; + // then check which of those SSSS keys have also encryypted the SSK and USK function intersect(s) { for (const k of Object.keys(stored)) { if (!s[k]) { diff --git a/src/crypto/index.js b/src/crypto/index.js index e863a8eff..61825df21 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -434,8 +434,10 @@ Crypto.prototype.bootstrapSecretStorage = async function({ // the old algorithm logger.log("Switching to symmetric"); const keys = {}; - // fetch the cross-signing private keys (needed to sign the - // new SSSS key) + // fetch the cross-signing private keys (needed to sign the new + // SSSS key). We store the cross-signing keys, and temporarily set + // a callback so that when the private key is needed while setting + // things up, we can provide it. this._baseApis._cryptoCallbacks.getCrossSigningKey = name => crossSigningPrivateKeys[name]; for (const type of ["master", "self_signing", "user_signing"]) { From 8f776807508d1f162de988536aaf2a79474482ad Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 24 Mar 2020 13:05:15 +0000 Subject: [PATCH 18/21] Typo Co-Authored-By: J. Ryan Stinnett --- src/crypto/CrossSigning.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/crypto/CrossSigning.js b/src/crypto/CrossSigning.js index 2ce7a1989..117aca373 100644 --- a/src/crypto/CrossSigning.js +++ b/src/crypto/CrossSigning.js @@ -149,7 +149,7 @@ export class CrossSigningInfo extends EventEmitter { // check what SSSS keys have encrypted the master key (if any) const stored = await secretStorage.isStored("m.cross_signing.master", false) || {}; - // then check which of those SSSS keys have also encryypted the SSK and USK + // then check which of those SSSS keys have also encrypted the SSK and USK function intersect(s) { for (const k of Object.keys(stored)) { if (!s[k]) { From 71740cabb5ef72594086d9df267eaf235102016d Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 24 Mar 2020 13:06:08 +0000 Subject: [PATCH 19/21] comment formatting --- src/crypto/aes.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/crypto/aes.js b/src/crypto/aes.js index d248a98bb..1632f3a3f 100644 --- a/src/crypto/aes.js +++ b/src/crypto/aes.js @@ -59,7 +59,8 @@ async function encryptNode(data, key, name) { }; } -/** decrypt a string in Node.js +/** + * decrypt a string in Node.js * * @param {object} data the encrypted data * @param {string} data.ciphertext the ciphertext in base64 From 859a0d8db21f586f4cd9dac091cdc69c11ca3ee8 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 24 Mar 2020 13:08:12 +0000 Subject: [PATCH 20/21] More comment formatting --- src/crypto/aes.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/crypto/aes.js b/src/crypto/aes.js index 1632f3a3f..0188f8ac2 100644 --- a/src/crypto/aes.js +++ b/src/crypto/aes.js @@ -106,7 +106,8 @@ function deriveKeysNode(key, name) { return [aesKey, hmacKey]; } -/** encrypt a string in Node.js +/** + * encrypt a string in Node.js * * @param {string} data the plaintext to encrypt * @param {Uint8Array} key the encryption key to use From 7e92f0e5c8dca41857c40e39dd4f3c99b621b036 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 24 Mar 2020 13:08:49 +0000 Subject: [PATCH 21/21] OK, that really is all the comment formatting --- src/crypto/aes.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/crypto/aes.js b/src/crypto/aes.js index 0188f8ac2..5ce7d77c9 100644 --- a/src/crypto/aes.js +++ b/src/crypto/aes.js @@ -148,7 +148,8 @@ async function encryptBrowser(data, key, name) { }; } -/** decrypt a string in the browser +/** + * decrypt a string in the browser * * @param {object} data the encrypted data * @param {string} data.ciphertext the ciphertext in base64