From 0545f6df096a1322bb2761aec9fb1a0b922df450 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Wed, 14 Jun 2023 16:38:43 +0200 Subject: [PATCH] ElementR: Add `rust-crypto#createRecoveryKeyFromPassphrase` implementation (#3472) * Add `rust-crypto#createRecoveryKeyFromPassphrase` implementation * Use `crypto` * Rename `IRecoveryKey` into `GeneratedSecretStorageKey` for rust crypto * Improve comments * Improve `createRecoveryKeyFromPassphrase` --- spec/unit/rust-crypto/rust-crypto.spec.ts | 33 ++++++++++++++++++++ src/crypto-api.ts | 26 ++++++++++++++++ src/crypto/api.ts | 12 ++----- src/rust-crypto/rust-crypto.ts | 38 +++++++++++++++++++++-- 4 files changed, 98 insertions(+), 11 deletions(-) diff --git a/spec/unit/rust-crypto/rust-crypto.spec.ts b/spec/unit/rust-crypto/rust-crypto.spec.ts index 8f44d041424..1e617c22d11 100644 --- a/spec/unit/rust-crypto/rust-crypto.spec.ts +++ b/spec/unit/rust-crypto/rust-crypto.spec.ts @@ -356,6 +356,39 @@ describe("RustCrypto", () => { expect(res).toBe(null); }); }); + + describe("createRecoveryKeyFromPassphrase", () => { + let rustCrypto: RustCrypto; + + beforeEach(async () => { + rustCrypto = await makeTestRustCrypto(); + }); + + it("should create a recovery key without password", async () => { + const recoveryKey = await rustCrypto.createRecoveryKeyFromPassphrase(); + + // Expected the encoded private key to have 59 chars + expect(recoveryKey.encodedPrivateKey?.length).toBe(59); + // Expect the private key to be an Uint8Array with a length of 32 + expect(recoveryKey.privateKey).toBeInstanceOf(Uint8Array); + expect(recoveryKey.privateKey.length).toBe(32); + // Expect keyInfo to be empty + expect(Object.keys(recoveryKey.keyInfo!).length).toBe(0); + }); + + it("should create a recovery key with password", async () => { + const recoveryKey = await rustCrypto.createRecoveryKeyFromPassphrase("my password"); + + // Expected the encoded private key to have 59 chars + expect(recoveryKey.encodedPrivateKey?.length).toBe(59); + // Expect the private key to be an Uint8Array with a length of 32 + expect(recoveryKey.privateKey).toBeInstanceOf(Uint8Array); + expect(recoveryKey.privateKey.length).toBe(32); + // Expect keyInfo.passphrase to be filled + expect(recoveryKey.keyInfo?.passphrase?.algorithm).toBe("m.pbkdf2"); + expect(recoveryKey.keyInfo?.passphrase?.iterations).toBe(500000); + }); + }); }); /** build a basic RustCrypto instance for testing diff --git a/src/crypto-api.ts b/src/crypto-api.ts index 8c89c408615..7f438307b1e 100644 --- a/src/crypto-api.ts +++ b/src/crypto-api.ts @@ -18,6 +18,7 @@ import type { IMegolmSessionData } from "./@types/crypto"; import { Room } from "./models/room"; import { DeviceMap } from "./models/device"; import { UIAuthCallback } from "./interactive-auth"; +import { AddSecretStorageKeyOpts } from "./secret-storage"; /** Types of cross-signing key */ export enum CrossSigningKey { @@ -26,6 +27,17 @@ export enum CrossSigningKey { UserSigning = "user_signing", } +/** + * Recovery key created by {@link CryptoApi#createRecoveryKeyFromPassphrase} + */ +export interface GeneratedSecretStorageKey { + keyInfo?: AddSecretStorageKeyOpts; + /** The raw generated private key. */ + privateKey: Uint8Array; + /** The generated key, encoded for display to the user per https://spec.matrix.org/v1.7/client-server-api/#key-representation. */ + encodedPrivateKey?: string; +} + /** * Public interface to the cryptography parts of the js-sdk * @@ -201,6 +213,20 @@ export interface CryptoApi { * @returns The current status of cross-signing keys: whether we have public and private keys cached locally, and whether the private keys are in secret storage. */ getCrossSigningStatus(): Promise; + + /** + * Create a recovery key (ie, a key suitable for use with server-side secret storage). + * + * The key can either be based on a user-supplied passphrase, or just created randomly. + * + * @param password - Optional passphrase string to use to derive the key, + * which can later be entered by the user as an alternative to entering the + * recovery key itself. If omitted, a key is generated randomly. + * + * @returns Object including recovery key and server upload parameters. + * The private key should be disposed of after displaying to the use. + */ + createRecoveryKeyFromPassphrase(password?: string): Promise; } /** diff --git a/src/crypto/api.ts b/src/crypto/api.ts index db9503300ff..c676389099d 100644 --- a/src/crypto/api.ts +++ b/src/crypto/api.ts @@ -16,10 +16,10 @@ limitations under the License. import { DeviceInfo } from "./deviceinfo"; import { IKeyBackupInfo } from "./keybackup"; -import type { AddSecretStorageKeyOpts } from "../secret-storage"; +import { GeneratedSecretStorageKey } from "../crypto-api"; /* re-exports for backwards compatibility. */ -export { CrossSigningKey } from "../crypto-api"; +export { CrossSigningKey, GeneratedSecretStorageKey as IRecoveryKey } from "../crypto-api"; export type { ImportRoomKeyProgressData as IImportOpts, @@ -66,12 +66,6 @@ export interface IEncryptedEventInfo { mismatchedSender: boolean; } -export interface IRecoveryKey { - keyInfo?: AddSecretStorageKeyOpts; - privateKey: Uint8Array; - encodedPrivateKey?: string; -} - export interface ICreateSecretStorageOpts { /** * Function called to await a secret storage key creation flow. @@ -79,7 +73,7 @@ export interface ICreateSecretStorageOpts { * recovery key which should be disposed of after displaying to the user, * and raw private key to avoid round tripping if needed. */ - createSecretStorageKey?: () => Promise; + createSecretStorageKey?: () => Promise; /** * The current key backup object. If passed, diff --git a/src/rust-crypto/rust-crypto.ts b/src/rust-crypto/rust-crypto.ts index b23c7fb4662..598c4b00073 100644 --- a/src/rust-crypto/rust-crypto.ts +++ b/src/rust-crypto/rust-crypto.ts @@ -34,16 +34,20 @@ import { BootstrapCrossSigningOpts, CrossSigningStatus, DeviceVerificationStatus, + GeneratedSecretStorageKey, ImportRoomKeyProgressData, ImportRoomKeysOpts, + CrossSigningKey, } from "../crypto-api"; import { deviceKeysToDeviceMap, rustDeviceToJsDevice } from "./device-converter"; import { IDownloadKeyResult, IQueryKeysRequest } from "../client"; import { Device, DeviceMap } from "../models/device"; -import { ServerSideSecretStorage } from "../secret-storage"; -import { CrossSigningKey } from "../crypto/api"; +import { AddSecretStorageKeyOpts, ServerSideSecretStorage } from "../secret-storage"; import { CrossSigningIdentity } from "./CrossSigningIdentity"; import { secretStorageContainsCrossSigningKeys } from "./secret-storage"; +import { keyFromPassphrase } from "../crypto/key_passphrase"; +import { encodeRecoveryKey } from "../crypto/recoverykey"; +import { crypto } from "../crypto/crypto"; /** * An implementation of {@link CryptoBackend} using the Rust matrix-sdk-crypto. @@ -405,6 +409,36 @@ export class RustCrypto implements CryptoBackend { }; } + /** + * Implementation of {@link CryptoApi#createRecoveryKeyFromPassphrase} + */ + public async createRecoveryKeyFromPassphrase(password?: string): Promise { + let key: Uint8Array; + + const keyInfo: AddSecretStorageKeyOpts = {}; + if (password) { + // Generate the key from the passphrase + const derivation = await keyFromPassphrase(password); + keyInfo.passphrase = { + algorithm: "m.pbkdf2", + iterations: derivation.iterations, + salt: derivation.salt, + }; + key = derivation.key; + } else { + // Using the navigator crypto API to generate the private key + key = new Uint8Array(32); + crypto.getRandomValues(key); + } + + const encodedPrivateKey = encodeRecoveryKey(key); + return { + keyInfo, + encodedPrivateKey, + privateKey: key, + }; + } + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // SyncCryptoCallbacks implementation