Skip to content

Commit

Permalink
Merge branch 'develop' into johannes/lint-file-extensions
Browse files Browse the repository at this point in the history
  • Loading branch information
Johennes committed Sep 17, 2024
2 parents 051c0f2 + fab9cab commit 04029f3
Show file tree
Hide file tree
Showing 7 changed files with 170 additions and 63 deletions.
38 changes: 38 additions & 0 deletions spec/unit/common-crypto/key-passphrase.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright 2024 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* 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 { keyFromAuthData } from "../../../src/common-crypto/key-passphrase.ts";

describe("key-passphrase", () => {
describe("keyFromAuthData", () => {
it("should throw an error if salt or iterations are missing", async () => {
// missing salt
expect(() => keyFromAuthData({ private_key_iterations: 5 }, "passphrase")).toThrow(
"Salt and/or iterations not found: this backup cannot be restored with a passphrase",
);

// missing iterations
expect(() => keyFromAuthData({ private_key_salt: "salt" }, "passphrase")).toThrow(
"Salt and/or iterations not found: this backup cannot be restored with a passphrase",
);
});

it("should derive key from auth data", async () => {
const key = await keyFromAuthData({ private_key_salt: "salt", private_key_iterations: 5 }, "passphrase");
expect(key).toBeDefined();
});
});
});
3 changes: 2 additions & 1 deletion src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,6 @@ import {
isCryptoAvailable,
} from "./crypto/index.ts";
import { DeviceInfo } from "./crypto/deviceinfo.ts";
import { keyFromAuthData } from "./crypto/key_passphrase.ts";
import { User, UserEvent, UserEventHandlerMap } from "./models/user.ts";
import { getHttpUriForMxc } from "./content-repo.ts";
import { SearchResult } from "./models/search-result.ts";
Expand Down Expand Up @@ -244,6 +243,7 @@ import { RoomMessageEventContent, StickerEventContent } from "./@types/events.ts
import { ImageInfo } from "./@types/media.ts";
import { Capabilities, ServerCapabilities } from "./serverCapabilities.ts";
import { sha256 } from "./digest.ts";
import { keyFromAuthData } from "./common-crypto/key-passphrase.ts";

export type Store = IStore;

