diff --git a/spec/unit/crypto-api/recovery-key.spec.ts b/spec/unit/crypto-api/recovery-key.spec.ts new file mode 100644 index 00000000000..c488b7d06fe --- /dev/null +++ b/spec/unit/crypto-api/recovery-key.spec.ts @@ -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)); + }); + }); +}); diff --git a/src/client.ts b/src/client.ts index ca506caa042..6746e505bcd 100644 --- a/src/client.ts +++ b/src/client.ts @@ -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"; @@ -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, @@ -3627,6 +3632,12 @@ export class MatrixClient extends TypedEventEmitter): 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)); +} diff --git a/src/crypto/backup.ts b/src/crypto/backup.ts index 3ac6a3ddc0b..2dc374f9868 100644 --- a/src/crypto/backup.ts +++ b/src/crypto/backup.ts @@ -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, @@ -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 diff --git a/src/crypto/index.ts b/src/crypto/index.ts index 420160c0de4..d72e41a6e3d 100644 --- a/src/crypto/index.ts +++ b/src/crypto/index.ts @@ -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"; @@ -89,8 +88,10 @@ import { BootstrapCrossSigningOpts, CrossSigningKeyInfo, CrossSigningStatus, + decodeRecoveryKey, DecryptionFailureCode, DeviceVerificationStatus, + encodeRecoveryKey, EventEncryptionInfo, EventShieldColour, EventShieldReason, diff --git a/src/crypto/recoverykey.ts b/src/crypto/recoverykey.ts index 3d7c12d4acf..6ce5d3f07e6 100644 --- a/src/crypto/recoverykey.ts +++ b/src/crypto/recoverykey.ts @@ -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): 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"; diff --git a/src/rust-crypto/rust-crypto.ts b/src/rust-crypto/rust-crypto.ts index c1d1c8424d5..45d63dd820a 100644 --- a/src/rust-crypto/rust-crypto.ts +++ b/src/rust-crypto/rust-crypto.ts @@ -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"; @@ -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";