Skip to content

Commit 8d97d14

Browse files
committed
Add rust-crypto#createRecoveryKeyFromPassphrase implementation
1 parent 9f30def commit 8d97d14

File tree

5 files changed

+96
-9
lines changed

5 files changed

+96
-9
lines changed

spec/setupTests.ts

+5
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,13 @@ limitations under the License.
1515
*/
1616

1717
import DOMException from "domexception";
18+
import * as crypto from "crypto";
1819

1920
global.DOMException = DOMException as typeof global.DOMException;
21+
// @ts-ignore
22+
// Set node crypto into global.crypto
23+
// Needed in `CryptoApi#createRecoveryKeyFromPassphrase` and `crypto/crypto.ts`
24+
global.crypto = crypto.webcrypto;
2025

2126
jest.mock("../src/http-api/utils", () => ({
2227
...jest.requireActual("../src/http-api/utils"),

spec/unit/rust-crypto/rust-crypto.spec.ts

+33
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,39 @@ describe("RustCrypto", () => {
356356
expect(res).toBe(null);
357357
});
358358
});
359+
360+
describe("createRecoveryKeyFromPassphrase", () => {
361+
let rustCrypto: RustCrypto;
362+
363+
beforeEach(async () => {
364+
rustCrypto = await makeTestRustCrypto();
365+
});
366+
367+
it("should create a recovery key without password", async () => {
368+
const recoveryKey = await rustCrypto.createRecoveryKeyFromPassphrase();
369+
370+
// Expected the encoded private key to have 59 chars
371+
expect(recoveryKey.encodedPrivateKey?.length).toBe(59);
372+
// Expect the private key to be an Uint8Array with a length of 32
373+
expect(recoveryKey.privateKey).toBeInstanceOf(Uint8Array);
374+
expect(recoveryKey.privateKey.length).toBe(32);
375+
// Expect keyInfo to be empty
376+
expect(Object.keys(recoveryKey.keyInfo!).length).toBe(0);
377+
});
378+
379+
it("should create a recovery key with password", async () => {
380+
const recoveryKey = await rustCrypto.createRecoveryKeyFromPassphrase("my password");
381+
382+
// Expected the encoded private key to have 59 chars
383+
expect(recoveryKey.encodedPrivateKey?.length).toBe(59);
384+
// Expect the private key to be an Uint8Array with a length of 32
385+
expect(recoveryKey.privateKey).toBeInstanceOf(Uint8Array);
386+
expect(recoveryKey.privateKey.length).toBe(32);
387+
// Expect keyInfo.passphrase to be filled
388+
expect(recoveryKey.keyInfo?.passphrase?.algorithm).toBe("m.pbkdf2");
389+
expect(recoveryKey.keyInfo?.passphrase?.iterations).toBe(500000);
390+
});
391+
});
359392
});
360393

361394
/** build a basic RustCrypto instance for testing

src/crypto-api.ts

+23
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import type { IMegolmSessionData } from "./@types/crypto";
1818
import { Room } from "./models/room";
1919
import { DeviceMap } from "./models/device";
2020
import { UIAuthCallback } from "./interactive-auth";
21+
import { AddSecretStorageKeyOpts } from "./secret-storage";
2122

2223
/** Types of cross-signing key */
2324
export enum CrossSigningKey {
@@ -26,6 +27,16 @@ export enum CrossSigningKey {
2627
UserSigning = "user_signing",
2728
}
2829

30+
/**
31+
* Recovery key created by {@link CryptoApi#createRecoveryKeyFromPassphrase}
32+
*/
33+
export interface IRecoveryKey {
34+
keyInfo?: AddSecretStorageKeyOpts;
35+
/* Generated private key Uint8Array(32) */
36+
privateKey: Uint8Array;
37+
encodedPrivateKey?: string;
38+
}
39+
2940
/**
3041
* Public interface to the cryptography parts of the js-sdk
3142
*
@@ -201,6 +212,18 @@ export interface CryptoApi {
201212
* @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.
202213
*/
203214
getCrossSigningStatus(): Promise<CrossSigningStatus>;
215+
216+
/**
217+
* Create a recovery key from a user-supplied passphrase.
218+
*
219+
* @param password - Passphrase string that can be entered by the user
220+
* when restoring the backup as an alternative to entering the recovery key.
221+
* Optional.
222+
* @returns Object with encoded private
223+
* recovery key which should be disposed of after displaying to the user,
224+
* and raw private key to avoid round tripping if needed.
225+
*/
226+
createRecoveryKeyFromPassphrase(password?: string): Promise<IRecoveryKey>;
204227
}
205228

