From 25aac4b7c0f074e92a4d3ebd1632d60186e744b3 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Thu, 30 Mar 2023 17:43:52 +0200 Subject: [PATCH 1/4] WIP --- src/matrix/Session.js | 16 +++++++++++----- src/matrix/ssss/SecretStorage.ts | 11 +++++++++++ 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/matrix/Session.js b/src/matrix/Session.js index 23c9cce6fe..6d3d55bc9b 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -89,6 +89,7 @@ export class Session { this._megolmDecryption = null; this._getSyncToken = () => this.syncToken; this._olmWorker = olmWorker; + this._secretStorage = undefined; this._keyBackup = new ObservableValue(undefined); this._crossSigning = new ObservableValue(undefined); this._observedRoomStatus = new Map(); @@ -332,13 +333,14 @@ export class Session { const isValid = await secretStorage.hasValidKeyForAnyAccountData(); log.set("isValid", isValid); if (isValid) { - await this._loadSecretStorageServices(secretStorage, log); + this._secretStorage = secretStorage; + await this._loadSecretStorageService(log); } return isValid; }); } - async _loadSecretStorageServices(secretStorage, log) { + async _loadSecretStorageServices(log) { try { await log.wrap("enable key backup", async log => { const keyBackup = new KeyBackup( @@ -348,7 +350,7 @@ export class Session { this._storage, this._platform, ); - if (await keyBackup.load(secretStorage, log)) { + if (await keyBackup.load(this._secretStorage, log)) { for (const room of this._rooms.values()) { if (room.isEncrypted) { room.enableKeyBackup(keyBackup); @@ -364,7 +366,7 @@ export class Session { await log.wrap("enable cross-signing", async log => { const crossSigning = new CrossSigning({ storage: this._storage, - secretStorage, + secretStorage: this._secretStorage, platform: this._platform, olm: this._olm, olmUtil: this._olmUtil, @@ -775,12 +777,13 @@ export class Session { txn.accountData.set(event); } } + changes.accountData = accountData.events; } return changes; } /** @internal */ - afterSync({syncInfo, e2eeAccountChanges}) { + afterSync({syncInfo, e2eeAccountChanges, accountData}) { if (syncInfo) { // sync transaction succeeded, modify object state now this._syncInfo = syncInfo; @@ -788,6 +791,9 @@ export class Session { if (this._e2eeAccount) { this._e2eeAccount.afterSync(e2eeAccountChanges); } + if (accountData && this._secretStorage) { + this._secretStorage.afterSync(accountData); + } } /** @internal */ diff --git a/src/matrix/ssss/SecretStorage.ts b/src/matrix/ssss/SecretStorage.ts index 4c767bbbfe..a5e157682d 100644 --- a/src/matrix/ssss/SecretStorage.ts +++ b/src/matrix/ssss/SecretStorage.ts @@ -41,6 +41,7 @@ export class SecretStorage { private readonly _key: Key; private readonly _platform: Platform; private readonly _storage: Storage; + private observedSecrets constructor({key, platform, storage}: {key: Key, platform: Platform, storage: Storage}) { this._key = key; @@ -48,6 +49,12 @@ export class SecretStorage { this._storage = storage; } + afterSync(accountData: ReadonlyArray<{type: string, content: Record}>): void { + for(const event of accountData) { + if (type === ) + } + } + /** this method will auto-commit any indexeddb transaction because of its use of the webcrypto api */ async hasValidKeyForAnyAccountData() { const txn = await this._storage.readTxn([ @@ -69,6 +76,10 @@ export class SecretStorage { return false; } + observeSecret(name: string): BaseObservableValue { + + } + /** this method will auto-commit any indexeddb transaction because of its use of the webcrypto api */ async readSecret(name: string): Promise { const txn = await this._storage.readTxn([ From 8cf7a1b161e26371958a7751d5b66a04d81ad26c Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 31 Mar 2023 17:42:38 +0200 Subject: [PATCH 2/4] WIP2 --- src/matrix/Session.js | 10 ++-- src/matrix/ssss/SecretStorage.ts | 78 +++++++++++++++++++++++++++++--- src/matrix/ssss/index.ts | 4 +- src/matrix/ssss/passphrase.ts | 2 +- 4 files changed, 81 insertions(+), 13 deletions(-) diff --git a/src/matrix/Session.js b/src/matrix/Session.js index 6d3d55bc9b..30335d23d2 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -777,13 +777,15 @@ export class Session { txn.accountData.set(event); } } - changes.accountData = accountData.events; + if (this._secretStorage) { + changes.secretStorageChanges = await this._secretStorage.writeSync(accountData.events, txn, log); + } } return changes; } /** @internal */ - afterSync({syncInfo, e2eeAccountChanges, accountData}) { + afterSync({syncInfo, e2eeAccountChanges, secretStorageChanges}) { if (syncInfo) { // sync transaction succeeded, modify object state now this._syncInfo = syncInfo; @@ -791,8 +793,8 @@ export class Session { if (this._e2eeAccount) { this._e2eeAccount.afterSync(e2eeAccountChanges); } - if (accountData && this._secretStorage) { - this._secretStorage.afterSync(accountData); + if (secretStorageChanges && this._secretStorage) { + this._secretStorage.afterSync(secretStorageChanges); } } diff --git a/src/matrix/ssss/SecretStorage.ts b/src/matrix/ssss/SecretStorage.ts index a5e157682d..e8e1308027 100644 --- a/src/matrix/ssss/SecretStorage.ts +++ b/src/matrix/ssss/SecretStorage.ts @@ -13,7 +13,11 @@ 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 type {Key} from "./common"; +import {BaseObservableValue, RetainedObservableValue} from "../../observable/value"; +import {KeyType} from "./index"; +import {keyFromPassphrase} from "./passphrase"; +import {keyFromRecoveryKey} from "./recoveryKey"; +import type {Key, KeyDescription} from "./common"; import type {Platform} from "../../platform/web/Platform.js"; import type {Transaction} from "../storage/idb/Transaction"; import type {Storage} from "../storage/idb/Storage"; @@ -37,11 +41,25 @@ class DecryptionError extends Error { } } +type AccountData = {type: string, content: Record}; + +type KeyCredentials = { + type: KeyType, + credential: string +} + export class SecretStorage { - private readonly _key: Key; + // we know the id but don't have the description yet + private _keyId?: string; + // we have the description but not the credentials yet + private _keyDescription?: KeyDescription; + // we have the credentials but not the id or description yet + private _keyCredentials?: KeyCredentials; + // we have everything to compose a valid key + private _key?: Key; private readonly _platform: Platform; private readonly _storage: Storage; - private observedSecrets + private observedSecrets: Map>; constructor({key, platform, storage}: {key: Key, platform: Platform, storage: Storage}) { this._key = key; @@ -49,12 +67,60 @@ export class SecretStorage { this._storage = storage; } - afterSync(accountData: ReadonlyArray<{type: string, content: Record}>): void { - for(const event of accountData) { - if (type === ) + load() { + // read key + } + + async setKey(type: KeyType, credential: string) { + const credentials: KeyCredentials = {type, credential}; + this._keyCredentials = credentials; + this.updateKey(this._keyDescription, this._keyCredentials); + } + + async setKeyWithDehydratedDeviceKey() { + + } + + private async updateKey(keyDescription: KeyDescription | undefined, credentials: KeyCredentials | undefined, txn: Transaction) { + if (keyDescription && credentials) { + if (credentials.type === KeyType.Passphrase) { + this._key = await keyFromPassphrase(keyDescription, credentials.credential, this._platform); + } else if (credentials.type === KeyType.RecoveryKey) { + this._key = await keyFromRecoveryKey(keyDescription, credentials.credential, this._olm, this._platform); + } + // + } + } + + private update(keyDescription: KeyDescription, credentials: KeyCredentials) { + + } + + writeSync(accountData: ReadonlyArray, txn: Transaction, log: ILogItem): Promise { + + const newDefaultKey = accountData.find(e => e.type === "m.secret_storage.default_key"); + const keyId: string | undefined = newDefaultKey ? newDefaultKey.content?.key : this._keyId; + const keyEventType = keyId ? `m.secret_storage.key.${keyId}` : undefined; + let newKey = keyEventType ? accountData.find(e => e.type === keyEventType) : undefined; + if (newDefaultKey && keyEventType && !newKey) { + newKey = await txn.accountData.get(keyEventType); + } + if (newKey) { + this.setKeyDescription() + } + const keyChanged = !!newDefaultKey || !!newKey; + if (keyChanged) { + // update all values + } else { + for(const event of accountData) { + + } } } + afterSync(): void { + } + /** this method will auto-commit any indexeddb transaction because of its use of the webcrypto api */ async hasValidKeyForAnyAccountData() { const txn = await this._storage.readTxn([ diff --git a/src/matrix/ssss/index.ts b/src/matrix/ssss/index.ts index 02f3290e70..3b939a92a5 100644 --- a/src/matrix/ssss/index.ts +++ b/src/matrix/ssss/index.ts @@ -30,8 +30,8 @@ const SSSS_KEY = `${SESSION_E2EE_KEY_PREFIX}ssssKey`; const BACKUPVERSION_KEY = `${SESSION_E2EE_KEY_PREFIX}keyBackupVersion`; export enum KeyType { - "RecoveryKey", - "Passphrase" + RecoveryKey, + Passphrase } async function readDefaultKeyDescription(storage: Storage): Promise { diff --git a/src/matrix/ssss/passphrase.ts b/src/matrix/ssss/passphrase.ts index 00e4801e4d..32d2cb0c14 100644 --- a/src/matrix/ssss/passphrase.ts +++ b/src/matrix/ssss/passphrase.ts @@ -27,7 +27,7 @@ const DEFAULT_BITSIZE = 256; * @param {Platform} platform * @return {Key} */ -export async function keyFromPassphrase(keyDescription: KeyDescription, passphrase: string, platform: Platform): Promise { +export async function keyFromPassphrase(passphrase: string, platform: Platform): Promise { const {passphraseParams} = keyDescription; if (!passphraseParams) { throw new Error("not a passphrase key"); From 67ad975377103306371d78248dfa3da04bdc1a45 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Thu, 6 Apr 2023 14:51:38 +0200 Subject: [PATCH 3/4] WIP --- src/matrix/ssss/SecretStorage.ts | 220 ++++++++++++++++++++----------- 1 file changed, 141 insertions(+), 79 deletions(-) diff --git a/src/matrix/ssss/SecretStorage.ts b/src/matrix/ssss/SecretStorage.ts index e8e1308027..cd4edf4e85 100644 --- a/src/matrix/ssss/SecretStorage.ts +++ b/src/matrix/ssss/SecretStorage.ts @@ -17,11 +17,13 @@ import {BaseObservableValue, RetainedObservableValue} from "../../observable/val import {KeyType} from "./index"; import {keyFromPassphrase} from "./passphrase"; import {keyFromRecoveryKey} from "./recoveryKey"; -import type {Key, KeyDescription} from "./common"; +import {Key, KeyDescription, KeyDescriptionData} from "./common"; import type {Platform} from "../../platform/web/Platform.js"; import type {Transaction} from "../storage/idb/Transaction"; import type {Storage} from "../storage/idb/Storage"; import type {AccountDataEntry} from "../storage/idb/stores/AccountDataStore"; +import type * as OlmNamespace from "@matrix-org/olm"; +type Olm = typeof OlmNamespace; type EncryptedData = { iv: string; @@ -30,6 +32,7 @@ type EncryptedData = { } export enum DecryptionFailure { + NoKey = 1, NotEncryptedWithKey, BadMAC, UnsupportedAlgorithm, @@ -39,9 +42,11 @@ class DecryptionError extends Error { constructor(msg: string, public readonly reason: DecryptionFailure) { super(msg); } -} -type AccountData = {type: string, content: Record}; + toString() { + return `${this.constructor.name}: ${super.message}: ${this.reason}` + } +} type KeyCredentials = { type: KeyType, @@ -50,21 +55,22 @@ type KeyCredentials = { export class SecretStorage { // we know the id but don't have the description yet - private _keyId?: string; + private keyId?: string; // we have the description but not the credentials yet - private _keyDescription?: KeyDescription; + private keyDescription?: KeyDescription; // we have the credentials but not the id or description yet - private _keyCredentials?: KeyCredentials; + private keyCredentials?: KeyCredentials; // we have everything to compose a valid key - private _key?: Key; - private readonly _platform: Platform; - private readonly _storage: Storage; + private key?: Key; + private readonly platform: Platform; + private readonly storage: Storage; private observedSecrets: Map>; - - constructor({key, platform, storage}: {key: Key, platform: Platform, storage: Storage}) { - this._key = key; - this._platform = platform; - this._storage = storage; + private readonly olm: Olm; + constructor({platform, storage, olm}: {platform: Platform, storage: Storage, olm: Olm}) { + this.platform = platform; + this.storage = storage; + this.olm = olm; + this.observedSecrets = new Map(); } load() { @@ -73,109 +79,123 @@ export class SecretStorage { async setKey(type: KeyType, credential: string) { const credentials: KeyCredentials = {type, credential}; - this._keyCredentials = credentials; - this.updateKey(this._keyDescription, this._keyCredentials); + this.keyCredentials = credentials; + this.updateKey(this.keyDescription, this.keyCredentials); } - async setKeyWithDehydratedDeviceKey() { - + async setKeyWithDehydratedDeviceKey(dehydrationKey: Key): Promise { + const {keyDescription} = this; + if (!keyDescription) { + return false; + } + if (await keyDescription.isCompatible(dehydrationKey, this.platform)) { + const key = dehydrationKey.withDescription(keyDescription); + this.key = key; + return true; + } + return false; } - private async updateKey(keyDescription: KeyDescription | undefined, credentials: KeyCredentials | undefined, txn: Transaction) { + private async updateKey(keyDescription: KeyDescription | undefined, credentials: KeyCredentials | undefined): Promise { if (keyDescription && credentials) { if (credentials.type === KeyType.Passphrase) { - this._key = await keyFromPassphrase(keyDescription, credentials.credential, this._platform); + this.key = await keyFromPassphrase(keyDescription, credentials.credential, this.platform); + return true; } else if (credentials.type === KeyType.RecoveryKey) { - this._key = await keyFromRecoveryKey(keyDescription, credentials.credential, this._olm, this._platform); + this.key = await keyFromRecoveryKey(keyDescription, credentials.credential, this.olm, this.platform); + return true; } - // } + return false; } - private update(keyDescription: KeyDescription, credentials: KeyCredentials) { - - } - - writeSync(accountData: ReadonlyArray, txn: Transaction, log: ILogItem): Promise { - + async writeSync(accountData: ReadonlyArray, txn: Transaction): Promise<{newKey?: KeyDescription, accountData: ReadonlyArray}> { const newDefaultKey = accountData.find(e => e.type === "m.secret_storage.default_key"); - const keyId: string | undefined = newDefaultKey ? newDefaultKey.content?.key : this._keyId; + const keyId: string | undefined = newDefaultKey ? newDefaultKey.content?.key : this.keyId; const keyEventType = keyId ? `m.secret_storage.key.${keyId}` : undefined; - let newKey = keyEventType ? accountData.find(e => e.type === keyEventType) : undefined; - if (newDefaultKey && keyEventType && !newKey) { - newKey = await txn.accountData.get(keyEventType); + let newKeyData = keyEventType ? accountData.find(e => e.type === keyEventType) : undefined; + // if the default key was changed but the key itself wasn't in the sync, get it from storage + if (newDefaultKey && keyEventType && !newKeyData) { + newKeyData = await txn.accountData.get(keyEventType); } - if (newKey) { - this.setKeyDescription() - } - const keyChanged = !!newDefaultKey || !!newKey; - if (keyChanged) { - // update all values - } else { - for(const event of accountData) { - - } + let newKey: KeyDescription | undefined; + if (newKeyData && keyId) { + newKey = new KeyDescription(keyId, newKeyData.content as KeyDescriptionData); } + return { + newKey, + accountData + }; } - afterSync(): void { - } - - /** this method will auto-commit any indexeddb transaction because of its use of the webcrypto api */ - async hasValidKeyForAnyAccountData() { - const txn = await this._storage.readTxn([ - this._storage.storeNames.accountData, - ]); - const allAccountData = await txn.accountData.getAll(); - for (const accountData of allAccountData) { - try { - const secret = await this._decryptAccountData(accountData); - return true; // decryption succeeded - } catch (err) { - if (err instanceof DecryptionError && err.reason !== DecryptionFailure.NotEncryptedWithKey) { - throw err; - } else { - continue; - } - } + afterSync({newKey, accountData}: {newKey?: KeyDescription, accountData: ReadonlyArray}): void { + if (newKey) { + this.updateKeyAndAllValues(newKey); + } else if (this.key) { + const observedValues = accountData.filter(a => this.observedSecrets.has(a.type)); + Promise.all(observedValues.map(async entry => { + const observable = this.observedSecrets.get(entry.type)!; + const secret = await this.decryptAccountData(entry); + observable.set(secret); + })).then(undefined, reason => { + this.platform.logger.log("SecretStorage.afterSync: decryption failed").catch(reason); + }); } - return false; } observeSecret(name: string): BaseObservableValue { - + const existingObservable = this.observedSecrets.get(name); + if (existingObservable) { + return existingObservable; + } + const observable: RetainedObservableValue = new RetainedObservableValue(undefined, () => { + this.observedSecrets.delete(name); + }); + this.observedSecrets.set(name, observable); + this.readSecret(name).then(secret => { + observable.set(secret); + }); + return observable; } /** this method will auto-commit any indexeddb transaction because of its use of the webcrypto api */ async readSecret(name: string): Promise { - const txn = await this._storage.readTxn([ - this._storage.storeNames.accountData, + const txn = await this.storage.readTxn([ + this.storage.storeNames.accountData, ]); const accountData = await txn.accountData.get(name); if (!accountData) { return; } - return await this._decryptAccountData(accountData); + try { + return await this.decryptAccountData(accountData); + } catch (err) { + this.platform.logger.log({l: "SecretStorage.readSecret: failed to read secret", id: name}).catch(err); + return undefined; + } } - async _decryptAccountData(accountData: AccountDataEntry): Promise { - const encryptedData = accountData?.content?.encrypted?.[this._key.id] as EncryptedData; + private async decryptAccountData(accountData: AccountDataEntry): Promise { + if (!this.key) { + throw new DecryptionError("No key set", DecryptionFailure.NoKey); + } + const encryptedData = accountData?.content?.encrypted?.[this.key.id] as EncryptedData; if (!encryptedData) { - throw new DecryptionError(`Secret ${accountData.type} is not encrypted for key ${this._key.id}`, DecryptionFailure.NotEncryptedWithKey); + throw new DecryptionError(`Secret ${accountData.type} is not encrypted for key ${this.key.id}`, DecryptionFailure.NotEncryptedWithKey); } - if (this._key.algorithm === "m.secret_storage.v1.aes-hmac-sha2") { - return await this._decryptAESSecret(accountData.type, encryptedData); + if (this.key.algorithm === "m.secret_storage.v1.aes-hmac-sha2") { + return await this.decryptAESSecret(accountData.type, encryptedData, this.key.binaryKey); } else { - throw new DecryptionError(`Unsupported algorithm for key ${this._key.id}: ${this._key.algorithm}`, DecryptionFailure.UnsupportedAlgorithm); + throw new DecryptionError(`Unsupported algorithm for key ${this.key.id}: ${this.key.algorithm}`, DecryptionFailure.UnsupportedAlgorithm); } } - async _decryptAESSecret(type: string, encryptedData: EncryptedData): Promise { - const {base64, utf8} = this._platform.encoding; + private async decryptAESSecret(type: string, encryptedData: EncryptedData, binaryKey: Uint8Array): Promise { + const {base64, utf8} = this.platform.encoding; // now derive the aes and mac key from the 4s key - const hkdfKey = await this._platform.crypto.derive.hkdf( - this._key.binaryKey, + const hkdfKey = await this.platform.crypto.derive.hkdf( + binaryKey, new Uint8Array(8).buffer, //zero salt utf8.encode(type), // info "SHA-256", @@ -185,7 +205,7 @@ export class SecretStorage { const hmacKey = hkdfKey.slice(32); const ciphertextBytes = base64.decode(encryptedData.ciphertext); - const isVerified = await this._platform.crypto.hmac.verify( + const isVerified = await this.platform.crypto.hmac.verify( hmacKey, base64.decode(encryptedData.mac), ciphertextBytes, "SHA-256"); @@ -193,7 +213,7 @@ export class SecretStorage { throw new DecryptionError("Bad MAC", DecryptionFailure.BadMAC); } - const plaintextBytes = await this._platform.crypto.aes.decryptCTR({ + const plaintextBytes = await this.platform.crypto.aes.decryptCTR({ key: aesKey, iv: base64.decode(encryptedData.iv), data: ciphertextBytes @@ -201,4 +221,46 @@ export class SecretStorage { return utf8.decode(plaintextBytes); } + + private async updateKeyAndAllValues(newKey: KeyDescription) { + this.keyDescription = newKey; + if (await this.updateKey(this.keyDescription, this.keyCredentials)) { + const valuesToUpdate = Array.from(this.observedSecrets.keys()); + const txn = await this.storage.readTxn([this.storage.storeNames.accountData]); + const entries = await Promise.all(valuesToUpdate.map(type => txn.accountData.get(type))); + const foundEntries = entries.filter(e => !!e) as ReadonlyArray; + this.decryptAndUpdateEntries(foundEntries); + } + } + + private decryptAndUpdateEntries(entries: ReadonlyArray): void { + Promise.all(entries.map(async entry => { + const observable = this.observedSecrets.get(entry.type)!; + const secret = await this.decryptAccountData(entry); + observable.set(secret); + })).then(undefined, reason => { + this.platform.logger.log("SecretStorage.afterSync: decryption failed").catch(reason); + }); + } + + /** this method will auto-commit any indexeddb transaction because of its use of the webcrypto api */ + private async hasValidKeyForAnyAccountData() { + const txn = await this.storage.readTxn([ + this.storage.storeNames.accountData, + ]); + const allAccountData = await txn.accountData.getAll(); + for (const accountData of allAccountData) { + try { + const secret = await this.decryptAccountData(accountData); + return true; // decryption succeeded + } catch (err) { + if (err instanceof DecryptionError && err.reason !== DecryptionFailure.NotEncryptedWithKey) { + throw err; + } else { + continue; + } + } + } + return false; + } } From caa65870f36b7f0c0d456b8adad988582893124e Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 7 Apr 2023 16:35:01 +0200 Subject: [PATCH 4/4] implementent SecretStorage.load --- src/matrix/Session.js | 12 +++++----- src/matrix/ssss/SecretStorage.ts | 41 +++++++++++++++++++++++++++----- src/matrix/ssss/index.ts | 5 ++-- src/matrix/ssss/passphrase.ts | 2 +- 4 files changed, 45 insertions(+), 15 deletions(-) diff --git a/src/matrix/Session.js b/src/matrix/Session.js index 30335d23d2..afe96afca7 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -536,12 +536,12 @@ export class Session { } } if (this._olm && this._e2eeAccount) { - // try set up session backup and cross-signing if we stored the ssss key - const ssssKey = await ssssReadKey(txn); - if (ssssKey) { - // this will close the txn above, so we do it last - await this._tryLoadSecretStorage(ssssKey, log); - } + this._secretStorage = new SecretStorage({ + platform: this._platform, + storage: this._storage, + olm: this._olm + }); + await this._secretStorage.load(txn); } } diff --git a/src/matrix/ssss/SecretStorage.ts b/src/matrix/ssss/SecretStorage.ts index cd4edf4e85..34bea0c964 100644 --- a/src/matrix/ssss/SecretStorage.ts +++ b/src/matrix/ssss/SecretStorage.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ import {BaseObservableValue, RetainedObservableValue} from "../../observable/value"; -import {KeyType} from "./index"; +import {KeyType, SSSS_KEY} from "./index"; import {keyFromPassphrase} from "./passphrase"; import {keyFromRecoveryKey} from "./recoveryKey"; import {Key, KeyDescription, KeyDescriptionData} from "./common"; @@ -48,11 +48,17 @@ class DecryptionError extends Error { } } -type KeyCredentials = { - type: KeyType, +type StringKeyCredential = { + type: KeyType.Passphrase | KeyType.RecoveryKey, credential: string } +type BitsKeyCredential = { + type: KeyType.KeyBits, + credential: Uint8Array +} +type KeyCredentials = StringKeyCredential | BitsKeyCredential; + export class SecretStorage { // we know the id but don't have the description yet private keyId?: string; @@ -73,11 +79,32 @@ export class SecretStorage { this.observedSecrets = new Map(); } - load() { - // read key + async load(txn: Transaction) { + // first try to read the key bits from previously enabling 4S + const keyData = await txn.session.get(SSSS_KEY); + if (keyData) { + const keyAccountData = await txn.accountData.get(`m.secret_storage.key.${keyData.id}`); + if (keyAccountData) { + this.keyId = keyData.id; + this.keyDescription = new KeyDescription(keyData.id, keyAccountData.content as KeyDescriptionData); + this.keyCredentials = {type: KeyType.KeyBits, credential: keyData.binaryKey}; + } + } else { + // then prepare to track the default key + const defaultKey = await txn.accountData.get("m.secret_storage.default_key"); + const keyId = defaultKey?.content?.key; + if (keyId) { + this.keyId = keyId; + const keyData = await txn.accountData.get(`m.secret_storage.key.${keyId}`); + if (keyData) { + this.keyDescription = new KeyDescription(keyId, keyData.content as KeyDescriptionData); + } + } + } + this.updateKey(this.keyDescription, this.keyCredentials); } - async setKey(type: KeyType, credential: string) { + async setKey(type: KeyType.Passphrase | KeyType.RecoveryKey, credential: string) { const credentials: KeyCredentials = {type, credential}; this.keyCredentials = credentials; this.updateKey(this.keyDescription, this.keyCredentials); @@ -104,6 +131,8 @@ export class SecretStorage { } else if (credentials.type === KeyType.RecoveryKey) { this.key = await keyFromRecoveryKey(keyDescription, credentials.credential, this.olm, this.platform); return true; + } else if (credentials.type === KeyType.KeyBits) { + this.key = new Key(keyDescription, credentials.credential); } } return false; diff --git a/src/matrix/ssss/index.ts b/src/matrix/ssss/index.ts index 3b939a92a5..12ddb998db 100644 --- a/src/matrix/ssss/index.ts +++ b/src/matrix/ssss/index.ts @@ -26,12 +26,13 @@ import type * as OlmNamespace from "@matrix-org/olm" type Olm = typeof OlmNamespace; -const SSSS_KEY = `${SESSION_E2EE_KEY_PREFIX}ssssKey`; +export const SSSS_KEY = `${SESSION_E2EE_KEY_PREFIX}ssssKey`; const BACKUPVERSION_KEY = `${SESSION_E2EE_KEY_PREFIX}keyBackupVersion`; export enum KeyType { RecoveryKey, - Passphrase + Passphrase, + KeyBits // decoded bits from either recovery key or passphrase, as stored on disk } async function readDefaultKeyDescription(storage: Storage): Promise { diff --git a/src/matrix/ssss/passphrase.ts b/src/matrix/ssss/passphrase.ts index 32d2cb0c14..00e4801e4d 100644 --- a/src/matrix/ssss/passphrase.ts +++ b/src/matrix/ssss/passphrase.ts @@ -27,7 +27,7 @@ const DEFAULT_BITSIZE = 256; * @param {Platform} platform * @return {Key} */ -export async function keyFromPassphrase(passphrase: string, platform: Platform): Promise { +export async function keyFromPassphrase(keyDescription: KeyDescription, passphrase: string, platform: Platform): Promise { const {passphraseParams} = keyDescription; if (!passphraseParams) { throw new Error("not a passphrase key");