Merge pull request #1272 from matrix-org/dbkr/symmetric-ssss-migrate

Migration to symmetric SSSS
This commit is contained in:
David Baker
2020-03-24 13:12:17 +00:00
committed by GitHub
9 changed files with 639 additions and 161 deletions
+36 -10
View File
@@ -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');
@@ -112,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();
@@ -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 () => {};
+40 -4
View File
@@ -217,8 +217,10 @@ function keyFromRecoverySession(session, decryptionKey) {
* Args:
* {object} keys Information about the keys:
* {
* <key name>: {
* pubkey: {UInt8Array}
* keys: {
* <key name>: {
* pubkey: {UInt8Array}
* }, ...
* }
* }
* {string} name the name of the value we want to read out of SSSS, for UI purposes.
@@ -1247,7 +1249,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
*/
/**
@@ -1295,6 +1299,7 @@ wrapCryptoFuncs(MatrixClient, [
"bootstrapSecretStorage",
"addSecretStorageKey",
"hasSecretStorageKey",
"secretStorageKeyNeedsUpgrade",
"storeSecret",
"getSecret",
"isSecretStored",
@@ -1581,7 +1586,9 @@ MatrixClient.prototype.prepareKeyBackupVersion = async function(
/**
* Check whether the key backup private key is stored in secret storage.
* @return {Promise<boolean>} Whether the backup key is stored.
* @return {Promise<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
*/
MatrixClient.prototype.isKeyBackupKeyStored = async function() {
return this.isSecretStored("m.megolm_backup.v1", false /* checkKey */);
@@ -1734,6 +1741,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<Buffer>} 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';
/**
+19 -5
View File
@@ -141,14 +141,28 @@ 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);
// 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 encrypted the SSK and USK
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;
}
/**
+135 -102
View File
@@ -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,13 @@ import {logger} from '../logger';
import * as olmlib from './olmlib';
import {pkVerify} from './olmlib';
import {randomString} from '../randomstring';
import {encryptAES, decryptAES} from './aes';
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";
/**
* Implements Secure Secret Storage and Sharing (MSC1946)
@@ -85,20 +90,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();
@@ -155,6 +152,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 = await this._baseApis.getAccountDataFromServer(
"m.secret_storage.key." + keyId,
);
return keyInfo ? [keyId, keyInfo] : null;
}
/**
* Check whether we have a key with a given ID.
*
@@ -163,16 +182,16 @@ export class SecretStorage extends EventEmitter {
* @return {boolean} Whether we have the key.
*/
async hasKey(keyId) {
if (!keyId) {
keyId = await this.getDefaultKeyId();
}
if (!keyId) {
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;
}
return !!this._baseApis.getAccountDataFromServer(
"m.secret_storage.key." + keyId,
);
}
/**
@@ -207,24 +226,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 +246,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 +302,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 +334,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();
}
}
@@ -362,22 +346,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)) {
@@ -385,32 +373,55 @@ 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];
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;
}
ret[keyId] = keyInfo;
continue;
}
switch (keyInfo.algorithm) {
case SECRET_STORAGE_ALGORITHM_V1:
case SECRET_STORAGE_ALGORITHM_V1_AES:
if (encInfo.iv && encInfo.ciphertext && encInfo.mac) {
ret[keyId] = keyInfo;
}
break;
case SECRET_STORAGE_ALGORITHM_V1_CURVE25519:
if (keyInfo.pubkey && encInfo.ciphertext && encInfo.mac
&& encInfo.ephemeral) {
return true;
if (checkKey) {
try {
pkVerify(
keyInfo,
this._crossSigningInfo.getId('master'),
this._crossSigningInfo.userId,
);
} catch (e) {
// not trusted, so move on to the next key
continue;
}
}
ret[keyId] = keyInfo;
}
break;
default:
// do nothing if we don't understand the encryption algorithm
}
}
return false;
return Object.keys(ret).length ? ret : null;
}
/**
@@ -607,26 +618,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);
}
}
}
+239
View File
@@ -0,0 +1,239 @@
/*
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);
}
+143 -37
View File
@@ -39,7 +39,7 @@ import {
UserTrustLevel,
createCryptoStoreCacheCallbacks,
} 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 {
@@ -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");
@@ -395,10 +399,82 @@ 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);
if (!this._crossSigningInfo.getId() || !inStorage) {
const decryptionKeys =
await this._crossSigningInfo.isStoredInSecretStorage(this._secretStorage);
const inStorage = !setupNewSecretStorage && decryptionKeys;
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
// the old algorithm
logger.log("Switching to symmetric");
const keys = {};
// 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"]) {
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",
@@ -424,40 +500,50 @@ Crypto.prototype.bootstrapSecretStorage = async function({
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,
};
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,
};
const backupKey = await getKeyBackupPassphrase();
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,
);
this.setDefaultSecretStorageKeyId(newKeyId);
// use the backup key as the new ssss key
ssssKeys[newKeyId] = backupKey;
}
newKeyId = await this.addSecretStorageKey(
SECRET_STORAGE_ALGORITHM_V1, 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 this key backup is trusted, sign it with the cross signing key
// 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",
);
@@ -466,20 +552,32 @@ Crypto.prototype.bootstrapSecretStorage = async function({
undefined, keyBackupInfo,
{prefix: httpApi.PREFIX_UNSTABLE},
);
await this.storeSecret(
"m.megolm_backup.v1", olmlib.encodeBase64(backupKey), [newKeyId],
);
} else {
console.log(
logger.log(
"Key backup is NOT TRUSTED: NOT adding cross signing signature",
);
}
} else {
logger.log("Secret storage default key not found, creating new key");
const keyOptions = await createSecretStorageKey();
newKeyId = await this.addSecretStorageKey(
SECRET_STORAGE_ALGORITHM_V1,
keyOptions,
);
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]);
}
}
await this.setDefaultSecretStorageKeyId(newKeyId);
} else {
logger.log("Have secret storage key");
}
@@ -535,6 +633,14 @@ 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);
};
Crypto.prototype.storeSecret = function(name, secret, keys) {
return this._secretStorage.store(name, secret, keys);
};
+6 -3
View File
@@ -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 = DEFAULT_BITSIZE) {
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);
+7
View File
@@ -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;
+14
View File
@@ -734,3 +734,17 @@ export async function promiseMapSeries<T>(
export function promiseTry<T>(fn: () => T): Promise<T> {
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;
}