Skip to content

Commit

Permalink
ElementR: Add rust-crypto#createRecoveryKeyFromPassphrase implement…
Browse files Browse the repository at this point in the history
…ation (#3472)

* Add `rust-crypto#createRecoveryKeyFromPassphrase` implementation

* Use `crypto`

* Rename `IRecoveryKey` into `GeneratedSecretStorageKey` for rust crypto

* Improve comments

* Improve `createRecoveryKeyFromPassphrase`
  • Loading branch information
florianduros authored Jun 14, 2023
1 parent d14fc42 commit 0545f6d
Show file tree
Hide file tree
Showing 4 changed files with 98 additions and 11 deletions.
33 changes: 33 additions & 0 deletions spec/unit/rust-crypto/rust-crypto.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 26 additions & 0 deletions src/crypto-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
*
Expand Down Expand Up @@ -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<CrossSigningStatus>;

/**
* 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<GeneratedSecretStorageKey>;
}

/**
Expand Down
12 changes: 3 additions & 9 deletions src/crypto/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -66,20 +66,14 @@ 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.
* @returns Promise resolving to an object with public key metadata, encoded private
* recovery key which should be disposed of after displaying to the user,
* and raw private key to avoid round tripping if needed.
*/
createSecretStorageKey?: () => Promise<IRecoveryKey>;
createSecretStorageKey?: () => Promise<GeneratedSecretStorageKey>;

/**
* The current key backup object. If passed,
Expand Down
38 changes: 36 additions & 2 deletions src/rust-crypto/rust-crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -405,6 +409,36 @@ export class RustCrypto implements CryptoBackend {
};
}

/**
* Implementation of {@link CryptoApi#createRecoveryKeyFromPassphrase}
*/
public async createRecoveryKeyFromPassphrase(password?: string): Promise<GeneratedSecretStorageKey> {
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
Expand Down

0 comments on commit 0545f6d

Please sign in to comment.