Expand Down Expand Up @@ -3658,6 +3658,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @param password - Passphrase
* @param backupInfo - Backup metadata from `checkKeyBackup`
* @returns key backup key
* @deprecated Deriving a backup key from a passphrase is not part of the matrix spec. Instead, a random key is generated and stored/shared via 4S.
*/
public keyBackupKeyFromPassword(password: string, backupInfo: IKeyBackupInfo): Promise<Uint8Array> {
return keyFromAuthData(backupInfo.auth_data, password);
Expand Down
43 changes: 43 additions & 0 deletions src/common-crypto/key-passphrase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright 2024 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* 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 { deriveRecoveryKeyFromPassphrase } from "../crypto-api/index.ts";

/* eslint-disable camelcase */
interface IAuthData {
private_key_salt?: string;
private_key_iterations?: number;
private_key_bits?: number;
}

/**
* Derive a backup key from a passphrase using the salt and iterations from the auth data.
* @param authData - The auth data containing the salt and iterations
* @param passphrase - The passphrase to derive the key from
* @deprecated Deriving a backup key from a passphrase is not part of the matrix spec. Instead, a random key is generated and stored/shared via 4S.
*/
export function keyFromAuthData(authData: IAuthData, passphrase: string): Promise<Uint8Array> {
if (!authData.private_key_salt || !authData.private_key_iterations) {
throw new Error("Salt and/or iterations not found: " + "this backup cannot be restored with a passphrase");
}

return deriveRecoveryKeyFromPassphrase(
passphrase,
authData.private_key_salt,
authData.private_key_iterations,
authData.private_key_bits,
);
}
1 change: 1 addition & 0 deletions src/crypto-api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -968,3 +968,4 @@ export interface OwnDeviceKeys {
export * from "./verification.ts";
export * from "./keybackup.ts";
export * from "./recovery-key.ts";
export * from "./key-passphrase.ts";
58 changes: 58 additions & 0 deletions src/crypto-api/key-passphrase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* Copyright 2024 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* 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.
*/

const DEFAULT_BIT_SIZE = 256;

/**
* Derive a recovery key from a passphrase and salt using PBKDF2.
* @see https://spec.matrix.org/v1.11/client-server-api/#deriving-keys-from-passphrases
*
* @param passphrase - The passphrase to derive the key from
* @param salt - The salt to use in the derivation
* @param iterations - The number of iterations to use in the derivation
* @param numBits - The number of bits to derive
*/
export async function deriveRecoveryKeyFromPassphrase(
passphrase: string,
salt: string,
iterations: number,
numBits = DEFAULT_BIT_SIZE,
): Promise<Uint8Array> {
if (!globalThis.crypto.subtle || !TextEncoder) {
throw new Error("Password-based backup is not available on this platform");
}

const key = await globalThis.crypto.subtle.importKey(
"raw",
new TextEncoder().encode(passphrase),
{ name: "PBKDF2" },
false,
["deriveBits"],
);

const keybits = await globalThis.crypto.subtle.deriveBits(
{
name: "PBKDF2",
salt: new TextEncoder().encode(salt),
iterations: iterations,
hash: "SHA-512",
},
key,
numBits,
);

return new Uint8Array(keybits);
}
66 changes: 10 additions & 56 deletions src/crypto/key_passphrase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,74 +15,28 @@ limitations under the License.
*/

import { randomString } from "../randomstring.ts";
import { deriveRecoveryKeyFromPassphrase } from "../crypto-api/index.ts";

const DEFAULT_ITERATIONS = 500000;

const DEFAULT_BITSIZE = 256;

/* eslint-disable camelcase */
interface IAuthData {
private_key_salt?: string;
private_key_iterations?: number;
private_key_bits?: number;
}
/* eslint-enable camelcase */

interface IKey {
key: Uint8Array;
salt: string;
iterations: number;
}

export function keyFromAuthData(authData: IAuthData, password: string): Promise<Uint8Array> {
if (!authData.private_key_salt || !authData.private_key_iterations) {
throw new Error("Salt and/or iterations not found: " + "this backup cannot be restored with a passphrase");
}

return deriveKey(
password,
authData.private_key_salt,
authData.private_key_iterations,
authData.private_key_bits || DEFAULT_BITSIZE,
);
}

export async function keyFromPassphrase(password: string): Promise<IKey> {
/**
* Generate a new recovery key, based on a passphrase.
* @param passphrase - The passphrase to generate the key from
*/
export async function keyFromPassphrase(passphrase: string): Promise<IKey> {
const salt = randomString(32);

const key = await deriveKey(password, salt, DEFAULT_ITERATIONS, DEFAULT_BITSIZE);
const key = await deriveRecoveryKeyFromPassphrase(passphrase, salt, DEFAULT_ITERATIONS);

return { key, salt, iterations: DEFAULT_ITERATIONS };
}

export async function deriveKey(
password: string,
salt: string,
iterations: number,
numBits = DEFAULT_BITSIZE,
): Promise<Uint8Array> {
if (!globalThis.crypto.subtle || !TextEncoder) {
throw new Error("Password-based backup is not available on this platform");
}

const key = await globalThis.crypto.subtle.importKey(
"raw",
new TextEncoder().encode(password),
{ name: "PBKDF2" },
false,
["deriveBits"],
);

const keybits = await globalThis.crypto.subtle.deriveBits(
{
name: "PBKDF2",
salt: new TextEncoder().encode(salt),
iterations: iterations,
hash: "SHA-512",
},
key,
numBits,
);

return new Uint8Array(keybits);
}
// Re-export the key passphrase functions to avoid breaking changes
export { deriveRecoveryKeyFromPassphrase as deriveKey };
export { keyFromAuthData } from "../common-crypto/key-passphrase.ts";
24 changes: 18 additions & 6 deletions src/rust-crypto/rust-crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,14 +59,14 @@ import {
UserVerificationStatus,
VerificationRequest,
encodeRecoveryKey,
deriveRecoveryKeyFromPassphrase,
} from "../crypto-api/index.ts";
import { deviceKeysToDeviceMap, rustDeviceToJsDevice } from "./device-converter.ts";
import { IDownloadKeyResult, IQueryKeysRequest } from "../client.ts";
import { Device, DeviceMap } from "../models/device.ts";
import { SECRET_STORAGE_ALGORITHM_V1_AES, ServerSideSecretStorage } from "../secret-storage.ts";
import { CrossSigningIdentity } from "./CrossSigningIdentity.ts";
import { secretStorageCanAccessSecrets, secretStorageContainsCrossSigningKeys } from "./secret-storage.ts";
import { keyFromPassphrase } from "../crypto/key_passphrase.ts";
import { isVerificationEvent, RustVerificationRequest, verificationMethodIdentifierToMethod } from "./verification.ts";
import { EventType, MsgType } from "../@types/event.ts";
import { CryptoEvent } from "../crypto/index.ts";
Expand Down Expand Up @@ -100,6 +100,11 @@ interface ISignableObject {
* @internal
*/
export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEventMap> implements CryptoBackend {
/**
* The number of iterations to use when deriving a recovery key from a passphrase.
*/
private readonly RECOVERY_KEY_DERIVATION_ITERATIONS = 500000;

private _trustCrossSignedDevices = true;

/** whether {@link stop} has been called */
Expand Down Expand Up @@ -879,17 +884,24 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
public async createRecoveryKeyFromPassphrase(password?: string): Promise<GeneratedSecretStorageKey> {
if (password) {
// Generate the key from the passphrase
const derivation = await keyFromPassphrase(password);
// first we generate a random salt
const salt = randomString(32);
// then we derive the key from the passphrase
const recoveryKey = await deriveRecoveryKeyFromPassphrase(
password,
salt,
this.RECOVERY_KEY_DERIVATION_ITERATIONS,
);
return {
keyInfo: {
passphrase: {
algorithm: "m.pbkdf2",
iterations: derivation.iterations,
salt: derivation.salt,
iterations: this.RECOVERY_KEY_DERIVATION_ITERATIONS,
salt,
},
},
privateKey: derivation.key,
encodedPrivateKey: encodeRecoveryKey(derivation.key),
privateKey: recoveryKey,
encodedPrivateKey: encodeRecoveryKey(recoveryKey),
};
} else {
// Using the navigator crypto API to generate the private key
Expand Down

0 comments on commit 04029f3

Please sign in to comment.