You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-12-17 21:42:17 +03:00
Merge pull request #4621 from matrix-org/dbkr/secure_random_string
Change randomString et al to be secure
This commit is contained in:
@@ -20,7 +20,7 @@ import { logger } from "../../src/logger";
|
||||
import { InteractiveAuth, AuthType } from "../../src/interactive-auth";
|
||||
import { HTTPError, MatrixError } from "../../src/http-api";
|
||||
import { sleep } from "../../src/utils";
|
||||
import { randomString } from "../../src/randomstring";
|
||||
import { secureRandomString } from "../../src/randomstring";
|
||||
|
||||
// Trivial client object to test interactive auth
|
||||
// (we do not need TestClient here)
|
||||
@@ -502,7 +502,7 @@ describe("InteractiveAuth", () => {
|
||||
const doRequest = jest.fn();
|
||||
const stateUpdated = jest.fn();
|
||||
const requestEmailToken = jest.fn();
|
||||
const sid = randomString(24);
|
||||
const sid = secureRandomString(24);
|
||||
requestEmailToken.mockImplementation(() => sleep(500, { sid }));
|
||||
|
||||
const ia = new InteractiveAuth({
|
||||
|
||||
@@ -19,7 +19,7 @@ import { KnownMembership } from "../../../src/@types/membership";
|
||||
import { DEFAULT_EXPIRE_DURATION, SessionMembershipData } from "../../../src/matrixrtc/CallMembership";
|
||||
import { MatrixRTCSession, MatrixRTCSessionEvent } from "../../../src/matrixrtc/MatrixRTCSession";
|
||||
import { EncryptionKeysEventContent } from "../../../src/matrixrtc/types";
|
||||
import { randomString } from "../../../src/randomstring";
|
||||
import { secureRandomString } from "../../../src/randomstring";
|
||||
import { flushPromises } from "../../test-utils/flushPromises";
|
||||
import { makeMockRoom, makeMockRoomState, membershipTemplate } from "./mocks";
|
||||
|
||||
@@ -98,7 +98,7 @@ describe("MatrixRTCSession", () => {
|
||||
});
|
||||
|
||||
it("safely ignores events with no memberships section", () => {
|
||||
const roomId = randomString(8);
|
||||
const roomId = secureRandomString(8);
|
||||
const event = {
|
||||
getType: jest.fn().mockReturnValue(EventType.GroupCallMemberPrefix),
|
||||
getContent: jest.fn().mockReturnValue({}),
|
||||
@@ -133,7 +133,7 @@ describe("MatrixRTCSession", () => {
|
||||
});
|
||||
|
||||
it("safely ignores events with junk memberships section", () => {
|
||||
const roomId = randomString(8);
|
||||
const roomId = secureRandomString(8);
|
||||
const event = {
|
||||
getType: jest.fn().mockReturnValue(EventType.GroupCallMemberPrefix),
|
||||
getContent: jest.fn().mockReturnValue({ memberships: ["i am a fish"] }),
|
||||
|
||||
@@ -16,7 +16,7 @@ limitations under the License.
|
||||
|
||||
import { EventType, MatrixEvent, Room } from "../../../src";
|
||||
import { SessionMembershipData } from "../../../src/matrixrtc/CallMembership";
|
||||
import { randomString } from "../../../src/randomstring";
|
||||
import { secureRandomString } from "../../../src/randomstring";
|
||||
|
||||
type MembershipData = SessionMembershipData[] | SessionMembershipData | {};
|
||||
|
||||
@@ -41,7 +41,7 @@ export const membershipTemplate: SessionMembershipData = {
|
||||
};
|
||||
|
||||
export function makeMockRoom(membershipData: MembershipData): Room {
|
||||
const roomId = randomString(8);
|
||||
const roomId = secureRandomString(8);
|
||||
// Caching roomState here so it does not get recreated when calling `getLiveTimeline.getState()`
|
||||
const roomState = makeMockRoomState(membershipData, roomId);
|
||||
const room = {
|
||||
|
||||
@@ -16,13 +16,18 @@ limitations under the License.
|
||||
|
||||
import { decodeBase64 } from "../../src/base64";
|
||||
import {
|
||||
randomLowercaseString,
|
||||
randomString,
|
||||
randomUppercaseString,
|
||||
secureRandomString,
|
||||
secureRandomBase64Url,
|
||||
secureRandomStringFrom,
|
||||
LOWERCASE,
|
||||
UPPERCASE,
|
||||
} from "../../src/randomstring";
|
||||
|
||||
describe("Random strings", () => {
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it.each([8, 16, 32])("secureRandomBase64 generates %i valid base64 bytes", (n: number) => {
|
||||
const randb641 = secureRandomBase64Url(n);
|
||||
const randb642 = secureRandomBase64Url(n);
|
||||
@@ -33,34 +38,77 @@ describe("Random strings", () => {
|
||||
expect(decoded).toHaveLength(n);
|
||||
});
|
||||
|
||||
it.each([8, 16, 32])("randomString generates string of %i characters", (n: number) => {
|
||||
const rand1 = randomString(n);
|
||||
const rand2 = randomString(n);
|
||||
it.each([8, 16, 32])("secureRandomString generates string of %i characters", (n: number) => {
|
||||
const rand1 = secureRandomString(n);
|
||||
const rand2 = secureRandomString(n);
|
||||
|
||||
expect(rand1).not.toEqual(rand2);
|
||||
|
||||
expect(rand1).toHaveLength(n);
|
||||
});
|
||||
|
||||
it.each([8, 16, 32])("randomLowercaseString generates lowercase string of %i characters", (n: number) => {
|
||||
const rand1 = randomLowercaseString(n);
|
||||
const rand2 = randomLowercaseString(n);
|
||||
it.each([8, 16, 32])(
|
||||
"secureRandomStringFrom generates lowercase string of %i characters when given lowercase",
|
||||
(n: number) => {
|
||||
const rand1 = secureRandomStringFrom(n, LOWERCASE);
|
||||
const rand2 = secureRandomStringFrom(n, LOWERCASE);
|
||||
|
||||
expect(rand1).not.toEqual(rand2);
|
||||
|
||||
expect(rand1).toHaveLength(n);
|
||||
|
||||
expect(rand1.toLowerCase()).toEqual(rand1);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it.each([8, 16, 32])("randomUppercaseString generates lowercase string of %i characters", (n: number) => {
|
||||
const rand1 = randomUppercaseString(n);
|
||||
const rand2 = randomUppercaseString(n);
|
||||
it.each([8, 16, 32])(
|
||||
"secureRandomStringFrom generates uppercase string of %i characters when given uppercase",
|
||||
(n: number) => {
|
||||
const rand1 = secureRandomStringFrom(n, UPPERCASE);
|
||||
const rand2 = secureRandomStringFrom(n, UPPERCASE);
|
||||
|
||||
expect(rand1).not.toEqual(rand2);
|
||||
|
||||
expect(rand1).toHaveLength(n);
|
||||
|
||||
expect(rand1.toUpperCase()).toEqual(rand1);
|
||||
},
|
||||
);
|
||||
|
||||
it("throws if given character set less than 2 characters", () => {
|
||||
expect(() => secureRandomStringFrom(8, "a")).toThrow();
|
||||
});
|
||||
|
||||
it("throws if given character set more than 256 characters", () => {
|
||||
const charSet = Array.from({ length: 257 }, (_, i) => "a").join("");
|
||||
|
||||
expect(() => secureRandomStringFrom(8, charSet)).toThrow();
|
||||
});
|
||||
|
||||
it("throws if given length less than 1", () => {
|
||||
expect(() => secureRandomStringFrom(0, "abc")).toThrow();
|
||||
});
|
||||
|
||||
it("throws if given length more than 32768", () => {
|
||||
expect(() => secureRandomStringFrom(32769, "abc")).toThrow();
|
||||
});
|
||||
|
||||
it("asks for more entropy if given entropy is unusable", () => {
|
||||
// This is testing the internal implementation details of the function rather
|
||||
// than strictly the public API. The intention is to have some assertion that
|
||||
// the rejection sampling to make the distribution even over all possible characters
|
||||
// is doing what it's supposed to do.
|
||||
|
||||
// mock once to fill with 255 the first time: 255 should be unusable because
|
||||
// we give 10 possible characters below and 256 is not evenly divisible by 10, so
|
||||
// this should force it to call for more entropy.
|
||||
jest.spyOn(globalThis.crypto, "getRandomValues").mockImplementationOnce((arr) => {
|
||||
if (arr === null) throw new Error("Buffer is null");
|
||||
new Uint8Array(arr.buffer).fill(255);
|
||||
return arr;
|
||||
});
|
||||
|
||||
secureRandomStringFrom(8, "0123456789");
|
||||
expect(globalThis.crypto.getRandomValues).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
ServerSideSecretStorageImpl,
|
||||
trimTrailingEquals,
|
||||
} from "../../src/secret-storage";
|
||||
import { randomString } from "../../src/randomstring";
|
||||
import { secureRandomString } from "../../src/randomstring";
|
||||
import { SecretInfo } from "../../src/secret-storage.ts";
|
||||
import { AccountDataEvents, ClientEvent, MatrixEvent, TypedEventEmitter } from "../../src";
|
||||
import { defer, IDeferred } from "../../src/utils";
|
||||
@@ -179,7 +179,7 @@ describe("ServerSideSecretStorageImpl", function () {
|
||||
it("should return true for a correct key check", async function () {
|
||||
const secretStorage = new ServerSideSecretStorageImpl({} as AccountDataClient, {});
|
||||
|
||||
const myKey = new TextEncoder().encode(randomString(32));
|
||||
const myKey = new TextEncoder().encode(secureRandomString(32));
|
||||
const { iv, mac } = await calculateKeyCheck(myKey);
|
||||
|
||||
const keyInfo: SecretStorageKeyDescriptionAesV1 = {
|
||||
|
||||
@@ -15,12 +15,12 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { IEvent } from "../../src";
|
||||
import { randomString } from "../../src/randomstring";
|
||||
import { secureRandomString } from "../../src/randomstring";
|
||||
import { getRelationsThreadFilter } from "../../src/thread-utils";
|
||||
|
||||
function makeEvent(relatesToEvent: string, relType: string): Partial<IEvent> {
|
||||
return {
|
||||
event_id: randomString(10),
|
||||
event_id: secureRandomString(10),
|
||||
type: "m.room.message",
|
||||
content: {
|
||||
"msgtype": "m.text",
|
||||
|
||||
@@ -160,7 +160,7 @@ import {
|
||||
Visibility,
|
||||
} from "./@types/partials.ts";
|
||||
import { EventMapper, eventMapperFor, MapperOpts } from "./event-mapper.ts";
|
||||
import { randomString } from "./randomstring.ts";
|
||||
import { secureRandomString } from "./randomstring.ts";
|
||||
import { BackupManager, IKeyBackup, IKeyBackupCheck, IPreparedKeyBackupVersion, TrustInfo } from "./crypto/backup.ts";
|
||||
import { DEFAULT_TREE_POWER_LEVELS_TEMPLATE, MSC3089TreeSpace } from "./models/MSC3089TreeSpace.ts";
|
||||
import { ISignatures } from "./@types/signed.ts";
|
||||
@@ -1346,7 +1346,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
this.usingExternalCrypto = opts.usingExternalCrypto ?? false;
|
||||
this.store = opts.store || new StubStore();
|
||||
this.deviceId = opts.deviceId || null;
|
||||
this.sessionId = randomString(10);
|
||||
this.sessionId = secureRandomString(10);
|
||||
|
||||
const userId = opts.userId || null;
|
||||
this.credentials = { userId };
|
||||
@@ -7998,7 +7998,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @returns A new client secret
|
||||
*/
|
||||
public generateClientSecret(): string {
|
||||
return randomString(32);
|
||||
return secureRandomString(32);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { randomString } from "../randomstring.ts";
|
||||
import { secureRandomString } from "../randomstring.ts";
|
||||
import { deriveRecoveryKeyFromPassphrase } from "../crypto-api/index.ts";
|
||||
|
||||
const DEFAULT_ITERATIONS = 500000;
|
||||
@@ -30,7 +30,7 @@ interface IKey {
|
||||
* @param passphrase - The passphrase to generate the key from
|
||||
*/
|
||||
export async function keyFromPassphrase(passphrase: string): Promise<IKey> {
|
||||
const salt = randomString(32);
|
||||
const salt = secureRandomString(32);
|
||||
|
||||
const key = await deriveRecoveryKeyFromPassphrase(passphrase, salt, DEFAULT_ITERATIONS);
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { randomString } from "../../../randomstring.ts";
|
||||
import { secureRandomString } from "../../../randomstring.ts";
|
||||
import { logger } from "../../../logger.ts";
|
||||
import {
|
||||
CANCEL_TYPE,
|
||||
@@ -283,7 +283,7 @@ export class ToDeviceChannel implements IVerificationChannel {
|
||||
* @returns the transaction id
|
||||
*/
|
||||
public static makeTransactionId(): string {
|
||||
return randomString(32);
|
||||
return secureRandomString(32);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ limitations under the License.
|
||||
import { IdTokenClaims, Log, OidcClient, SigninResponse, SigninState, WebStorageStateStore } from "oidc-client-ts";
|
||||
|
||||
import { logger } from "../logger.ts";
|
||||
import { randomString } from "../randomstring.ts";
|
||||
import { secureRandomString } from "../randomstring.ts";
|
||||
import { OidcError } from "./error.ts";
|
||||
import {
|
||||
BearerTokenResponse,
|
||||
@@ -52,7 +52,7 @@ export type AuthorizationParams = {
|
||||
* @returns scope
|
||||
*/
|
||||
export const generateScope = (deviceId?: string): string => {
|
||||
const safeDeviceId = deviceId ?? randomString(10);
|
||||
const safeDeviceId = deviceId ?? secureRandomString(10);
|
||||
return `openid urn:matrix:org.matrix.msc2967.client:api:* urn:matrix:org.matrix.msc2967.client:device:${safeDeviceId}`;
|
||||
};
|
||||
|
||||
@@ -79,9 +79,9 @@ const generateCodeChallenge = async (codeVerifier: string): Promise<string> => {
|
||||
export const generateAuthorizationParams = ({ redirectUri }: { redirectUri: string }): AuthorizationParams => ({
|
||||
scope: generateScope(),
|
||||
redirectUri,
|
||||
state: randomString(8),
|
||||
nonce: randomString(8),
|
||||
codeVerifier: randomString(64), // https://tools.ietf.org/html/rfc7636#section-4.1 length needs to be 43-128 characters
|
||||
state: secureRandomString(8),
|
||||
nonce: secureRandomString(8),
|
||||
codeVerifier: secureRandomString(64), // https://tools.ietf.org/html/rfc7636#section-4.1 length needs to be 43-128 characters
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -17,9 +17,23 @@ limitations under the License.
|
||||
|
||||
import { encodeUnpaddedBase64Url } from "./base64.ts";
|
||||
|
||||
const LOWERCASE = "abcdefghijklmnopqrstuvwxyz";
|
||||
const UPPERCASE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
const DIGITS = "0123456789";
|
||||
/**
|
||||
* String representing the lowercase latin alphabet for use in {@link secureRandomStringFrom}
|
||||
* (can be combined with other such exports or other characters by appending strings)
|
||||
*/
|
||||
export const LOWERCASE = "abcdefghijklmnopqrstuvwxyz";
|
||||
|
||||
/**
|
||||
* String representing the uppercase latin alphabet for use in secureRandomStringFrom
|
||||
* (can be combined with other such exports or other characters by appending strings)
|
||||
*/
|
||||
export const UPPERCASE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
|
||||
/**
|
||||
* String representing the arabic numerals for use in secureRandomStringFrom
|
||||
* (can be combined with other such exports or other characters by appending strings)
|
||||
*/
|
||||
export const DIGITS = "0123456789";
|
||||
|
||||
export function secureRandomBase64Url(len: number): string {
|
||||
const key = new Uint8Array(len);
|
||||
@@ -28,24 +42,62 @@ export function secureRandomBase64Url(len: number): string {
|
||||
return encodeUnpaddedBase64Url(key);
|
||||
}
|
||||
|
||||
export function randomString(len: number): string {
|
||||
return randomStringFrom(len, UPPERCASE + LOWERCASE + DIGITS);
|
||||
/**
|
||||
* Generates a random string of uppercase and lowercase letters plus digits using a
|
||||
* cryptographically secure random number generator.
|
||||
* @param len The length of the string to generate
|
||||
* @returns Random string of uppercase and lowercase letters plus digits of length `len`
|
||||
*/
|
||||
export function secureRandomString(len: number): string {
|
||||
return secureRandomStringFrom(len, UPPERCASE + LOWERCASE + DIGITS);
|
||||
}
|
||||
|
||||
export function randomLowercaseString(len: number): string {
|
||||
return randomStringFrom(len, LOWERCASE);
|
||||
}
|
||||
|
||||
export function randomUppercaseString(len: number): string {
|
||||
return randomStringFrom(len, UPPERCASE);
|
||||
}
|
||||
|
||||
function randomStringFrom(len: number, chars: string): string {
|
||||
let ret = "";
|
||||
|
||||
for (let i = 0; i < len; ++i) {
|
||||
ret += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
/**
|
||||
* Generate a cryptographically secure random string using characters given.
|
||||
*
|
||||
* @param len - The length of the string to generate (must be positive and less than 32768).
|
||||
* @param chars - The characters to use in the random string (between 2 and 256 characters long).
|
||||
* @returns Random string of characters of length `len`.
|
||||
*/
|
||||
export function secureRandomStringFrom(len: number, chars: string): string {
|
||||
// This is intended for latin strings so 256 possibilities should be more than enough and
|
||||
// means we can use random bytes, minimising the amount of entropy we need to ask for.
|
||||
if (chars.length < 2 || chars.length > 256) {
|
||||
throw new Error("Character set must be between 2 and 256 characters long");
|
||||
}
|
||||
|
||||
return ret;
|
||||
if (len < 1 || len > 32768) {
|
||||
throw new Error("Requested random string length must be between 1 and 32768");
|
||||
}
|
||||
|
||||
// We'll generate random unsigned bytes, so get the largest number less than 256 that is a multiple
|
||||
// of the length of the character set: We'll need to discard any random values that are larger than
|
||||
// this as we can't possibly map them onto the character set while keeping each character equally
|
||||
// likely to be chosen (minus 1 to convert to indices in a string). (Essentially, we're using a d8
|
||||
// to choose between 7 possibilities and re-rolling on an 8, keeping all 7 outcomes equally likely.)
|
||||
// Our random values must be strictly less than this
|
||||
const randomValueCutoff = 256 - (256 % chars.length);
|
||||
|
||||
// Grab 30% more entropy than we need. This should be enough that we can discard the values that are
|
||||
// too high without having to go back and grab more unless we're super unlucky.
|
||||
const entropyBuffer = new Uint8Array(Math.floor(len * 1.3));
|
||||
// Mark all of this buffer as used to start with (we haven't populated it with entropy yet) so it will
|
||||
// be filled on the first iteration.
|
||||
let entropyBufferPos = entropyBuffer.length;
|
||||
|
||||
const result = [];
|
||||
while (result.length < len) {
|
||||
if (entropyBufferPos === entropyBuffer.length) {
|
||||
globalThis.crypto.getRandomValues(entropyBuffer);
|
||||
entropyBufferPos = 0;
|
||||
}
|
||||
|
||||
const randomByte = entropyBuffer[entropyBufferPos++];
|
||||
|
||||
if (randomByte < randomValueCutoff) {
|
||||
result.push(chars[randomByte % chars.length]);
|
||||
}
|
||||
}
|
||||
|
||||
return result.join("");
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ import { EventType, MsgType } from "../@types/event.ts";
|
||||
import { TypedEventEmitter } from "../models/typed-event-emitter.ts";
|
||||
import { decryptionKeyMatchesKeyBackupInfo, RustBackupManager } from "./backup.ts";
|
||||
import { TypedReEmitter } from "../ReEmitter.ts";
|
||||
import { randomString } from "../randomstring.ts";
|
||||
import { secureRandomString } from "../randomstring.ts";
|
||||
import { ClientStoppedError } from "../errors.ts";
|
||||
import { ISignatures } from "../@types/signed.ts";
|
||||
import { decodeBase64, encodeBase64 } from "../base64.ts";
|
||||
@@ -956,7 +956,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
|
||||
if (password) {
|
||||
// Generate the key from the passphrase
|
||||
// first we generate a random salt
|
||||
const salt = randomString(32);
|
||||
const salt = secureRandomString(32);
|
||||
// then we derive the key from the passphrase
|
||||
const recoveryKey = await deriveRecoveryKeyFromPassphrase(
|
||||
password,
|
||||
@@ -1099,7 +1099,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
|
||||
* @returns the event id
|
||||
*/
|
||||
private async sendVerificationRequestContent(roomId: string, verificationEventContent: string): Promise<string> {
|
||||
const txId = randomString(32);
|
||||
const txId = secureRandomString(32);
|
||||
// Send the verification request content to the DM room
|
||||
const { event_id: eventId } = await this.http.authedRequest<{ event_id: string }>(
|
||||
Method.Put,
|
||||
|
||||
@@ -23,7 +23,7 @@ limitations under the License.
|
||||
import { TypedEventEmitter } from "./models/typed-event-emitter.ts";
|
||||
import { ClientEvent, ClientEventHandlerMap } from "./client.ts";
|
||||
import { MatrixEvent } from "./models/event.ts";
|
||||
import { randomString } from "./randomstring.ts";
|
||||
import { secureRandomString } from "./randomstring.ts";
|
||||
import { logger } from "./logger.ts";
|
||||
import encryptAESSecretStorageItem from "./utils/encryptAESSecretStorageItem.ts";
|
||||
import decryptAESSecretStorageItem from "./utils/decryptAESSecretStorageItem.ts";
|
||||
@@ -435,7 +435,7 @@ export class ServerSideSecretStorageImpl implements ServerSideSecretStorage {
|
||||
// Create a unique key id. XXX: this is racey.
|
||||
if (!keyId) {
|
||||
do {
|
||||
keyId = randomString(32);
|
||||
keyId = secureRandomString(32);
|
||||
} while (await this.accountDataAdapter.getAccountDataFromServer(`m.secret_storage.key.${keyId}`));
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ import { checkObjectHasKeys, isNullOrUndefined, recursivelyAssign } from "../uti
|
||||
import { MatrixEvent } from "../models/event.ts";
|
||||
import { EventType, TimelineEvents, ToDeviceMessageId } from "../@types/event.ts";
|
||||
import { RoomMember } from "../models/room-member.ts";
|
||||
import { randomString } from "../randomstring.ts";
|
||||
import { secureRandomString } from "../randomstring.ts";
|
||||
import {
|
||||
MCallReplacesEvent,
|
||||
MCallAnswer,
|
||||
@@ -277,7 +277,7 @@ export class CallError extends Error {
|
||||
}
|
||||
|
||||
export function genCallID(): string {
|
||||
return Date.now().toString() + randomString(16);
|
||||
return Date.now().toString() + secureRandomString(16);
|
||||
}
|
||||
|
||||
function getCodecParamMods(isPtt: boolean): CodecParamsMod[] {
|
||||
|
||||
Reference in New Issue
Block a user