206229
/**

src/crypto/api.ts

+2-8
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@ limitations under the License.
1616

1717
import { DeviceInfo } from "./deviceinfo";
1818
import { IKeyBackupInfo } from "./keybackup";
19-
import type { AddSecretStorageKeyOpts } from "../secret-storage";
19+
import { IRecoveryKey } from "../crypto-api";
2020

2121
/* re-exports for backwards compatibility. */
22-
export { CrossSigningKey } from "../crypto-api";
22+
export { CrossSigningKey, IRecoveryKey } from "../crypto-api";
2323

2424
export type {
2525
ImportRoomKeyProgressData as IImportOpts,
@@ -66,12 +66,6 @@ export interface IEncryptedEventInfo {
6666
mismatchedSender: boolean;
6767
}
6868

69-
export interface IRecoveryKey {
70-
keyInfo?: AddSecretStorageKeyOpts;
71-
privateKey: Uint8Array;
72-
encodedPrivateKey?: string;
73-
}
74-
7569
export interface ICreateSecretStorageOpts {
7670
/**
7771
* Function called to await a secret storage key creation flow.

src/rust-crypto/rust-crypto.ts

+33-1
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,11 @@ import { deviceKeysToDeviceMap, rustDeviceToJsDevice } from "./device-converter"
4141
import { IDownloadKeyResult, IQueryKeysRequest } from "../client";
4242
import { Device, DeviceMap } from "../models/device";
4343
import { ServerSideSecretStorage } from "../secret-storage";
44-
import { CrossSigningKey } from "../crypto/api";
44+
import { CrossSigningKey, IRecoveryKey } from "../crypto/api";
4545
import { CrossSigningIdentity } from "./CrossSigningIdentity";
4646
import { secretStorageContainsCrossSigningKeys } from "./secret-storage";
47+
import { keyFromPassphrase } from "../crypto/key_passphrase";
48+
import { encodeRecoveryKey } from "../crypto/recoverykey";
4749

4850
/**
4951
* An implementation of {@link CryptoBackend} using the Rust matrix-sdk-crypto.
@@ -405,6 +407,36 @@ export class RustCrypto implements CryptoBackend {
405407
};
406408
}
407409

410+
/**
411+
* Implementation of {@link CryptoApi#createRecoveryKeyFromPassphrase}
412+
*/
413+
public async createRecoveryKeyFromPassphrase(password?: string): Promise<IRecoveryKey> {
414+
let privateKey: Uint8Array;
415+
416+
const keyInfo: Partial<IRecoveryKey["keyInfo"]> = {};
417+
if (password) {
418+
// Get the private key from the passphrase
419+
const derivation = await keyFromPassphrase(password);
420+
keyInfo.passphrase = {
421+
algorithm: "m.pbkdf2",
422+
iterations: derivation.iterations,
423+
salt: derivation.salt,
424+
};
425+
privateKey = derivation.key;
426+
} else {
427+
// Using the navigator crypto API to generate the private key
428+
privateKey = new Uint8Array(32);
429+
global.crypto.getRandomValues(privateKey);
430+
}
431+
432+
const encodedPrivateKey = encodeRecoveryKey(privateKey);
433+
return {
434+
keyInfo: keyInfo as IRecoveryKey["keyInfo"],
435+
encodedPrivateKey,
436+
privateKey,
437+
};
438+
}
439+
408440
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
409441
//
410442
// SyncCryptoCallbacks implementation

0 commit comments

Comments
 (0)