Skip to content

Commit 424c258

Browse files
authored
Merge pull request #4621 from matrix-org/dbkr/secure_random_string
Change randomString et al to be secure
2 parents b496601 + ea67d39 commit 424c258

14 files changed

+164
-64
lines changed

spec/unit/interactive-auth.spec.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import { logger } from "../../src/logger";
2020
import { InteractiveAuth, AuthType } from "../../src/interactive-auth";
2121
import { HTTPError, MatrixError } from "../../src/http-api";
2222
import { sleep } from "../../src/utils";
23-
import { randomString } from "../../src/randomstring";
23+
import { secureRandomString } from "../../src/randomstring";
2424

2525
// Trivial client object to test interactive auth
2626
// (we do not need TestClient here)
@@ -502,7 +502,7 @@ describe("InteractiveAuth", () => {
502502
const doRequest = jest.fn();
503503
const stateUpdated = jest.fn();
504504
const requestEmailToken = jest.fn();
505-
const sid = randomString(24);
505+
const sid = secureRandomString(24);
506506
requestEmailToken.mockImplementation(() => sleep(500, { sid }));
507507

508508
const ia = new InteractiveAuth({

spec/unit/matrixrtc/MatrixRTCSession.spec.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { KnownMembership } from "../../../src/@types/membership";
1919
import { DEFAULT_EXPIRE_DURATION, SessionMembershipData } from "../../../src/matrixrtc/CallMembership";
2020
import { MatrixRTCSession, MatrixRTCSessionEvent } from "../../../src/matrixrtc/MatrixRTCSession";
2121
import { EncryptionKeysEventContent } from "../../../src/matrixrtc/types";
22-
import { randomString } from "../../../src/randomstring";
22+
import { secureRandomString } from "../../../src/randomstring";
2323
import { flushPromises } from "../../test-utils/flushPromises";
2424
import { makeMockRoom, makeMockRoomState, membershipTemplate } from "./mocks";
2525

@@ -98,7 +98,7 @@ describe("MatrixRTCSession", () => {
9898
});
9999

100100
it("safely ignores events with no memberships section", () => {
101-
const roomId = randomString(8);
101+
const roomId = secureRandomString(8);
102102
const event = {
103103
getType: jest.fn().mockReturnValue(EventType.GroupCallMemberPrefix),
104104
getContent: jest.fn().mockReturnValue({}),
@@ -133,7 +133,7 @@ describe("MatrixRTCSession", () => {
133133
});
134134

135135
it("safely ignores events with junk memberships section", () => {
136-
const roomId = randomString(8);
136+
const roomId = secureRandomString(8);
137137
const event = {
138138
getType: jest.fn().mockReturnValue(EventType.GroupCallMemberPrefix),
139139
getContent: jest.fn().mockReturnValue({ memberships: ["i am a fish"] }),

spec/unit/matrixrtc/mocks.ts

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

1717
import { EventType, MatrixEvent, Room } from "../../../src";
1818
import { SessionMembershipData } from "../../../src/matrixrtc/CallMembership";
19-
import { randomString } from "../../../src/randomstring";
19+
import { secureRandomString } from "../../../src/randomstring";
2020

2121
type MembershipData = SessionMembershipData[] | SessionMembershipData | {};
2222

@@ -41,7 +41,7 @@ export const membershipTemplate: SessionMembershipData = {
4141
};
4242

4343
export function makeMockRoom(membershipData: MembershipData): Room {
44-
const roomId = randomString(8);
44+
const roomId = secureRandomString(8);
4545
// Caching roomState here so it does not get recreated when calling `getLiveTimeline.getState()`
4646
const roomState = makeMockRoomState(membershipData, roomId);
4747
const room = {

spec/unit/randomstring.spec.ts

+66-18
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,18 @@ limitations under the License.
1616

1717
import { decodeBase64 } from "../../src/base64";
1818
import {
19-
randomLowercaseString,
20-
randomString,
21-
randomUppercaseString,
19+
secureRandomString,
2220
secureRandomBase64Url,
21+
secureRandomStringFrom,
22+
LOWERCASE,
23+
UPPERCASE,
2324
} from "../../src/randomstring";
2425

2526
describe("Random strings", () => {
27+
afterEach(() => {
28+
jest.restoreAllMocks();
29+
});
30+
2631
it.each([8, 16, 32])("secureRandomBase64 generates %i valid base64 bytes", (n: number) => {
2732
const randb641 = secureRandomBase64Url(n);
2833
const randb642 = secureRandomBase64Url(n);
@@ -33,34 +38,77 @@ describe("Random strings", () => {
3338
expect(decoded).toHaveLength(n);
3439
});
3540

36-
it.each([8, 16, 32])("randomString generates string of %i characters", (n: number) => {
37-
const rand1 = randomString(n);
38-
const rand2 = randomString(n);
41+
it.each([8, 16, 32])("secureRandomString generates string of %i characters", (n: number) => {
42+
const rand1 = secureRandomString(n);
43+
const rand2 = secureRandomString(n);
3944

4045
expect(rand1).not.toEqual(rand2);
4146

4247
expect(rand1).toHaveLength(n);
4348
});
4449

45-
it.each([8, 16, 32])("randomLowercaseString generates lowercase string of %i characters", (n: number) => {
46-
const rand1 = randomLowercaseString(n);
47-
const rand2 = randomLowercaseString(n);
50+
it.each([8, 16, 32])(
51+
"secureRandomStringFrom generates lowercase string of %i characters when given lowercase",
52+
(n: number) => {
53+
const rand1 = secureRandomStringFrom(n, LOWERCASE);
54+
const rand2 = secureRandomStringFrom(n, LOWERCASE);
4855

49-
expect(rand1).not.toEqual(rand2);
56+
expect(rand1).not.toEqual(rand2);
5057

51-
expect(rand1).toHaveLength(n);
58+
expect(rand1).toHaveLength(n);
59+
60+
expect(rand1.toLowerCase()).toEqual(rand1);
61+
},
62+
);
63+
64+
it.each([8, 16, 32])(
65+
"secureRandomStringFrom generates uppercase string of %i characters when given uppercase",
66+
(n: number) => {
67+
const rand1 = secureRandomStringFrom(n, UPPERCASE);
68+
const rand2 = secureRandomStringFrom(n, UPPERCASE);
69+
70+
expect(rand1).not.toEqual(rand2);
71+
72+
expect(rand1).toHaveLength(n);
5273

53-
expect(rand1.toLowerCase()).toEqual(rand1);
74+
expect(rand1.toUpperCase()).toEqual(rand1);
75+
},
76+
);
77+
78+
it("throws if given character set less than 2 characters", () => {
79+
expect(() => secureRandomStringFrom(8, "a")).toThrow();
5480
});
5581

56-
it.each([8, 16, 32])("randomUppercaseString generates lowercase string of %i characters", (n: number) => {
57-
const rand1 = randomUppercaseString(n);
58-
const rand2 = randomUppercaseString(n);
82+
it("throws if given character set more than 256 characters", () => {
83+
const charSet = Array.from({ length: 257 }, (_, i) => "a").join("");
5984

60-
expect(rand1).not.toEqual(rand2);
85+
expect(() => secureRandomStringFrom(8, charSet)).toThrow();
86+
});
6187

62-
expect(rand1).toHaveLength(n);
88+
it("throws if given length less than 1", () => {
89+
expect(() => secureRandomStringFrom(0, "abc")).toThrow();
90+
});
91+
92+
it("throws if given length more than 32768", () => {
93+
expect(() => secureRandomStringFrom(32769, "abc")).toThrow();
94+
});
6395

64-
expect(rand1.toUpperCase()).toEqual(rand1);
96+
it("asks for more entropy if given entropy is unusable", () => {
97+
// This is testing the internal implementation details of the function rather
98+
// than strictly the public API. The intention is to have some assertion that
99+
// the rejection sampling to make the distribution even over all possible characters
100+
// is doing what it's supposed to do.
101+
102+
// mock once to fill with 255 the first time: 255 should be unusable because
103+
// we give 10 possible characters below and 256 is not evenly divisible by 10, so
104+
// this should force it to call for more entropy.
105+
jest.spyOn(globalThis.crypto, "getRandomValues").mockImplementationOnce((arr) => {
106+
if (arr === null) throw new Error("Buffer is null");
107+
new Uint8Array(arr.buffer).fill(255);
108+
return arr;
109+
});
110+
111+
secureRandomStringFrom(8, "0123456789");
112+
expect(globalThis.crypto.getRandomValues).toHaveBeenCalledTimes(2);
65113
});
66114
});

spec/unit/secret-storage.spec.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import {
2727
ServerSideSecretStorageImpl,
2828
trimTrailingEquals,
2929
} from "../../src/secret-storage";
30-
import { randomString } from "../../src/randomstring";
30+
import { secureRandomString } from "../../src/randomstring";
3131
import { SecretInfo } from "../../src/secret-storage.ts";
3232
import { AccountDataEvents, ClientEvent, MatrixEvent, TypedEventEmitter } from "../../src";
3333
import { defer, IDeferred } from "../../src/utils";
@@ -179,7 +179,7 @@ describe("ServerSideSecretStorageImpl", function () {
179179
it("should return true for a correct key check", async function () {
180180
const secretStorage = new ServerSideSecretStorageImpl({} as AccountDataClient, {});
181181

182-
const myKey = new TextEncoder().encode(randomString(32));
182+
const myKey = new TextEncoder().encode(secureRandomString(32));
183183
const { iv, mac } = await calculateKeyCheck(myKey);
184184

185185
const keyInfo: SecretStorageKeyDescriptionAesV1 = {

spec/unit/thread-utils.spec.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@ limitations under the License.
1515
*/
1616

1717
import { IEvent } from "../../src";
18-
import { randomString } from "../../src/randomstring";
18+
import { secureRandomString } from "../../src/randomstring";
1919
import { getRelationsThreadFilter } from "../../src/thread-utils";
2020

2121
function makeEvent(relatesToEvent: string, relType: string): Partial<IEvent> {
2222
return {
23-
event_id: randomString(10),
23+
event_id: secureRandomString(10),
2424
type: "m.room.message",
2525
content: {
2626
"msgtype": "m.text",

src/client.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ import {
160160
Visibility,
161161
} from "./@types/partials.ts";
162162
import { EventMapper, eventMapperFor, MapperOpts } from "./event-mapper.ts";
163-
import { randomString } from "./randomstring.ts";
163+
import { secureRandomString } from "./randomstring.ts";
164164
import { BackupManager, IKeyBackup, IKeyBackupCheck, IPreparedKeyBackupVersion, TrustInfo } from "./crypto/backup.ts";
165165
import { DEFAULT_TREE_POWER_LEVELS_TEMPLATE, MSC3089TreeSpace } from "./models/MSC3089TreeSpace.ts";
166166
import { ISignatures } from "./@types/signed.ts";
@@ -1346,7 +1346,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
13461346
this.usingExternalCrypto = opts.usingExternalCrypto ?? false;
13471347
this.store = opts.store || new StubStore();
13481348
this.deviceId = opts.deviceId || null;
1349-
this.sessionId = randomString(10);
1349+
this.sessionId = secureRandomString(10);
13501350

13511351
const userId = opts.userId || null;
13521352
this.credentials = { userId };
@@ -7998,7 +7998,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
79987998
* @returns A new client secret
79997999
*/
80008000
public generateClientSecret(): string {
8001-
return randomString(32);
8001+
return secureRandomString(32);
80028002
}
80038003

80048004
/**

src/crypto/key_passphrase.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17-
import { randomString } from "../randomstring.ts";
17+
import { secureRandomString } from "../randomstring.ts";
1818
import { deriveRecoveryKeyFromPassphrase } from "../crypto-api/index.ts";
1919

2020
const DEFAULT_ITERATIONS = 500000;
@@ -30,7 +30,7 @@ interface IKey {
3030
* @param passphrase - The passphrase to generate the key from
3131
*/
3232
export async function keyFromPassphrase(passphrase: string): Promise<IKey> {
33-
const salt = randomString(32);
33+
const salt = secureRandomString(32);
3434

3535
const key = await deriveRecoveryKeyFromPassphrase(passphrase, salt, DEFAULT_ITERATIONS);
3636

src/crypto/verification/request/ToDeviceChannel.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
1515
limitations under the License.
1616
*/
1717

18-
import { randomString } from "../../../randomstring.ts";
18+
import { secureRandomString } from "../../../randomstring.ts";
1919
import { logger } from "../../../logger.ts";
2020
import {
2121
CANCEL_TYPE,
@@ -283,7 +283,7 @@ export class ToDeviceChannel implements IVerificationChannel {
283283
* @returns the transaction id
284284
*/
285285
public static makeTransactionId(): string {
286-
return randomString(32);
286+
return secureRandomString(32);
287287
}
288288
}
289289

src/oidc/authorize.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ limitations under the License.
1717
import { IdTokenClaims, Log, OidcClient, SigninResponse, SigninState, WebStorageStateStore } from "oidc-client-ts";
1818

1919
import { logger } from "../logger.ts";
20-
import { randomString } from "../randomstring.ts";
20+
import { secureRandomString } from "../randomstring.ts";
2121
import { OidcError } from "./error.ts";
2222
import {
2323
BearerTokenResponse,
@@ -52,7 +52,7 @@ export type AuthorizationParams = {
5252
* @returns scope
5353
*/
5454
export const generateScope = (deviceId?: string): string => {
55-
const safeDeviceId = deviceId ?? randomString(10);
55+
const safeDeviceId = deviceId ?? secureRandomString(10);
5656
return `openid urn:matrix:org.matrix.msc2967.client:api:* urn:matrix:org.matrix.msc2967.client:device:${safeDeviceId}`;
5757
};
5858

@@ -79,9 +79,9 @@ const generateCodeChallenge = async (codeVerifier: string): Promise<string> => {
7979
export const generateAuthorizationParams = ({ redirectUri }: { redirectUri: string }): AuthorizationParams => ({
8080
scope: generateScope(),
8181
redirectUri,
82-
state: randomString(8),
83-
nonce: randomString(8),
84-
codeVerifier: randomString(64), // https://tools.ietf.org/html/rfc7636#section-4.1 length needs to be 43-128 characters
82+
state: secureRandomString(8),
83+
nonce: secureRandomString(8),
84+
codeVerifier: secureRandomString(64), // https://tools.ietf.org/html/rfc7636#section-4.1 length needs to be 43-128 characters
8585
});
8686

8787
/**

0 commit comments

Comments
 (0)