Skip to content

Commit

Permalink
Move crypto/recoverykey.ts to crypto-api/recovery-key.ts (#4399)
Browse files Browse the repository at this point in the history
* Move `crypto/recoverykey.ts` to `crypto-api/recovery-key.ts`

* Re-export `crypto-api/recovery-key` into `crypto/recoverykey`

* Add a bit of doc

* Deprecate `MatrixClient.isValidRecoveryKey` and `MatrixClient.keyBackupKeyFromRecoveryKey`

* Import `index.ts` directly

* Update `recovery-key.ts` doc

* Add tests for `decodeRecoveryKey`

* Move `recovery-key.spec.ts` file
  • Loading branch information
florianduros committed Sep 13, 2024
1 parent 7987ce7 commit 37fefa2
Show file tree
Hide file tree
Showing 8 changed files with 134 additions and 50 deletions.
44 changes: 44 additions & 0 deletions spec/unit/crypto-api/recovery-key.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* 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 { decodeRecoveryKey, encodeRecoveryKey } from "../../../src/crypto-api";

describe("recovery key", () => {
describe("decodeRecoveryKey", () => {
it("should thrown an incorrect length error", () => {
const key = [0, 1];
const encodedKey = encodeRecoveryKey(key)!;

expect(() => decodeRecoveryKey(encodedKey)).toThrow("Incorrect length");
});

it("should thrown an incorrect parity", () => {
const key = Array.from({ length: 32 }, (_, i) => i);
let encodedKey = encodeRecoveryKey(key)!;
// Mutate the encoded key to have incorrect parity
encodedKey = encodedKey.replace("EsSz", "EsSZ");

expect(() => decodeRecoveryKey(encodedKey!)).toThrow("Incorrect parity");
});

it("should decode a valid encoded key", () => {
const key = Array.from({ length: 32 }, (_, i) => i);
const encodedKey = encodeRecoveryKey(key)!;

expect(decodeRecoveryKey(encodedKey)).toEqual(new Uint8Array(key));
});
});
});
16 changes: 14 additions & 2 deletions 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 { decodeRecoveryKey } from "./crypto/recoverykey.ts";
import { keyFromAuthData } from "./crypto/key_passphrase.ts";
import { User, UserEvent, UserEventHandlerMap } from "./models/user.ts";
import { getHttpUriForMxc } from "./content-repo.ts";
Expand Down Expand Up @@ -223,7 +222,13 @@ import { LocalNotificationSettings } from "./@types/local_notifications.ts";
import { buildFeatureSupportMap, Feature, ServerSupport } from "./feature.ts";
import { BackupDecryptor, CryptoBackend } from "./common-crypto/CryptoBackend.ts";
import { RUST_SDK_STORE_PREFIX } from "./rust-crypto/constants.ts";
import { BootstrapCrossSigningOpts, CrossSigningKeyInfo, CryptoApi, ImportRoomKeysOpts } from "./crypto-api/index.ts";
import {
BootstrapCrossSigningOpts,
CrossSigningKeyInfo,
CryptoApi,
decodeRecoveryKey,
ImportRoomKeysOpts,
} from "./crypto-api/index.ts";
import { DeviceInfoMap } from "./crypto/DeviceList.ts";
import {
AddSecretStorageKeyOpts,
Expand Down Expand Up @@ -3627,6 +3632,12 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
return this.crypto.backupManager.flagAllGroupSessionsForBackup();
}

/**
* Return true if recovery key is valid.
* Try to decode the recovery key and check if it's successful.
* @param recoveryKey
* @deprecated Use {@link decodeRecoveryKey} directly
*/
public isValidRecoveryKey(recoveryKey: string): boolean {
try {
decodeRecoveryKey(recoveryKey);
Expand Down Expand Up @@ -3658,6 +3669,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
*
* @param recoveryKey - The recovery key
* @returns key backup key
* @deprecated Use {@link decodeRecoveryKey} directly
*/
public keyBackupKeyFromRecoveryKey(recoveryKey: string): Uint8Array {
return decodeRecoveryKey(recoveryKey);
Expand Down
1 change: 1 addition & 0 deletions src/crypto-api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -967,3 +967,4 @@ export interface OwnDeviceKeys {

export * from "./verification.ts";
export * from "./keybackup.ts";
export * from "./recovery-key.ts";
69 changes: 69 additions & 0 deletions src/crypto-api/recovery-key.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* 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 bs58 from "bs58";

// picked arbitrarily but to try & avoid clashing with any bitcoin ones
// (which are also base58 encoded, but bitcoin's involve a lot more hashing)
const OLM_RECOVERY_KEY_PREFIX = [0x8b, 0x01];
const KEY_SIZE = 32;

/**
* Encode a recovery key using the Matrix {@link https://spec.matrix.org/v1.11/appendices/#cryptographic-key-representation | Cryptographic key representation}
* @param key
*/
export function encodeRecoveryKey(key: ArrayLike<number>): string | undefined {
const buf = Buffer.alloc(OLM_RECOVERY_KEY_PREFIX.length + key.length + 1);
buf.set(OLM_RECOVERY_KEY_PREFIX, 0);
buf.set(key, OLM_RECOVERY_KEY_PREFIX.length);

let parity = 0;
for (let i = 0; i < buf.length - 1; ++i) {
parity ^= buf[i];
}
buf[buf.length - 1] = parity;
const base58key = bs58.encode(buf);

return base58key.match(/.{1,4}/g)?.join(" ");
}

/**
* Decode a recovery key encoded with the Matrix {@link https://spec.matrix.org/v1.11/appendices/#cryptographic-key-representation | Cryptographic key representation} encoding.
* @param recoveryKey
*/
export function decodeRecoveryKey(recoveryKey: string): Uint8Array {
const result = bs58.decode(recoveryKey.replace(/ /g, ""));

let parity = 0;
for (const b of result) {
parity ^= b;
}
if (parity !== 0) {
throw new Error("Incorrect parity");
}

for (let i = 0; i < OLM_RECOVERY_KEY_PREFIX.length; ++i) {
if (result[i] !== OLM_RECOVERY_KEY_PREFIX[i]) {
throw new Error("Incorrect prefix");
}
}

if (result.length !== OLM_RECOVERY_KEY_PREFIX.length + KEY_SIZE + 1) {
throw new Error("Incorrect length");
}

return Uint8Array.from(result.slice(OLM_RECOVERY_KEY_PREFIX.length, OLM_RECOVERY_KEY_PREFIX.length + KEY_SIZE));
}
2 changes: 1 addition & 1 deletion src/crypto/backup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ import { DeviceTrustLevel } from "./CrossSigning.ts";
import { keyFromPassphrase } from "./key_passphrase.ts";
import { encodeUri, safeSet, sleep } from "../utils.ts";
import { IndexedDBCryptoStore } from "./store/indexeddb-crypto-store.ts";
import { encodeRecoveryKey } from "./recoverykey.ts";
import { calculateKeyCheck, decryptAES, encryptAES, IEncryptedPayload } from "./aes.ts";
import {
Curve25519SessionData,
Expand All @@ -41,6 +40,7 @@ import { CryptoEvent } from "./index.ts";
import { ClientPrefix, HTTPError, MatrixError, Method } from "../http-api/index.ts";
import { BackupTrustInfo } from "../crypto-api/keybackup.ts";
import { BackupDecryptor } from "../common-crypto/CryptoBackend.ts";
import { encodeRecoveryKey } from "../crypto-api/index.ts";

const KEY_BACKUP_KEYS_PER_REQUEST = 200;
const KEY_BACKUP_CHECK_RATE_LIMIT = 5000; // ms
Expand Down
3 changes: 2 additions & 1 deletion src/crypto/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ import { VerificationBase } from "./verification/Base.ts";
import { ReciprocateQRCode, SCAN_QR_CODE_METHOD, SHOW_QR_CODE_METHOD } from "./verification/QRCode.ts";
import { SAS as SASVerification } from "./verification/SAS.ts";
import { keyFromPassphrase } from "./key_passphrase.ts";
import { decodeRecoveryKey, encodeRecoveryKey } from "./recoverykey.ts";
import { VerificationRequest } from "./verification/request/VerificationRequest.ts";
import { InRoomChannel, InRoomRequests } from "./verification/request/InRoomChannel.ts";
import { Request, ToDeviceChannel, ToDeviceRequests } from "./verification/request/ToDeviceChannel.ts";
Expand Down Expand Up @@ -89,8 +88,10 @@ import {
BootstrapCrossSigningOpts,
CrossSigningKeyInfo,
CrossSigningStatus,
decodeRecoveryKey,
DecryptionFailureCode,
DeviceVerificationStatus,
encodeRecoveryKey,
EventEncryptionInfo,
EventShieldColour,
EventShieldReason,
Expand Down
47 changes: 2 additions & 45 deletions src/crypto/recoverykey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,48 +14,5 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

import bs58 from "bs58";

// picked arbitrarily but to try & avoid clashing with any bitcoin ones
// (which are also base58 encoded, but bitcoin's involve a lot more hashing)
const OLM_RECOVERY_KEY_PREFIX = [0x8b, 0x01];
const KEY_SIZE = 32;

export function encodeRecoveryKey(key: ArrayLike<number>): string | undefined {
const buf = Buffer.alloc(OLM_RECOVERY_KEY_PREFIX.length + key.length + 1);
buf.set(OLM_RECOVERY_KEY_PREFIX, 0);
buf.set(key, OLM_RECOVERY_KEY_PREFIX.length);

let parity = 0;
for (let i = 0; i < buf.length - 1; ++i) {
parity ^= buf[i];
}
buf[buf.length - 1] = parity;
const base58key = bs58.encode(buf);

return base58key.match(/.{1,4}/g)?.join(" ");
}

export function decodeRecoveryKey(recoveryKey: string): Uint8Array {
const result = bs58.decode(recoveryKey.replace(/ /g, ""));

let parity = 0;
for (const b of result) {
parity ^= b;
}
if (parity !== 0) {
throw new Error("Incorrect parity");
}

for (let i = 0; i < OLM_RECOVERY_KEY_PREFIX.length; ++i) {
if (result[i] !== OLM_RECOVERY_KEY_PREFIX[i]) {
throw new Error("Incorrect prefix");
}
}

if (result.length !== OLM_RECOVERY_KEY_PREFIX.length + KEY_SIZE + 1) {
throw new Error("Incorrect length");
}

return Uint8Array.from(result.slice(OLM_RECOVERY_KEY_PREFIX.length, OLM_RECOVERY_KEY_PREFIX.length + KEY_SIZE));
}
// Re-export to avoid breaking changes
export * from "../crypto-api/recovery-key.ts";
2 changes: 1 addition & 1 deletion src/rust-crypto/rust-crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import {
OwnDeviceKeys,
UserVerificationStatus,
VerificationRequest,
encodeRecoveryKey,
} from "../crypto-api/index.ts";
import { deviceKeysToDeviceMap, rustDeviceToJsDevice } from "./device-converter.ts";
import { IDownloadKeyResult, IQueryKeysRequest } from "../client.ts";
Expand All @@ -66,7 +67,6 @@ import { SECRET_STORAGE_ALGORITHM_V1_AES, ServerSideSecretStorage } from "../sec
import { CrossSigningIdentity } from "./CrossSigningIdentity.ts";
import { secretStorageCanAccessSecrets, secretStorageContainsCrossSigningKeys } from "./secret-storage.ts";
import { keyFromPassphrase } from "../crypto/key_passphrase.ts";
import { encodeRecoveryKey } from "../crypto/recoverykey.ts";
import { isVerificationEvent, RustVerificationRequest, verificationMethodIdentifierToMethod } from "./verification.ts";
import { EventType, MsgType } from "../@types/event.ts";
import { CryptoEvent } from "../crypto/index.ts";
Expand Down

0 comments on commit 37fefa2

Please sign in to comment.