From ca7b49d20916594019d69e3030319da8122e9854 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Thu, 9 Apr 2020 00:04:07 -0400 Subject: [PATCH 1/8] fix incorrect backup key format in SSSS --- spec/unit/crypto/secrets.spec.js | 711 +++++++++++++++++++++++++++---- src/client.js | 14 +- src/crypto/SecretStorage.js | 14 +- src/crypto/index.js | 361 ++++++++++------ 4 files changed, 876 insertions(+), 224 deletions(-) diff --git a/spec/unit/crypto/secrets.spec.js b/spec/unit/crypto/secrets.spec.js index 0a045935ee8..fc17703a65c 100644 --- a/spec/unit/crypto/secrets.spec.js +++ b/spec/unit/crypto/secrets.spec.js @@ -20,6 +20,7 @@ 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 {encryptAES} from "../../../src/crypto/aes"; import * as utils from "../../../src/utils"; @@ -266,104 +267,640 @@ describe("Secrets", function() { expect(secret).toBe("bar"); }); - 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, - }, - }, + describe("bootstrap", function() { + // keys used in some of the tests + const XSK = new Uint8Array( + olmlib.decodeBase64("3lo2YdJugHjfE+Or7KJ47NuKbhE7AAGLgQ/dc19913Q="), ); - bob.uploadDeviceSigningKeys = async () => {}; - bob.uploadKeySignatures = async () => {}; - bob.setAccountData = async function(eventType, contents, callback) { - const event = new MatrixEvent({ - type: eventType, - content: contents, - }); - this.store.storeAccountDataEvents([ - event, - ]); - this.emit("accountData", event); - }; - - await bob.bootstrapSecretStorage(); + const XSPubKey = "DRb8pFVJyEJ9OWvXeUoM0jq/C2Wt+NxzBZVuk2nRb+0"; + const USK = new Uint8Array( + olmlib.decodeBase64("lKWi3hJGUie5xxHgySoz8PHFnZv6wvNaud/p2shN9VU="), + ); + const USPubKey = "CUpoiTtHiyXpUmd+3ohb7JVxAlUaOG1NYs9Jlx8soQU"; + const SSK = new Uint8Array( + olmlib.decodeBase64("1R6JVlXX99UcfUZzKuCDGQgJTw8ur1/ofgPD8pp+96M="), + ); + const SSPubKey = "0DfNsRDzEvkCLA0gD3m7VAGJ5VClhjEsewI35xq873Q"; + const backupKey = new Uint8Array( + olmlib.decodeBase64( + "OTmOKrPsWP1qxmt0s2qat4vYxULG+U9cmFAWFco3A5U=", + ), + ); + const SSSSKey = new Uint8Array( + olmlib.decodeBase64( + "XrmITOOdBhw6yY5Bh7trb/bgp1FRdIGyCUxxMP873R0=", + ), + ); + const SSSSPubKey = "v3A8HTypbccUm6jaRCRw5l+7lSdzQUACeY9xpQ5BVmE"; - const crossSigning = bob._crypto._crossSigningInfo; - const secretStorage = bob._crypto._secretStorage; + 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]; + }); - expect(crossSigning.getId()).toBeTruthy(); - expect(await crossSigning.isStoredInSecretStorage(secretStorage)).toBeTruthy(); - expect(await secretStorage.hasKey()).toBeTruthy(); - }); + const bob = await makeTestClient( + { + userId: "@bob:example.com", + deviceId: "bob1", + }, + { + cryptoCallbacks: { + getSecretStorageKey: getKey, + }, + }, + ); + bob.uploadDeviceSigningKeys = async () => {}; + bob.uploadKeySignatures = async () => {}; + bob.setAccountData = async function(eventType, contents, callback) { + const event = new MatrixEvent({ + type: eventType, + content: contents, + }); + this.store.storeAccountDataEvents([ + event, + ]); + this.emit("accountData", event); + }; + + await bob.bootstrapSecretStorage(); + + const crossSigning = bob._crypto._crossSigningInfo; + const secretStorage = bob._crypto._secretStorage; + + expect(crossSigning.getId()).toBeTruthy(); + expect(await crossSigning.isStoredInSecretStorage(secretStorage)) + .toBeTruthy(); + expect(await secretStorage.hasKey()).toBeTruthy(); + }); - it("bootstraps when cross-signing keys in secret storage", async function() { - const decryption = new global.Olm.PkDecryption(); - const storagePublicKey = decryption.generate_key(); - const storagePrivateKey = decryption.get_private_key(); + it("bootstraps when cross-signing keys in secret storage", async function() { + const decryption = new global.Olm.PkDecryption(); + const storagePublicKey = decryption.generate_key(); + const storagePrivateKey = decryption.get_private_key(); - const bob = await makeTestClient( - { - userId: "@bob:example.com", - deviceId: "bob1", - }, - { - cryptoCallbacks: { - getSecretStorageKey: async request => { - const defaultKeyId = await bob.getDefaultSecretStorageKeyId(); - expect(Object.keys(request.keys)).toEqual([defaultKeyId]); - return [defaultKeyId, storagePrivateKey]; + const bob = await makeTestClient( + { + userId: "@bob:example.com", + deviceId: "bob1", + }, + { + cryptoCallbacks: { + getSecretStorageKey: async request => { + const defaultKeyId = await bob.getDefaultSecretStorageKeyId(); + expect(Object.keys(request.keys)).toEqual([defaultKeyId]); + return [defaultKeyId, storagePrivateKey]; + }, }, }, - }, - ); + ); - bob.uploadDeviceSigningKeys = async () => {}; - bob.uploadKeySignatures = async () => {}; - bob.setAccountData = async function(eventType, contents, callback) { - const event = new MatrixEvent({ - type: eventType, - content: contents, + bob.uploadDeviceSigningKeys = async () => {}; + bob.uploadKeySignatures = async () => {}; + bob.setAccountData = async function(eventType, contents, callback) { + const event = new MatrixEvent({ + type: eventType, + content: contents, + }); + this.store.storeAccountDataEvents([ + event, + ]); + this.emit("accountData", event); + }; + bob._crypto.checkKeyBackup = async () => {}; + + const crossSigning = bob._crypto._crossSigningInfo; + const secretStorage = bob._crypto._secretStorage; + + // Set up cross-signing keys from scratch with specific storage key + await bob.bootstrapSecretStorage({ + createSecretStorageKey: async () => ({ + // `pubkey` not used anymore with symmetric 4S + keyInfo: { pubkey: storagePublicKey }, + privateKey: storagePrivateKey, + }), }); - this.store.storeAccountDataEvents([ - event, - ]); - this.emit("accountData", event); - }; - bob._crypto.checkKeyBackup = async () => {}; - - const crossSigning = bob._crypto._crossSigningInfo; - const secretStorage = bob._crypto._secretStorage; - // Set up cross-signing keys from scratch with specific storage key - await bob.bootstrapSecretStorage({ - createSecretStorageKey: async () => ({ - // `pubkey` not used anymore with symmetric 4S - keyInfo: { pubkey: storagePublicKey }, - privateKey: storagePrivateKey, - }), + // Clear local cross-signing keys and read from secret storage + bob._crypto._deviceList.storeCrossSigningForUser( + "@bob:example.com", + crossSigning.toStorage(), + ); + crossSigning.keys = {}; + await bob.bootstrapSecretStorage(); + + expect(crossSigning.getId()).toBeTruthy(); + expect(await crossSigning.isStoredInSecretStorage(secretStorage)) + .toBeTruthy(); + expect(await secretStorage.hasKey()).toBeTruthy(); }); - // Clear local cross-signing keys and read from secret storage - bob._crypto._deviceList.storeCrossSigningForUser( - "@bob:example.com", - crossSigning.toStorage(), - ); - crossSigning.keys = {}; - await bob.bootstrapSecretStorage(); - - expect(crossSigning.getId()).toBeTruthy(); - expect(await crossSigning.isStoredInSecretStorage(secretStorage)).toBeTruthy(); - expect(await secretStorage.hasKey()).toBeTruthy(); + it("converts asymmetric SSSS to symmetric SSSS", async function() { + let crossSigningKeys = {}; + const secretStorageKeys = { + "old_key_id": SSSSKey, + "new_key_id": SSSSKey, + }; + const alice = await makeTestClient( + {userId: "@alice:example.com", deviceId: "Osborne2"}, + { + cryptoCallbacks: { + getCrossSigningKey: t => crossSigningKeys[t], + saveCrossSigningKeys: k => crossSigningKeys = k, + getSecretStorageKey: ({keys}, name) => { + for (const keyId of Object.keys(keys)) { + if (secretStorageKeys[keyId]) { + return [keyId, secretStorageKeys[keyId]]; + } + } + }, + }, + }, + ); + const encryption = new global.Olm.PkEncryption(); + encryption.set_recipient_key(SSSSPubKey); + alice.store.storeAccountDataEvents([ + new MatrixEvent({ + type: "m.secret_storage.default_key", + content: { + key: "old_key_id", + }, + }), + new MatrixEvent({ + type: "m.secret_storage.key.old_key_id", + content: { + algorithm: "m.secret_storage.v1.curve25519-aes-sha2", + passphrase: { + algorithm: "m.pbkdf2", + iterations: 500000, + salt: "GbkvwKHVMveo1zGVSb2GMMdCinG2npJK", + }, + pubkey: "v3A8HTypbccUm6jaRCRw5l+7lSdzQUACeY9xpQ5BVmE", + signatures: { + "@alice:example.com": { + "ed25519:DRb8pFVJyEJ9OWvXeUoM0jq/C2Wt+NxzBZVuk2nRb+0": + "wHXPunpXd1IppW8DN54BrC5UTevG8MNNOnJFP5wB8xE8n7xuF7ntounA" + + "hI34Q/wR+xRyaWLaAoFgI+nWeopJDQ", + }, + }, + }, + }), + new MatrixEvent({ + type: "m.cross_signing.master", + content: { + encrypted: { + old_key_id: encryption.encrypt(olmlib.encodeBase64(XSK)), + }, + }, + }), + new MatrixEvent({ + type: "m.cross_signing.self_signing", + content: { + encrypted: { + old_key_id: encryption.encrypt(olmlib.encodeBase64(SSK)), + }, + }, + }), + new MatrixEvent({ + type: "m.cross_signing.user_signing", + content: { + encrypted: { + old_key_id: encryption.encrypt(olmlib.encodeBase64(USK)), + }, + }, + }), + new MatrixEvent({ + type: "m.megolm_backup.v1", + content: { + encrypted: { + old_key_id: encryption.encrypt( + olmlib.encodeBase64(backupKey), + ), + }, + }, + }), + ]); + encryption.free(); + alice._crypto._deviceList.storeCrossSigningForUser("@alice:example.com", { + keys: { + master: { + user_id: "@alice:example.com", + usage: ["master"], + keys: { + [`ed25519:${XSPubKey}`]: XSPubKey, + }, + }, + self_signing: { + user_id: "@alice:example.com", + usage: ["self_signing"], + keys: { + [`ed25519:${SSPubKey}`]: SSPubKey, + }, + signatures: { + "@alice:example.com": { + "ed25519:DRb8pFVJyEJ9OWvXeUoM0jq/C2Wt+NxzBZVuk2nRb+0": + "zdvI7+HmaOMYInWsYD1ctZe1tuCL7ENvVHBiNAagRwPaekF0zLSBnDrg" + + "SRwIC4do4Oi/v+9dl6+IVSdr5sdSAw", + }, + }, + }, + user_signing: { + user_id: "@alice:example.com", + usage: ["user_signing"], + keys: { + [`ed25519:${USPubKey}`]: USPubKey, + }, + signatures: { + "@alice:example.com": { + "ed25519:DRb8pFVJyEJ9OWvXeUoM0jq/C2Wt+NxzBZVuk2nRb+0": + "xK9XibBb13VSOwOaITx9ifEGWgXxJoxHY3IkTf99rxJ/YVoxTJNXxx5S" + + "QzAIFzPKBxA7Zm59efYZ+UehDE0sBw", + }, + }, + }, + }, + }); + alice.getKeyBackupVersion = async () => { + return { + version: "1", + algorithm: "m.megolm_backup.v1.curve25519-aes-sha2", + auth_data: { + public_key: "pxEXhg+4vdMf/kFwP4bVawFWdb0EmytL3eFJx++zQ0A", + signatures: { + "@alice:example.com": { + "ed25519:DRb8pFVJyEJ9OWvXeUoM0jq/C2Wt+NxzBZVuk2nRb+0": + "GfdOoC3xJ26bR5mpePq55GdPcPxu46LLbEZ0mDU3PLnTF/Ol/NQWszpz" + + "S51wmCPfE60znwLFCD0OqswSCNtHBA", + }, + }, + }, + }; + }; + const origAddSecretStorageKey = alice._crypto.addSecretStorageKey; + alice._crypto.addSecretStorageKey = async function(algorithm, opts, keyId) { + return await origAddSecretStorageKey.call( + alice._crypto, algorithm, opts, keyId || "new_key_id", + ); + }; + alice.setAccountData = async function(name, data) { + const event = new MatrixEvent({ + type: name, + content: data, + }); + alice.store.storeAccountDataEvents([event]); + this.emit("accountData", event); + }; + + await alice.bootstrapSecretStorage(); + + // it should create a new key with the same parameters + expect(alice.getAccountData("m.secret_storage.default_key").getContent()) + .toEqual({key: "new_key_id"}); + const newKeyInfo = alice.getAccountData("m.secret_storage.key.new_key_id") + .getContent(); + expect(newKeyInfo.algorithm) + .toEqual("m.secret_storage.v1.aes-hmac-sha2"); + expect(newKeyInfo.passphrase).toEqual({ + algorithm: "m.pbkdf2", + iterations: 500000, + salt: "GbkvwKHVMveo1zGVSb2GMMdCinG2npJK", + }); + expect(newKeyInfo).toHaveProperty("iv"); + expect(newKeyInfo).toHaveProperty("mac"); + expect(alice.checkSecretStorageKey(secretStorageKeys.new_key_id, newKeyInfo)) + .toBeTruthy(); + + const crossSigningMaster = alice.getAccountData("m.cross_signing.master") + .getContent(); + expect(crossSigningMaster.encrypted).toHaveProperty("new_key_id"); + expect(crossSigningMaster.encrypted).not.toHaveProperty("old_key_id"); + expect(await alice.getSecret("m.cross_signing.master")) + .toEqual(olmlib.encodeBase64(XSK)); + + const USKInfo = alice.getAccountData("m.cross_signing.self_signing") + .getContent(); + expect(USKInfo.encrypted).toHaveProperty("new_key_id"); + expect(USKInfo.encrypted).not.toHaveProperty("old_key_id"); + expect(await alice.getSecret("m.cross_signing.user_signing")) + .toEqual(olmlib.encodeBase64(USK)); + + const SSKInfo = alice.getAccountData("m.cross_signing.self_signing") + .getContent(); + expect(SSKInfo.encrypted).toHaveProperty("new_key_id"); + expect(SSKInfo.encrypted).not.toHaveProperty("old_key_id"); + expect(await alice.getSecret("m.cross_signing.self_signing")) + .toEqual(olmlib.encodeBase64(SSK)); + + const backupKeyInfo = alice.getAccountData("m.megolm_backup.v1") + .getContent(); + expect(backupKeyInfo.encrypted).toHaveProperty("new_key_id"); + expect(backupKeyInfo.encrypted).not.toHaveProperty("old_key_id"); + expect(await alice.getSecret("m.megolm_backup.v1")) + .toEqual(olmlib.encodeBase64(backupKey)); + }); + it("adds passphrase checking if it's lacking", async function() { + let crossSigningKeys = { + master: XSK, + user_signing: USK, + self_signing: SSK, + }; + const secretStorageKeys = { + key_id: SSSSKey, + }; + const alice = await makeTestClient( + {userId: "@alice:example.com", deviceId: "Osborne2"}, + { + cryptoCallbacks: { + getCrossSigningKey: t => crossSigningKeys[t], + saveCrossSigningKeys: k => crossSigningKeys = k, + getSecretStorageKey: ({keys}, name) => { + for (const keyId of Object.keys(keys)) { + if (secretStorageKeys[keyId]) { + return [keyId, secretStorageKeys[keyId]]; + } + } + }, + }, + }, + ); + alice.store.storeAccountDataEvents([ + new MatrixEvent({ + type: "m.secret_storage.default_key", + content: { + key: "key_id", + }, + }), + new MatrixEvent({ + type: "m.secret_storage.key.key_id", + content: { + algorithm: "m.secret_storage.v1.aes-hmac-sha2", + passphrase: { + algorithm: "m.pbkdf2", + iterations: 500000, + salt: "GbkvwKHVMveo1zGVSb2GMMdCinG2npJK", + }, + }, + }), + // we never use these values, other than checking that they + // exist, so just use dummy values + new MatrixEvent({ + type: "m.cross_signing.master", + content: { + encrypted: { + key_id: {ciphertext: "bla", mac: "bla", iv: "bla"}, + }, + }, + }), + new MatrixEvent({ + type: "m.cross_signing.self_signing", + content: { + encrypted: { + key_id: {ciphertext: "bla", mac: "bla", iv: "bla"}, + }, + }, + }), + new MatrixEvent({ + type: "m.cross_signing.user_signing", + content: { + encrypted: { + key_id: {ciphertext: "bla", mac: "bla", iv: "bla"}, + }, + }, + }), + ]); + alice._crypto._deviceList.storeCrossSigningForUser("@alice:example.com", { + keys: { + master: { + user_id: "@alice:example.com", + usage: ["master"], + keys: { + [`ed25519:${XSPubKey}`]: XSPubKey, + }, + }, + self_signing: { + user_id: "@alice:example.com", + usage: ["self_signing"], + keys: { + [`ed25519:${SSPubKey}`]: SSPubKey, + }, + signatures: { + "@alice:example.com": { + "ed25519:DRb8pFVJyEJ9OWvXeUoM0jq/C2Wt+NxzBZVuk2nRb+0": + "zdvI7+HmaOMYInWsYD1ctZe1tuCL7ENvVHBiNAagRwPaekF0zLSBnDrg" + + "SRwIC4do4Oi/v+9dl6+IVSdr5sdSAw", + }, + }, + }, + user_signing: { + user_id: "@alice:example.com", + usage: ["user_signing"], + keys: { + [`ed25519:${USPubKey}`]: USPubKey, + }, + signatures: { + "@alice:example.com": { + "ed25519:DRb8pFVJyEJ9OWvXeUoM0jq/C2Wt+NxzBZVuk2nRb+0": + "xK9XibBb13VSOwOaITx9ifEGWgXxJoxHY3IkTf99rxJ/YVoxTJNXxx5S" + + "QzAIFzPKBxA7Zm59efYZ+UehDE0sBw", + }, + }, + }, + }, + }); + alice.getKeyBackupVersion = async () => { + return { + version: "1", + algorithm: "m.megolm_backup.v1.curve25519-aes-sha2", + auth_data: { + public_key: "pxEXhg+4vdMf/kFwP4bVawFWdb0EmytL3eFJx++zQ0A", + signatures: { + "@alice:example.com": { + "ed25519:DRb8pFVJyEJ9OWvXeUoM0jq/C2Wt+NxzBZVuk2nRb+0": + "GfdOoC3xJ26bR5mpePq55GdPcPxu46LLbEZ0mDU3PLnTF/Ol/NQWszpz" + + "S51wmCPfE60znwLFCD0OqswSCNtHBA", + }, + }, + }, + }; + }; + alice.setAccountData = async function(name, data) { + const event = new MatrixEvent({ + type: name, + content: data, + }); + alice.store.storeAccountDataEvents([event]); + this.emit("accountData", event); + }; + + await alice.bootstrapSecretStorage(); + + expect(alice.getAccountData("m.secret_storage.default_key").getContent()) + .toEqual({key: "key_id"}); + const keyInfo = alice.getAccountData("m.secret_storage.key.key_id") + .getContent(); + expect(keyInfo.algorithm) + .toEqual("m.secret_storage.v1.aes-hmac-sha2"); + expect(keyInfo.passphrase).toEqual({ + algorithm: "m.pbkdf2", + iterations: 500000, + salt: "GbkvwKHVMveo1zGVSb2GMMdCinG2npJK", + }); + expect(keyInfo).toHaveProperty("iv"); + expect(keyInfo).toHaveProperty("mac"); + expect(alice.checkSecretStorageKey(secretStorageKeys.key_id, keyInfo)) + .toBeTruthy(); + }); + it("fixes backup keys in the wrong format", async function() { + let crossSigningKeys = { + master: XSK, + user_signing: USK, + self_signing: SSK, + }; + const secretStorageKeys = { + key_id: SSSSKey, + }; + const alice = await makeTestClient( + {userId: "@alice:example.com", deviceId: "Osborne2"}, + { + cryptoCallbacks: { + getCrossSigningKey: t => crossSigningKeys[t], + saveCrossSigningKeys: k => crossSigningKeys = k, + getSecretStorageKey: ({keys}, name) => { + for (const keyId of Object.keys(keys)) { + if (secretStorageKeys[keyId]) { + return [keyId, secretStorageKeys[keyId]]; + } + } + }, + }, + }, + ); + alice.store.storeAccountDataEvents([ + new MatrixEvent({ + type: "m.secret_storage.default_key", + content: { + key: "key_id", + }, + }), + new MatrixEvent({ + type: "m.secret_storage.key.key_id", + content: { + algorithm: "m.secret_storage.v1.aes-hmac-sha2", + passphrase: { + algorithm: "m.pbkdf2", + iterations: 500000, + salt: "GbkvwKHVMveo1zGVSb2GMMdCinG2npJK", + }, + }, + }), + new MatrixEvent({ + type: "m.cross_signing.master", + content: { + encrypted: { + key_id: {ciphertext: "bla", mac: "bla", iv: "bla"}, + }, + }, + }), + new MatrixEvent({ + type: "m.cross_signing.self_signing", + content: { + encrypted: { + key_id: {ciphertext: "bla", mac: "bla", iv: "bla"}, + }, + }, + }), + new MatrixEvent({ + type: "m.cross_signing.user_signing", + content: { + encrypted: { + key_id: {ciphertext: "bla", mac: "bla", iv: "bla"}, + }, + }, + }), + new MatrixEvent({ + type: "m.megolm_backup.v1", + content: { + encrypted: { + key_id: await encryptAES( + "123,45,6,7,89,1,234,56,78,90,12,34,5,67,8,90", + secretStorageKeys.key_id, "m.megolm_backup.v1", + ), + }, + }, + }), + ]); + alice._crypto._deviceList.storeCrossSigningForUser("@alice:example.com", { + keys: { + master: { + user_id: "@alice:example.com", + usage: ["master"], + keys: { + [`ed25519:${XSPubKey}`]: XSPubKey, + }, + }, + self_signing: { + user_id: "@alice:example.com", + usage: ["self_signing"], + keys: { + [`ed25519:${SSPubKey}`]: SSPubKey, + }, + signatures: { + "@alice:example.com": { + "ed25519:DRb8pFVJyEJ9OWvXeUoM0jq/C2Wt+NxzBZVuk2nRb+0": + "zdvI7+HmaOMYInWsYD1ctZe1tuCL7ENvVHBiNAagRwPaekF0zLSBnDrg" + + "SRwIC4do4Oi/v+9dl6+IVSdr5sdSAw", + }, + }, + }, + user_signing: { + user_id: "@alice:example.com", + usage: ["user_signing"], + keys: { + [`ed25519:${USPubKey}`]: USPubKey, + }, + signatures: { + "@alice:example.com": { + "ed25519:DRb8pFVJyEJ9OWvXeUoM0jq/C2Wt+NxzBZVuk2nRb+0": + "xK9XibBb13VSOwOaITx9ifEGWgXxJoxHY3IkTf99rxJ/YVoxTJNXxx5S" + + "QzAIFzPKBxA7Zm59efYZ+UehDE0sBw", + }, + }, + }, + }, + }); + alice.getKeyBackupVersion = async () => { + return { + version: "1", + algorithm: "m.megolm_backup.v1.curve25519-aes-sha2", + auth_data: { + public_key: "pxEXhg+4vdMf/kFwP4bVawFWdb0EmytL3eFJx++zQ0A", + signatures: { + "@alice:example.com": { + "ed25519:DRb8pFVJyEJ9OWvXeUoM0jq/C2Wt+NxzBZVuk2nRb+0": + "GfdOoC3xJ26bR5mpePq55GdPcPxu46LLbEZ0mDU3PLnTF/Ol/NQWszpz" + + "S51wmCPfE60znwLFCD0OqswSCNtHBA", + }, + }, + }, + }; + }; + alice.setAccountData = async function(name, data) { + const event = new MatrixEvent({ + type: name, + content: data, + }); + alice.store.storeAccountDataEvents([event]); + this.emit("accountData", event); + }; + + await alice.bootstrapSecretStorage(); + + const backupKey = alice.getAccountData("m.megolm_backup.v1") + .getContent(); + expect(backupKey.encrypted).toHaveProperty("key_id"); + expect(await alice.getSecret("m.megolm_backup.v1")) + .toEqual("ey0GB1kB6jhOWgwiBUMIWg=="); + }); }); }); diff --git a/src/client.js b/src/client.js index 64174c993af..4c8cb567e41 100644 --- a/src/client.js +++ b/src/client.js @@ -42,7 +42,7 @@ import * as olmlib from "./crypto/olmlib"; import {ReEmitter} from './ReEmitter'; import {RoomList} from './crypto/RoomList'; import {logger} from './logger'; -import {Crypto, isCryptoAvailable} from './crypto'; +import {Crypto, isCryptoAvailable, fixBackupKey} from './crypto'; import {decodeRecoveryKey} from './crypto/recoverykey'; import {keyFromAuthData} from './crypto/key_passphrase'; import {randomString} from './randomstring'; @@ -1815,7 +1815,17 @@ MatrixClient.prototype.restoreKeyBackupWithPassword = async function( MatrixClient.prototype.restoreKeyBackupWithSecretStorage = async function( backupInfo, targetRoomId, targetSessionId, opts, ) { - const privKey = decodeBase64(await this.getSecret("m.megolm_backup.v1")); + const storedKey = await this.getSecret("m.megolm_backup.v1"); + + // ensure that the key is in the right format. If not, fix the key and + // store the fixed version + const fixedKey = fixBackupKey(storedKey); + if (fixedKey) { + const [keyId] = await this.getSecretStorageKey(); + await this.storeSecret("m.megolm_backup.v1", [keyId]); + } + + const privKey = decodeBase64(fixedKey || storedKey); return this._restoreKeyBackup( privKey, targetRoomId, targetSessionId, backupInfo, opts, ); diff --git a/src/crypto/SecretStorage.js b/src/crypto/SecretStorage.js index 123fecf45d7..6f3c0c5891b 100644 --- a/src/crypto/SecretStorage.js +++ b/src/crypto/SecretStorage.js @@ -20,6 +20,7 @@ import * as olmlib from './olmlib'; import {pkVerify} from './olmlib'; import {randomString} from '../randomstring'; import {encryptAES, decryptAES} from './aes'; +import {encodeBase64} from "./olmlib"; export const SECRET_STORAGE_ALGORITHM_V1_AES = "m.secret_storage.v1.aes-hmac-sha2"; @@ -98,7 +99,7 @@ export class SecretStorage extends EventEmitter { keyData.passphrase = opts.passphrase; } if (opts.key) { - const {iv, mac} = await encryptAES(ZERO_STR, opts.key, ""); + const {iv, mac} = await SecretStorage._calculateKeyCheck(opts.key); keyData.iv = iv; keyData.mac = mac; } @@ -209,7 +210,7 @@ export class SecretStorage extends EventEmitter { case SECRET_STORAGE_ALGORITHM_V1_AES: { if (info.mac) { - const {mac} = await encryptAES(ZERO_STR, key, "", info.iv); + const {mac} = await SecretStorage._calculateKeyCheck(key, info.iv); return info.mac === mac; } else { // if we have no information, we have to assume the key is right @@ -235,6 +236,10 @@ export class SecretStorage extends EventEmitter { } } + static async _calculateKeyCheck(key, iv) { + return await encryptAES(ZERO_STR, key, "", iv); + } + /** * Store an encrypted secret on the server * @@ -372,8 +377,9 @@ export class SecretStorage extends EventEmitter { const encInfo = secretInfo.encrypted[keyId]; // 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 decryption.get_private_key(); + // since we just want to return the key itself. It must be base64 + // encoded, since this is how a key would normally be stored. + if (encInfo.passthrough) return encodeBase64(decryption.get_private_key()); return await decryption.decrypt(encInfo); } finally { diff --git a/src/crypto/index.js b/src/crypto/index.js index b8b02fba3f2..1d6360f04f6 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -432,6 +432,14 @@ Crypto.prototype.isCrossSigningReady = async function() { * up, then no changes are made, so this is safe to run to ensure secret storage * is ready for use. * + * This function + * - creates a new Secure Secret Storage key if no default key exists + * - if a key backup exists, it is migrated to store the key in the Secret + * Storage + * - creates a backup if none exists, and one is requested + * - migrates Secure Secret Storage to use the latest algorithm, if an outdated + * algorithm is found + * * @param {function} [opts.authUploadDeviceSigningKeys] Optional. Function * called to await an interactive auth flow when uploading device signing keys. * Args: @@ -501,21 +509,150 @@ Crypto.prototype.bootstrapSecretStorage = async function({ return key; }; + // create a new SSSS key and set it as default + const createSSSS = async (opts, privateKey) => { + opts = opts || {}; + if (privateKey) { + opts.key = privateKey; + } + + const newKeyId = await this.addSecretStorageKey( + SECRET_STORAGE_ALGORITHM_V1_AES, opts, + ); + await this.setDefaultSecretStorageKeyId(newKeyId); + + if (privateKey) { + // cache the private key so that we can access it again + ssssKeys[newKeyId] = privateKey; + } + return newKeyId; + }; + + // reset the cross-signing keys + const resetCrossSigning = async () => { + this._baseApis._cryptoCallbacks.saveCrossSigningKeys = + keys => Object.assign(crossSigningPrivateKeys, keys); + this._baseApis._cryptoCallbacks.getCrossSigningKey = + name => crossSigningPrivateKeys[name]; + await this.resetCrossSigningKeys( + CrossSigningLevel.MASTER, + { authUploadDeviceSigningKeys }, + ); + }; + + const ensureCanCheckPassphrase = async (keyId, keyInfo) => { + if (!keyInfo.mac) { + const key = await this._baseApis._cryptoCallbacks.getSecretStorageKey( + {keys: {[keyId]: keyInfo}}, "", + ); + if (key) { + const keyData = key[1]; + ssssKeys[keyId] = keyData; + const {iv, mac} = await SecretStorage._calculateKeyCheck(keyData); + keyInfo.iv = iv; + keyInfo.mac = mac; + + await this._crossSigningInfo.signObject(keyInfo, 'master'); + + await this._baseApis.setAccountData( + `m.secret_storage.key.${keyId}`, keyInfo, + ); + } + } + }; + try { + const oldSSSSKey = await this.getSecretStorageKey(); + const [oldKeyId, oldKeyInfo] = oldSSSSKey || [null, null]; const decryptionKeys = await this._crossSigningInfo.isStoredInSecretStorage(this._secretStorage); const inStorage = !setupNewSecretStorage && decryptionKeys; - if (decryptionKeys && !(Object.values(decryptionKeys).some( + + if (!decryptionKeys && !keyBackupInfo) { + // either we don't have anything, or we've been asked to restart + // from scratch + logger.log( + "Cross-signing private keys not found in secret storage, " + + "creating new keys", + ); + + await resetCrossSigning(); + + if (oldKeyInfo && oldKeyInfo.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) { + // if we already have a default SSSS key; use it + newKeyId = oldKeyId; + } else { + // otherwise, create a new one + const { keyInfo, privateKey } = await createSecretStorageKey(); + newKeyId = await createSSSS(keyInfo, privateKey); + } + + if (oldKeyInfo && oldKeyInfo.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) { + await ensureCanCheckPassphrase(oldKeyId, oldKeyInfo); + } + + if (setupNewKeyBackup) { + const info = await this._baseApis.prepareKeyBackupVersion( + null /* random key */, + { secureSecretStorage: true }, + ); + await this._baseApis.createKeyBackupVersion(info); + } + } else if (!inStorage && keyBackupInfo) { + // we have an existing backup, but no SSSS + + logger.log("Secret storage default key not found, using key backup key"); + + const backupKey = await getKeyBackupPassphrase(); + + // create new cross-signing keys + await resetCrossSigning(); + + // create a new SSSS key and use the backup key as the new SSSS key + 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 createSSSS(opts, backupKey); + + // store the backup key in secret storage + await this.storeSecret( + "m.megolm_backup.v1", olmlib.encodeBase64(backupKey), [newKeyId], + ); + + // The backup is trusted because the user provided the private key. + // Sign the backup with the cross signing key so the key backup can + // be trusted via cross-signing. + logger.log("Adding cross signing signature to key backup"); + await this._crossSigningInfo.signObject( + keyBackupInfo.auth_data, "master", + ); + await this._baseApis._http.authedRequest( + undefined, "PUT", "/room_keys/version/" + keyBackupInfo.version, + undefined, keyBackupInfo, + {prefix: httpApi.PREFIX_UNSTABLE}, + ); + } 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"); + // we have an asymmetric SSSS + logger.log("Asymmetric SSSS found. Switching SSSS 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. + // SSSS key, and so that we can re-encrypt them with the new key). + // We store the cross-signing keys, and temporarily set a callback + // so that when the private keys are 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"]) { @@ -524,149 +661,67 @@ Crypto.prototype.bootstrapSecretStorage = async function({ keys[type] = secret; crossSigningPrivateKeys[type] = olmlib.decodeBase64(secret); } + await this.checkOwnCrossSigningTrust(); + const opts = {}; - let oldKeyId = null; + + let oldKey = 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; + oldKey = ssssKeys[keyId]; if (keyInfo.passphrase) { opts.passphrase = keyInfo.passphrase; } break; } } - if (oldKeyId) { - opts.key = ssssKeys[oldKeyId]; - } - // 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 + + // create new symmetric SSSS key + newKeyId = await createSSSS(opts, oldKey); + + logger.log("re-encrypting cross-signing keys"); + // re-encrypt all the cross-signing 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 (inStorage) { - logger.log("Cross-signing private keys found in secret storage"); - await this.checkOwnCrossSigningTrust(); - } else { - logger.log( - "Cross-signing private keys not found in secret storage, " + - "creating new keys", - ); - this._baseApis._cryptoCallbacks.saveCrossSigningKeys = - keys => Object.assign(crossSigningPrivateKeys, keys); - this._baseApis._cryptoCallbacks.getCrossSigningKey = - name => crossSigningPrivateKeys[name]; - await this.resetCrossSigningKeys( - CrossSigningLevel.MASTER, - { authUploadDeviceSigningKeys }, - ); - } - } else { - logger.log("Cross signing keys are present in secret storage"); - } - - // 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 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, - }; - } + if (await this.isSecretStored("m.megolm_backup.v1", false)) { + // re-encrypt the backup key if we had one too + logger.log("re-encrypting backup key"); + const backupKey = await this.getSecret("m.megolm_backup.v1"); - // use the backup key as the new ssss key - ssssKeys[newKeyId] = backupKey; - opts.key = backupKey; + await this.storeSecret( + "m.megolm_backup.v1", fixBackupKey(backupKey) || backupKey, + [newKeyId], + ); + } + } else if (!this._crossSigningInfo.getId()) { + // we have SSSS, but we don't know if the server's cross-signing + // keys should be trusted + logger.log("Cross-signing private keys found in secret storage"); - newKeyId = await this.addSecretStorageKey( - SECRET_STORAGE_ALGORITHM_V1_AES, opts, - ); - await this.setDefaultSecretStorageKeyId(newKeyId); - } + // fetch the private keys and set up our local copy of the keys for + // use + await this.checkOwnCrossSigningTrust(); - // 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) { - logger.log("Adding cross signing signature to key backup"); - await this._crossSigningInfo.signObject( - keyBackupInfo.auth_data, "master", - ); - await this._baseApis._http.authedRequest( - undefined, "PUT", "/room_keys/version/" + keyBackupInfo.version, - undefined, keyBackupInfo, - {prefix: httpApi.PREFIX_UNSTABLE}, - ); - await this.storeSecret( - "m.megolm_backup.v1", olmlib.encodeBase64(backupKey), [newKeyId], - ); - } else { - logger.log( - "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 { keyInfo, privateKey } = await createSecretStorageKey(); - if (keyInfo && privateKey) { - keyInfo.key = privateKey; - } - newKeyId = await this.addSecretStorageKey( - SECRET_STORAGE_ALGORITHM_V1_AES, - keyInfo, - ); - await this.setDefaultSecretStorageKeyId(newKeyId); - ssssKeys[newKeyId] = privateKey; - } - 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]); - } + if (oldKeyInfo && oldKeyInfo.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) { + // make sure that the default key has the information needed to + // check the passphrase + await ensureCanCheckPassphrase(oldKeyId, oldKeyInfo); } } else { - logger.log("Have secret storage key"); + // we have SSSS and we cross-signing is already set up + logger.log("Cross signing keys are present in secret storage"); + + if (oldKeyInfo && oldKeyInfo.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) { + // make sure that the default key has the information needed to + // check the passphrase + await ensureCanCheckPassphrase(oldKeyId, oldKeyInfo); + } } // If cross-signing keys were reset, store them in Secure Secret Storage. @@ -711,8 +766,26 @@ Crypto.prototype.bootstrapSecretStorage = async function({ const sessionBackupKey = await this.getSecret('m.megolm_backup.v1'); if (sessionBackupKey) { logger.info("Got session backup key from secret storage: caching"); - const decoded = olmlib.decodeBase64(sessionBackupKey); - await this.storeSessionBackupPrivateKey(decoded); + // fix up the backup key if it's in the wrong format, and replace + // in secret storage + const fixedBackupKey = fixBackupKey(sessionBackupKey); + if (fixedBackupKey) { + await this.storeSecret( + "m.megolm_backup.v1", fixedBackupKey, [newKeyId || oldKeyId], + ); + } + const decodedBackupKey = new Uint8Array(olmlib.decodeBase64( + fixedBackupKey || sessionBackupKey, + )); + await this.storeSessionBackupPrivateKey(decodedBackupKey); + } + + if (setupNewKeyBackup && !keyBackupInfo) { + const info = await this._baseApis.prepareKeyBackupVersion( + null /* random key */, + { secureSecretStorage: true }, + ); + await this._baseApis.createKeyBackupVersion(info); } } finally { // Restore the original callbacks. NB. we must do this by manipulating @@ -729,6 +802,26 @@ Crypto.prototype.bootstrapSecretStorage = async function({ logger.log("Secure Secret Storage ready"); }; +/** + * Fix up the backup key, that may be in the wrong format due to a bug in a + * migration step. Some backup keys were stored as a comma-separated list of + * integers, rather than a base64-encoded byte array. If this function is + * passed a string that looks like a list of integers rather than a base64 + * string, it will attempt to convert it to the right format. + * + * @param {string} key the key to check + * @returns {null | string} If the key is in the wrong format, then the fixed + * key will be returned. Otherwise null will be returned. + * + */ +export function fixBackupKey(key) { + if (key.indexOf(",") < 0) { + return null; + } + const fixedKey = Uint8Array.from(key.split(","), x => parseInt(x)); + return olmlib.encodeBase64(fixedKey); +} + Crypto.prototype.addSecretStorageKey = function(algorithm, opts, keyID) { return this._secretStorage.addKey(algorithm, opts, keyID); }; @@ -802,7 +895,7 @@ Crypto.prototype.checkSecretStoragePrivateKey = function(privateKey, expectedPub * @returns {Promise} the key, if any, or null */ Crypto.prototype.getSessionBackupPrivateKey = async function() { - return new Promise((resolve) => { + let key = await new Promise((resolve) => { this._cryptoStore.doTxn( 'readonly', [IndexedDBCryptoStore.STORE_ACCOUNT], @@ -815,6 +908,12 @@ Crypto.prototype.getSessionBackupPrivateKey = async function() { }, ); }); + + // make sure we have a Uint8Array, rather than a string + if (key && typeof(key === "string")) { + key = olmlib.decodeBase64(fixBackupKey(key) || key); + await this.storeSessionBackupPrivateKey(key); + } }; /** From 75703f273f607c1fd8f7891bcf3797ba15b7144d Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Thu, 9 Apr 2020 00:18:21 -0400 Subject: [PATCH 2/8] fix unit tests --- src/crypto/index.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/crypto/index.js b/src/crypto/index.js index 1d6360f04f6..0bcc9a703da 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -659,7 +659,7 @@ Crypto.prototype.bootstrapSecretStorage = async function({ const secretName = `m.cross_signing.${type}`; const secret = await this.getSecret(secretName); keys[type] = secret; - crossSigningPrivateKeys[type] = olmlib.decodeBase64(secret); + crossSigningPrivateKeys[type] = new Uint8Array(olmlib.decodeBase64(secret)); } await this.checkOwnCrossSigningTrust(); @@ -911,9 +911,10 @@ Crypto.prototype.getSessionBackupPrivateKey = async function() { // make sure we have a Uint8Array, rather than a string if (key && typeof(key === "string")) { - key = olmlib.decodeBase64(fixBackupKey(key) || key); + key = new Uint8Array(olmlib.decodeBase64(fixBackupKey(key) || key)); await this.storeSessionBackupPrivateKey(key); } + return key; }; /** From 5bd146bb85b0d6eb7827045ca31f9c38ef023285 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Thu, 9 Apr 2020 00:21:31 -0400 Subject: [PATCH 3/8] lint --- src/crypto/index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/crypto/index.js b/src/crypto/index.js index 0bcc9a703da..71617e3bee8 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -659,7 +659,8 @@ Crypto.prototype.bootstrapSecretStorage = async function({ const secretName = `m.cross_signing.${type}`; const secret = await this.getSecret(secretName); keys[type] = secret; - crossSigningPrivateKeys[type] = new Uint8Array(olmlib.decodeBase64(secret)); + crossSigningPrivateKeys[type] + = new Uint8Array(olmlib.decodeBase64(secret)); } await this.checkOwnCrossSigningTrust(); From df38fde336d1bfcb4a4aafc3905c495923f42323 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Thu, 9 Apr 2020 11:47:45 -0400 Subject: [PATCH 4/8] apply changes from code review --- src/client.js | 2 +- src/crypto/index.js | 26 ++++++++------------------ 2 files changed, 9 insertions(+), 19 deletions(-) diff --git a/src/client.js b/src/client.js index 4c8cb567e41..f2f21f4bb51 100644 --- a/src/client.js +++ b/src/client.js @@ -1822,7 +1822,7 @@ MatrixClient.prototype.restoreKeyBackupWithSecretStorage = async function( const fixedKey = fixBackupKey(storedKey); if (fixedKey) { const [keyId] = await this.getSecretStorageKey(); - await this.storeSecret("m.megolm_backup.v1", [keyId]); + await this.storeSecret("m.megolm_backup.v1", fixedKey, [keyId]); } const privKey = decodeBase64(fixedKey || storedKey); diff --git a/src/crypto/index.js b/src/crypto/index.js index 71617e3bee8..09a998e5a3c 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -516,16 +516,16 @@ Crypto.prototype.bootstrapSecretStorage = async function({ opts.key = privateKey; } - const newKeyId = await this.addSecretStorageKey( + const keyId = await this.addSecretStorageKey( SECRET_STORAGE_ALGORITHM_V1_AES, opts, ); - await this.setDefaultSecretStorageKeyId(newKeyId); + await this.setDefaultSecretStorageKeyId(keyId); if (privateKey) { // cache the private key so that we can access it again - ssssKeys[newKeyId] = privateKey; + ssssKeys[keyId] = privateKey; } - return newKeyId; + return keyId; }; // reset the cross-signing keys @@ -568,7 +568,7 @@ Crypto.prototype.bootstrapSecretStorage = async function({ await this._crossSigningInfo.isStoredInSecretStorage(this._secretStorage); const inStorage = !setupNewSecretStorage && decryptionKeys; - if (!decryptionKeys && !keyBackupInfo) { + if (!inStorage && !keyBackupInfo) { // either we don't have anything, or we've been asked to restart // from scratch logger.log( @@ -578,10 +578,8 @@ Crypto.prototype.bootstrapSecretStorage = async function({ await resetCrossSigning(); - if (oldKeyInfo && oldKeyInfo.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) { - // if we already have a default SSSS key; use it - newKeyId = oldKeyId; - } else { + if (!oldKeyInfo || oldKeyInfo.algorithm !== SECRET_STORAGE_ALGORITHM_V1_AES) { + // if we already have a usable default SSSS key, just use it. // otherwise, create a new one const { keyInfo, privateKey } = await createSecretStorageKey(); newKeyId = await createSSSS(keyInfo, privateKey); @@ -613,7 +611,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", @@ -780,14 +778,6 @@ Crypto.prototype.bootstrapSecretStorage = async function({ )); await this.storeSessionBackupPrivateKey(decodedBackupKey); } - - if (setupNewKeyBackup && !keyBackupInfo) { - const info = await this._baseApis.prepareKeyBackupVersion( - null /* random key */, - { secureSecretStorage: true }, - ); - await this._baseApis.createKeyBackupVersion(info); - } } finally { // Restore the original callbacks. NB. we must do this by manipulating // the same object since the CrossSigning class has a reference to the From 4039498eee5f8fae9ac3e6b150aacf3af3a90ec2 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Thu, 9 Apr 2020 17:05:01 -0400 Subject: [PATCH 5/8] use cached backup key if available --- src/crypto/index.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/crypto/index.js b/src/crypto/index.js index 09a998e5a3c..005c815f836 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -601,7 +601,10 @@ Crypto.prototype.bootstrapSecretStorage = async function({ logger.log("Secret storage default key not found, using key backup key"); - const backupKey = await getKeyBackupPassphrase(); + // if we have the backup key already cached, use it; otherwise use the + // callback to prompt for the key + const backupKey = await this.getSessionBackupPrivateKey() || + await getKeyBackupPassphrase(); // create new cross-signing keys await resetCrossSigning(); From 864fe459b74eb2bf4257f5627cc625a2782fcc3d Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Mon, 13 Apr 2020 16:27:46 -0400 Subject: [PATCH 6/8] improve readability of tests --- spec/unit/crypto/secrets.spec.js | 119 ++++++++----------------------- 1 file changed, 28 insertions(+), 91 deletions(-) diff --git a/spec/unit/crypto/secrets.spec.js b/spec/unit/crypto/secrets.spec.js index fc17703a65c..a40ceb13744 100644 --- a/spec/unit/crypto/secrets.spec.js +++ b/spec/unit/crypto/secrets.spec.js @@ -50,6 +50,13 @@ async function makeTestClient(userInfo, options) { return client; } +// Wrapper around pkSign to return a signed object. pkSign returns the +// signature, rather than the signed object. +function sign(obj, key, userId) { + olmlib.pkSign(obj, key, userId); + return obj; +} + describe("Secrets", function() { if (!global.Olm) { console.warn('Not running megolm backup unit tests: libolm not present'); @@ -429,22 +436,15 @@ describe("Secrets", function() { }), new MatrixEvent({ type: "m.secret_storage.key.old_key_id", - content: { + content: sign({ algorithm: "m.secret_storage.v1.curve25519-aes-sha2", passphrase: { algorithm: "m.pbkdf2", iterations: 500000, salt: "GbkvwKHVMveo1zGVSb2GMMdCinG2npJK", }, - pubkey: "v3A8HTypbccUm6jaRCRw5l+7lSdzQUACeY9xpQ5BVmE", - signatures: { - "@alice:example.com": { - "ed25519:DRb8pFVJyEJ9OWvXeUoM0jq/C2Wt+NxzBZVuk2nRb+0": - "wHXPunpXd1IppW8DN54BrC5UTevG8MNNOnJFP5wB8xE8n7xuF7ntounA" - + "hI34Q/wR+xRyaWLaAoFgI+nWeopJDQ", - }, - }, - }, + pubkey: SSSSPubKey, + }, XSK, "@alice:example.com"), }), new MatrixEvent({ type: "m.cross_signing.master", @@ -491,50 +491,29 @@ describe("Secrets", function() { [`ed25519:${XSPubKey}`]: XSPubKey, }, }, - self_signing: { + self_signing: sign({ user_id: "@alice:example.com", usage: ["self_signing"], keys: { [`ed25519:${SSPubKey}`]: SSPubKey, }, - signatures: { - "@alice:example.com": { - "ed25519:DRb8pFVJyEJ9OWvXeUoM0jq/C2Wt+NxzBZVuk2nRb+0": - "zdvI7+HmaOMYInWsYD1ctZe1tuCL7ENvVHBiNAagRwPaekF0zLSBnDrg" - + "SRwIC4do4Oi/v+9dl6+IVSdr5sdSAw", - }, - }, - }, - user_signing: { + }, XSK, "@alice:example.com"), + user_signing: sign({ user_id: "@alice:example.com", usage: ["user_signing"], keys: { [`ed25519:${USPubKey}`]: USPubKey, }, - signatures: { - "@alice:example.com": { - "ed25519:DRb8pFVJyEJ9OWvXeUoM0jq/C2Wt+NxzBZVuk2nRb+0": - "xK9XibBb13VSOwOaITx9ifEGWgXxJoxHY3IkTf99rxJ/YVoxTJNXxx5S" - + "QzAIFzPKBxA7Zm59efYZ+UehDE0sBw", - }, - }, - }, + }, XSK, "@alice:example.com"), }, }); alice.getKeyBackupVersion = async () => { return { version: "1", algorithm: "m.megolm_backup.v1.curve25519-aes-sha2", - auth_data: { + auth_data: sign({ public_key: "pxEXhg+4vdMf/kFwP4bVawFWdb0EmytL3eFJx++zQ0A", - signatures: { - "@alice:example.com": { - "ed25519:DRb8pFVJyEJ9OWvXeUoM0jq/C2Wt+NxzBZVuk2nRb+0": - "GfdOoC3xJ26bR5mpePq55GdPcPxu46LLbEZ0mDU3PLnTF/Ol/NQWszpz" - + "S51wmCPfE60znwLFCD0OqswSCNtHBA", - }, - }, - }, + }, XSK, "@alice:example.com"), }; }; const origAddSecretStorageKey = alice._crypto.addSecretStorageKey; @@ -678,50 +657,29 @@ describe("Secrets", function() { [`ed25519:${XSPubKey}`]: XSPubKey, }, }, - self_signing: { + self_signing: sign({ user_id: "@alice:example.com", usage: ["self_signing"], keys: { [`ed25519:${SSPubKey}`]: SSPubKey, }, - signatures: { - "@alice:example.com": { - "ed25519:DRb8pFVJyEJ9OWvXeUoM0jq/C2Wt+NxzBZVuk2nRb+0": - "zdvI7+HmaOMYInWsYD1ctZe1tuCL7ENvVHBiNAagRwPaekF0zLSBnDrg" - + "SRwIC4do4Oi/v+9dl6+IVSdr5sdSAw", - }, - }, - }, - user_signing: { + }, XSK, "@alice:example.com"), + user_signing: sign({ user_id: "@alice:example.com", usage: ["user_signing"], keys: { [`ed25519:${USPubKey}`]: USPubKey, }, - signatures: { - "@alice:example.com": { - "ed25519:DRb8pFVJyEJ9OWvXeUoM0jq/C2Wt+NxzBZVuk2nRb+0": - "xK9XibBb13VSOwOaITx9ifEGWgXxJoxHY3IkTf99rxJ/YVoxTJNXxx5S" - + "QzAIFzPKBxA7Zm59efYZ+UehDE0sBw", - }, - }, - }, + }, XSK, "@alice:example.com"), }, }); alice.getKeyBackupVersion = async () => { return { version: "1", algorithm: "m.megolm_backup.v1.curve25519-aes-sha2", - auth_data: { + auth_data: sign({ public_key: "pxEXhg+4vdMf/kFwP4bVawFWdb0EmytL3eFJx++zQ0A", - signatures: { - "@alice:example.com": { - "ed25519:DRb8pFVJyEJ9OWvXeUoM0jq/C2Wt+NxzBZVuk2nRb+0": - "GfdOoC3xJ26bR5mpePq55GdPcPxu46LLbEZ0mDU3PLnTF/Ol/NQWszpz" - + "S51wmCPfE60znwLFCD0OqswSCNtHBA", - }, - }, - }, + }, XSK, "@alice:example.com"), }; }; alice.setAccountData = async function(name, data) { @@ -839,50 +797,29 @@ describe("Secrets", function() { [`ed25519:${XSPubKey}`]: XSPubKey, }, }, - self_signing: { + self_signing: sign({ user_id: "@alice:example.com", usage: ["self_signing"], keys: { [`ed25519:${SSPubKey}`]: SSPubKey, }, - signatures: { - "@alice:example.com": { - "ed25519:DRb8pFVJyEJ9OWvXeUoM0jq/C2Wt+NxzBZVuk2nRb+0": - "zdvI7+HmaOMYInWsYD1ctZe1tuCL7ENvVHBiNAagRwPaekF0zLSBnDrg" - + "SRwIC4do4Oi/v+9dl6+IVSdr5sdSAw", - }, - }, - }, - user_signing: { + }, XSK, "@alice:example.com"), + user_signing: sign({ user_id: "@alice:example.com", usage: ["user_signing"], keys: { [`ed25519:${USPubKey}`]: USPubKey, }, - signatures: { - "@alice:example.com": { - "ed25519:DRb8pFVJyEJ9OWvXeUoM0jq/C2Wt+NxzBZVuk2nRb+0": - "xK9XibBb13VSOwOaITx9ifEGWgXxJoxHY3IkTf99rxJ/YVoxTJNXxx5S" - + "QzAIFzPKBxA7Zm59efYZ+UehDE0sBw", - }, - }, - }, + }, XSK, "@alice:example.com"), }, }); alice.getKeyBackupVersion = async () => { return { version: "1", algorithm: "m.megolm_backup.v1.curve25519-aes-sha2", - auth_data: { + auth_data: sign({ public_key: "pxEXhg+4vdMf/kFwP4bVawFWdb0EmytL3eFJx++zQ0A", - signatures: { - "@alice:example.com": { - "ed25519:DRb8pFVJyEJ9OWvXeUoM0jq/C2Wt+NxzBZVuk2nRb+0": - "GfdOoC3xJ26bR5mpePq55GdPcPxu46LLbEZ0mDU3PLnTF/Ol/NQWszpz" - + "S51wmCPfE60znwLFCD0OqswSCNtHBA", - }, - }, - }, + }, XSK, "@alice:example.com"), }; }; alice.setAccountData = async function(name, data) { From 5d606bba66af245ff4e2b20e99c680ec9f6c2a59 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Mon, 13 Apr 2020 16:54:02 -0400 Subject: [PATCH 7/8] add test for passthrough on backups --- spec/unit/crypto/secrets.spec.js | 138 +++++++++++++++++++++++++++++++ 1 file changed, 138 insertions(+) diff --git a/spec/unit/crypto/secrets.spec.js b/spec/unit/crypto/secrets.spec.js index a40ceb13744..0e3318de44e 100644 --- a/spec/unit/crypto/secrets.spec.js +++ b/spec/unit/crypto/secrets.spec.js @@ -578,6 +578,144 @@ describe("Secrets", function() { expect(await alice.getSecret("m.megolm_backup.v1")) .toEqual(olmlib.encodeBase64(backupKey)); }); + it("converts asymmetric SSSS with passthrough to symmetric SSSS", async function() { + let crossSigningKeys = {}; + const secretStorageKeys = { + "old_key_id": SSSSKey, + "new_key_id": SSSSKey, + }; + const alice = await makeTestClient( + {userId: "@alice:example.com", deviceId: "Osborne2"}, + { + cryptoCallbacks: { + getCrossSigningKey: t => crossSigningKeys[t], + saveCrossSigningKeys: k => crossSigningKeys = k, + getSecretStorageKey: ({keys}, name) => { + for (const keyId of Object.keys(keys)) { + if (secretStorageKeys[keyId]) { + return [keyId, secretStorageKeys[keyId]]; + } + } + }, + }, + }, + ); + const encryption = new global.Olm.PkEncryption(); + encryption.set_recipient_key(SSSSPubKey); + alice.store.storeAccountDataEvents([ + new MatrixEvent({ + type: "m.secret_storage.default_key", + content: { + key: "old_key_id", + }, + }), + new MatrixEvent({ + type: "m.secret_storage.key.old_key_id", + content: sign({ + algorithm: "m.secret_storage.v1.curve25519-aes-sha2", + passphrase: { + algorithm: "m.pbkdf2", + iterations: 500000, + salt: "GbkvwKHVMveo1zGVSb2GMMdCinG2npJK", + }, + pubkey: "v3A8HTypbccUm6jaRCRw5l+7lSdzQUACeY9xpQ5BVmE", + }, XSK, "@alice:example.com"), + }), + new MatrixEvent({ + type: "m.cross_signing.master", + content: { + encrypted: { + old_key_id: encryption.encrypt(olmlib.encodeBase64(XSK)), + }, + }, + }), + new MatrixEvent({ + type: "m.cross_signing.self_signing", + content: { + encrypted: { + old_key_id: encryption.encrypt(olmlib.encodeBase64(SSK)), + }, + }, + }), + new MatrixEvent({ + type: "m.cross_signing.user_signing", + content: { + encrypted: { + old_key_id: encryption.encrypt(olmlib.encodeBase64(USK)), + }, + }, + }), + new MatrixEvent({ + type: "m.megolm_backup.v1", + content: { + encrypted: { + old_key_id: { + passthrough: true, + }, + }, + }, + }), + ]); + encryption.free(); + alice._crypto._deviceList.storeCrossSigningForUser("@alice:example.com", { + keys: { + master: { + user_id: "@alice:example.com", + usage: ["master"], + keys: { + [`ed25519:${XSPubKey}`]: XSPubKey, + }, + }, + self_signing: sign({ + user_id: "@alice:example.com", + usage: ["self_signing"], + keys: { + [`ed25519:${SSPubKey}`]: SSPubKey, + }, + }, XSK, "@alice:example.com"), + user_signing: sign({ + user_id: "@alice:example.com", + usage: ["user_signing"], + keys: { + [`ed25519:${USPubKey}`]: USPubKey, + }, + }, XSK, "@alice:example.com"), + }, + }); + alice.getKeyBackupVersion = async () => { + return { + version: "1", + algorithm: "m.megolm_backup.v1.curve25519-aes-sha2", + auth_data: sign({ + public_key: "v3A8HTypbccUm6jaRCRw5l+7lSdzQUACeY9xpQ5BVmE", + }, XSK, "@alice:example.com"), + }; + }; + const origAddSecretStorageKey = alice._crypto.addSecretStorageKey; + alice._crypto.addSecretStorageKey = async function(algorithm, opts, keyId) { + return await origAddSecretStorageKey.call( + alice._crypto, algorithm, opts, keyId || "new_key_id", + ); + }; + alice.setAccountData = async function(name, data) { + const event = new MatrixEvent({ + type: name, + content: data, + }); + alice.store.storeAccountDataEvents([event]); + this.emit("accountData", event); + }; + + await alice.bootstrapSecretStorage(); + + // the new backup key should be the encoded SSSS key + const backupKeyInfo = alice.getAccountData("m.megolm_backup.v1") + .getContent(); + expect(backupKeyInfo.encrypted).toHaveProperty("new_key_id"); + expect(backupKeyInfo.encrypted).not.toHaveProperty("old_key_id"); + expect(await alice.getSecret("m.megolm_backup.v1")) + .toEqual(olmlib.encodeBase64(SSSSKey)); + }); it("adds passphrase checking if it's lacking", async function() { let crossSigningKeys = { master: XSK, From d2f24c3e872763570d60c9385ac918521c34cad9 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Mon, 13 Apr 2020 17:03:17 -0400 Subject: [PATCH 8/8] cut long line to appease lint --- spec/unit/crypto/secrets.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/unit/crypto/secrets.spec.js b/spec/unit/crypto/secrets.spec.js index 0e3318de44e..5f56e103450 100644 --- a/spec/unit/crypto/secrets.spec.js +++ b/spec/unit/crypto/secrets.spec.js @@ -578,7 +578,7 @@ describe("Secrets", function() { expect(await alice.getSecret("m.megolm_backup.v1")) .toEqual(olmlib.encodeBase64(backupKey)); }); - it("converts asymmetric SSSS with passthrough to symmetric SSSS", async function() { + it("converts asymmetric SSSS with passthrough", async function() { let crossSigningKeys = {}; const secretStorageKeys = { "old_key_id": SSSSKey,