Skip to content

Commit 0545f6d

Browse files
authored
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`
1 parent d14fc42 commit 0545f6d

File tree

4 files changed

+98
-11
lines changed

4 files changed

+98
-11
lines changed

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

+26
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,17 @@ export enum CrossSigningKey {
2627
UserSigning = "user_signing",
2728
}
2829

30+
/**
31+
* Recovery key created by {@link CryptoApi#createRecoveryKeyFromPassphrase}
32+
*/
33+
export interface GeneratedSecretStorageKey {
34+
keyInfo?: AddSecretStorageKeyOpts;
35+
/** The raw generated private key. */
36+
privateKey: Uint8Array;
37+
/** The generated key, encoded for display to the user per https://spec.matrix.org/v1.7/client-server-api/#key-representation. */
38+
encodedPrivateKey?: string;
39+
}
40+
2941
/**
3042
* Public interface to the cryptography parts of the js-sdk
3143
*
@@ -201,6 +213,20 @@ export interface CryptoApi {
201213
* @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.
202214
*/
203215
getCrossSigningStatus(): Promise<CrossSigningStatus>;
216+
217+
/**
218+
* Create a recovery key (ie, a key suitable for use with server-side secret storage).
219+
*
220+
* The key can either be based on a user-supplied passphrase, or just created randomly.
221+
*
222+
* @param password - Optional passphrase string to use to derive the key,
223+
* which can later be entered by the user as an alternative to entering the
224+
* recovery key itself. If omitted, a key is generated randomly.
225+
*
226+
* @returns Object including recovery key and server upload parameters.
227+
* The private key should be disposed of after displaying to the use.
228+
*/
229+
createRecoveryKeyFromPassphrase(password?: string): Promise<GeneratedSecretStorageKey>;
204230
}
205231

206232
/**

src/crypto/api.ts

+3-9
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 { GeneratedSecretStorageKey } from "../crypto-api";
2020

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

2424
export type {
2525
ImportRoomKeyProgressData as IImportOpts,
@@ -66,20 +66,14 @@ 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.
7872
* @returns Promise resolving to an object with public key metadata, encoded private
7973
* recovery key which should be disposed of after displaying to the user,
8074
* and raw private key to avoid round tripping if needed.
8175
*/
82-
createSecretStorageKey?: () => Promise<IRecoveryKey>;
76+
createSecretStorageKey?: () => Promise<GeneratedSecretStorageKey>;
8377

8478
/**
8579
* The current key backup object. If passed,

src/rust-crypto/rust-crypto.ts

+36-2
Original file line numberDiff line numberDiff line change
@@ -34,16 +34,20 @@ import {
3434
BootstrapCrossSigningOpts,
3535
CrossSigningStatus,
3636
DeviceVerificationStatus,
37+
GeneratedSecretStorageKey,
3738
ImportRoomKeyProgressData,
3839
ImportRoomKeysOpts,
40+
CrossSigningKey,
3941
} from "../crypto-api";
4042
import { deviceKeysToDeviceMap, rustDeviceToJsDevice } from "./device-converter";
4143
import { IDownloadKeyResult, IQueryKeysRequest } from "../client";
4244
import { Device, DeviceMap } from "../models/device";
43-
import { ServerSideSecretStorage } from "../secret-storage";
44-
import { CrossSigningKey } from "../crypto/api";
45+
import { AddSecretStorageKeyOpts, ServerSideSecretStorage } from "../secret-storage";
4546
import { CrossSigningIdentity } from "./CrossSigningIdentity";
4647
import { secretStorageContainsCrossSigningKeys } from "./secret-storage";
48+
import { keyFromPassphrase } from "../crypto/key_passphrase";
49+
import { encodeRecoveryKey } from "../crypto/recoverykey";
50+
import { crypto } from "../crypto/crypto";
4751

4852
/**
4953
* An implementation of {@link CryptoBackend} using the Rust matrix-sdk-crypto.
@@ -405,6 +409,36 @@ export class RustCrypto implements CryptoBackend {
405409
};
406410
}
407411

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

0 commit comments

Comments
 (0)