Skip to content

Commit beb3721

Browse files
authored
Avoid use of Buffer as it does not exist in the Web natively (#4569)
1 parent 1cad6f4 commit beb3721

File tree

15 files changed

+112
-113
lines changed

15 files changed

+112
-113
lines changed

spec/integ/crypto/verification.spec.ts

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -473,21 +473,23 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
473473
expect(request.phase).toEqual(VerificationPhase.Ready);
474474

475475
// we should now have QR data we can display
476-
const qrCodeBuffer = (await request.generateQRCode())!;
477-
expect(qrCodeBuffer).toBeTruthy();
476+
const rawQrCodeBuffer = (await request.generateQRCode())!;
477+
expect(rawQrCodeBuffer).toBeTruthy();
478+
const qrCodeBuffer = new Uint8Array(rawQrCodeBuffer);
478479

480+
const textDecoder = new TextDecoder();
479481
// https://spec.matrix.org/v1.7/client-server-api/#qr-code-format
480-
expect(qrCodeBuffer.subarray(0, 6).toString("latin1")).toEqual("MATRIX");
481-
expect(qrCodeBuffer.readUint8(6)).toEqual(0x02); // version
482-
expect(qrCodeBuffer.readUint8(7)).toEqual(0x02); // mode
483-
const txnIdLen = qrCodeBuffer.readUint16BE(8);
484-
expect(qrCodeBuffer.subarray(10, 10 + txnIdLen).toString("utf-8")).toEqual(transactionId);
482+
expect(textDecoder.decode(qrCodeBuffer.slice(0, 6))).toEqual("MATRIX");
483+
expect(qrCodeBuffer[6]).toEqual(0x02); // version
484+
expect(qrCodeBuffer[7]).toEqual(0x02); // mode
485+
const txnIdLen = (qrCodeBuffer[8] << 8) + qrCodeBuffer[9];
486+
expect(textDecoder.decode(qrCodeBuffer.slice(10, 10 + txnIdLen))).toEqual(transactionId);
485487
// Alice's device's public key comes next, but we have nothing to do with it here.
486-
// const aliceDevicePubKey = qrCodeBuffer.subarray(10 + txnIdLen, 32 + 10 + txnIdLen);
487-
expect(qrCodeBuffer.subarray(42 + txnIdLen, 32 + 42 + txnIdLen)).toEqual(
488-
Buffer.from(MASTER_CROSS_SIGNING_PUBLIC_KEY_BASE64, "base64"),
488+
// const aliceDevicePubKey = qrCodeBuffer.slice(10 + txnIdLen, 32 + 10 + txnIdLen);
489+
expect(encodeUnpaddedBase64(qrCodeBuffer.slice(42 + txnIdLen, 32 + 42 + txnIdLen))).toEqual(
490+
MASTER_CROSS_SIGNING_PUBLIC_KEY_BASE64,
489491
);
490-
const sharedSecret = qrCodeBuffer.subarray(74 + txnIdLen);
492+
const sharedSecret = qrCodeBuffer.slice(74 + txnIdLen);
491493

492494
// we should still be "Ready" and have no verifier
493495
expect(request.phase).toEqual(VerificationPhase.Ready);
@@ -805,7 +807,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
805807
// we should now have QR data we can display
806808
const qrCodeBuffer = (await request.generateQRCode())!;
807809
expect(qrCodeBuffer).toBeTruthy();
808-
const sharedSecret = qrCodeBuffer.subarray(74 + transactionId.length);
810+
const sharedSecret = qrCodeBuffer.slice(74 + transactionId.length);
809811

810812
// the dummy device "scans" the displayed QR code and acknowledges it with a "m.key.verification.start"
811813
returnToDeviceMessageFromSync(buildReciprocateStartMessage(transactionId, sharedSecret));
@@ -1627,7 +1629,7 @@ function buildReadyMessage(
16271629
}
16281630

16291631
/** build an m.key.verification.start to-device message suitable for the m.reciprocate.v1 flow, originating from the dummy device */
1630-
function buildReciprocateStartMessage(transactionId: string, sharedSecret: Uint8Array) {
1632+
function buildReciprocateStartMessage(transactionId: string, sharedSecret: ArrayBuffer) {
16311633
return {
16321634
type: "m.key.verification.start",
16331635
content: {
@@ -1723,7 +1725,7 @@ function buildQRCode(
17231725
key2Base64: string,
17241726
sharedSecret: string,
17251727
mode = 0x02,
1726-
): Uint8Array {
1728+
): Uint8ClampedArray {
17271729
// https://spec.matrix.org/v1.7/client-server-api/#qr-code-format
17281730

17291731
const qrCodeBuffer = Buffer.alloc(150); // oversize
@@ -1739,5 +1741,5 @@ function buildQRCode(
17391741
idx += qrCodeBuffer.write(sharedSecret, idx);
17401742

17411743
// truncate to the right length
1742-
return qrCodeBuffer.subarray(0, idx);
1744+
return new Uint8ClampedArray(qrCodeBuffer.subarray(0, idx));
17431745
}

spec/unit/base64.spec.ts

Lines changed: 1 addition & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -15,34 +15,10 @@ limitations under the License.
1515
*/
1616

1717
import { TextEncoder, TextDecoder } from "util";
18-
import NodeBuffer from "node:buffer";
1918

2019
import { decodeBase64, encodeBase64, encodeUnpaddedBase64, encodeUnpaddedBase64Url } from "../../src/base64";
2120

22-
describe.each(["browser", "node"])("Base64 encoding (%s)", (env) => {
23-
let origBuffer = Buffer;
24-
25-
beforeAll(() => {
26-
if (env === "browser") {
27-
origBuffer = Buffer;
28-
// @ts-ignore
29-
// eslint-disable-next-line no-global-assign
30-
Buffer = undefined;
31-
32-
globalThis.atob = NodeBuffer.atob;
33-
globalThis.btoa = NodeBuffer.btoa;
34-
}
35-
});
36-
37-
afterAll(() => {
38-
// eslint-disable-next-line no-global-assign
39-
Buffer = origBuffer;
40-
// @ts-ignore
41-
globalThis.atob = undefined;
42-
// @ts-ignore
43-
globalThis.btoa = undefined;
44-
});
45-
21+
describe("Base64 encoding", () => {
4622
it("Should decode properly encoded data", () => {
4723
const decoded = new TextDecoder().decode(decodeBase64("ZW5jb2RpbmcgaGVsbG8gd29ybGQ="));
4824

spec/unit/matrixrtc/MatrixRTCSession.spec.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ const membershipTemplate: CallMembershipData = {
3838

3939
const mockFocus = { type: "mock" };
4040

41+
const textEncoder = new TextEncoder();
42+
4143
describe("MatrixRTCSession", () => {
4244
let client: MatrixClient;
4345
let sess: MatrixRTCSession | undefined;
@@ -1345,7 +1347,7 @@ describe("MatrixRTCSession", () => {
13451347
sess!.reemitEncryptionKeys();
13461348
expect(encryptionKeyChangedListener).toHaveBeenCalledTimes(1);
13471349
expect(encryptionKeyChangedListener).toHaveBeenCalledWith(
1348-
Buffer.from("this is the key", "utf-8"),
1350+
textEncoder.encode("this is the key"),
13491351
0,
13501352
"@bob:example.org:bobsphone",
13511353
);
@@ -1377,7 +1379,7 @@ describe("MatrixRTCSession", () => {
13771379
sess!.reemitEncryptionKeys();
13781380
expect(encryptionKeyChangedListener).toHaveBeenCalledTimes(1);
13791381
expect(encryptionKeyChangedListener).toHaveBeenCalledWith(
1380-
Buffer.from("this is the key", "utf-8"),
1382+
textEncoder.encode("this is the key"),
13811383
4,
13821384
"@bob:example.org:bobsphone",
13831385
);
@@ -1409,7 +1411,7 @@ describe("MatrixRTCSession", () => {
14091411
sess!.reemitEncryptionKeys();
14101412
expect(encryptionKeyChangedListener).toHaveBeenCalledTimes(1);
14111413
expect(encryptionKeyChangedListener).toHaveBeenCalledWith(
1412-
Buffer.from("this is the key", "utf-8"),
1414+
textEncoder.encode("this is the key"),
14131415
0,
14141416
"@bob:example.org:bobsphone",
14151417
);
@@ -1436,12 +1438,12 @@ describe("MatrixRTCSession", () => {
14361438
sess!.reemitEncryptionKeys();
14371439
expect(encryptionKeyChangedListener).toHaveBeenCalledTimes(2);
14381440
expect(encryptionKeyChangedListener).toHaveBeenCalledWith(
1439-
Buffer.from("this is the key", "utf-8"),
1441+
textEncoder.encode("this is the key"),
14401442
0,
14411443
"@bob:example.org:bobsphone",
14421444
);
14431445
expect(encryptionKeyChangedListener).toHaveBeenCalledWith(
1444-
Buffer.from("this is the key", "utf-8"),
1446+
textEncoder.encode("this is the key"),
14451447
4,
14461448
"@bob:example.org:bobsphone",
14471449
);
@@ -1489,7 +1491,7 @@ describe("MatrixRTCSession", () => {
14891491
sess!.reemitEncryptionKeys();
14901492
expect(encryptionKeyChangedListener).toHaveBeenCalledTimes(1);
14911493
expect(encryptionKeyChangedListener).toHaveBeenCalledWith(
1492-
Buffer.from("newer key", "utf-8"),
1494+
textEncoder.encode("newer key"),
14931495
0,
14941496
"@bob:example.org:bobsphone",
14951497
);
@@ -1537,7 +1539,7 @@ describe("MatrixRTCSession", () => {
15371539
sess!.reemitEncryptionKeys();
15381540
expect(encryptionKeyChangedListener).toHaveBeenCalledTimes(1);
15391541
expect(encryptionKeyChangedListener).toHaveBeenCalledWith(
1540-
Buffer.from("second key", "utf-8"),
1542+
textEncoder.encode("second key"),
15411543
0,
15421544
"@bob:example.org:bobsphone",
15431545
);

src/@types/global.d.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,4 +67,24 @@ declare global {
6767
// on webkit: we should check if we still need to do this
6868
webkitGetUserMedia?: DummyInterfaceWeShouldntBeUsingThis;
6969
}
70+
71+
export interface Uint8ArrayToBase64Options {
72+
alphabet?: "base64" | "base64url";
73+
omitPadding?: boolean;
74+
}
75+
76+
interface Uint8Array {
77+
// https://tc39.es/proposal-arraybuffer-base64/spec/#sec-uint8array.prototype.tobase64
78+
toBase64?(options?: Uint8ArrayToBase64Options): string;
79+
}
80+
81+
export interface Uint8ArrayFromBase64Options {
82+
alphabet?: "base64"; // Our fallback code only handles base64.
83+
lastChunkHandling?: "loose"; // Our fallback code doesn't support other handling at this time.
84+
}
85+
86+
interface Uint8ArrayConstructor {
87+
// https://tc39.es/proposal-arraybuffer-base64/spec/#sec-uint8array.frombase64
88+
fromBase64?(base64: string, options?: Uint8ArrayFromBase64Options): Uint8Array;
89+
}
7090
}

src/base64.ts

Lines changed: 38 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -18,48 +18,61 @@ limitations under the License.
1818
* Base64 encoding and decoding utilities
1919
*/
2020

21+
function toBase64(uint8Array: Uint8Array, options: Uint8ArrayToBase64Options): string {
22+
if (typeof uint8Array.toBase64 === "function") {
23+
// Currently this is only supported in Firefox,
24+
// but we match the options in the hope in the future we can rely on it for all environments.
25+
// https://tc39.es/proposal-arraybuffer-base64/spec/#sec-uint8array.prototype.tobase64
26+
return uint8Array.toBase64(options);
27+
}
28+
29+
let base64 = btoa(uint8Array.reduce((acc, current) => acc + String.fromCharCode(current), ""));
30+
if (options.omitPadding) {
31+
base64 = base64.replace(/={1,2}$/, "");
32+
}
33+
if (options.alphabet === "base64url") {
34+
base64 = base64.replace(/\+/g, "-").replace(/\//g, "_");
35+
}
36+
37+
return base64;
38+
}
39+
2140
/**
2241
* Encode a typed array of uint8 as base64.
2342
* @param uint8Array - The data to encode.
2443
* @returns The base64.
2544
*/
26-
export function encodeBase64(uint8Array: ArrayBuffer | Uint8Array): string {
27-
// A brief note on the state of base64 encoding in Javascript.
28-
// As of 2023, there is still no common native impl between both browsers and
29-
// node. Older Webpack provides an impl for Buffer and there is a polyfill class
30-
// for it. There are also plenty of pure js impls, eg. base64-js which has 2336
31-
// dependents at current count. Using this would probably be fine although it's
32-
// a little under-docced and run by an individual. The node impl works fine,
33-
// the browser impl works but predates Uint8Array and so only uses strings.
34-
// Right now, switching between native (or polyfilled) impls like this feels
35-
// like the least bad option, but... *shrugs*.
36-
if (typeof Buffer === "function") {
37-
return Buffer.from(uint8Array).toString("base64");
38-
} else if (typeof btoa === "function" && uint8Array instanceof Uint8Array) {
39-
// ArrayBuffer is a node concept so the param should always be a Uint8Array on
40-
// the browser. We need to check because ArrayBuffers don't have reduce.
41-
return btoa(uint8Array.reduce((acc, current) => acc + String.fromCharCode(current), ""));
42-
} else {
43-
throw new Error("No base64 impl found!");
44-
}
45+
export function encodeBase64(uint8Array: Uint8Array): string {
46+
return toBase64(uint8Array, { alphabet: "base64", omitPadding: false });
4547
}
4648

4749
/**
4850
* Encode a typed array of uint8 as unpadded base64.
4951
* @param uint8Array - The data to encode.
5052
* @returns The unpadded base64.
5153
*/
52-
export function encodeUnpaddedBase64(uint8Array: ArrayBuffer | Uint8Array): string {
53-
return encodeBase64(uint8Array).replace(/={1,2}$/, "");
54+
export function encodeUnpaddedBase64(uint8Array: Uint8Array): string {
55+
return toBase64(uint8Array, { alphabet: "base64", omitPadding: true });
5456
}
5557

5658
/**
5759
* Encode a typed array of uint8 as unpadded base64 using the URL-safe encoding.
5860
* @param uint8Array - The data to encode.
5961
* @returns The unpadded base64.
6062
*/
61-
export function encodeUnpaddedBase64Url(uint8Array: ArrayBuffer | Uint8Array): string {
62-
return encodeUnpaddedBase64(uint8Array).replace(/\+/g, "-").replace(/\//g, "_");
63+
export function encodeUnpaddedBase64Url(uint8Array: Uint8Array): string {
64+
return toBase64(uint8Array, { alphabet: "base64url", omitPadding: true });
65+
}
66+
67+
function fromBase64(base64: string, options: Uint8ArrayFromBase64Options): Uint8Array {
68+
if (typeof Uint8Array.fromBase64 === "function") {
69+
// Currently this is only supported in Firefox,
70+
// but we match the options in the hope in the future we can rely on it for all environments.
71+
// https://tc39.es/proposal-arraybuffer-base64/spec/#sec-uint8array.frombase64
72+
return Uint8Array.fromBase64(base64, options);
73+
}
74+
75+
return Uint8Array.from(atob(base64), (c) => c.charCodeAt(0));
6376
}
6477

6578
/**
@@ -68,21 +81,6 @@ export function encodeUnpaddedBase64Url(uint8Array: ArrayBuffer | Uint8Array): s
6881
* @returns The decoded data.
6982
*/
7083
export function decodeBase64(base64: string): Uint8Array {
71-
// See encodeBase64 for a short treatise on base64 en/decoding in JS
72-
if (typeof Buffer === "function") {
73-
return Buffer.from(base64, "base64");
74-
} else if (typeof atob === "function") {
75-
const itFunc = function* (): Generator<number> {
76-
const decoded = atob(
77-
// built-in atob doesn't support base64url: convert so we support either
78-
base64.replace(/-/g, "+").replace(/_/g, "/"),
79-
);
80-
for (let i = 0; i < decoded.length; ++i) {
81-
yield decoded.charCodeAt(i);
82-
}
83-
};
84-
return Uint8Array.from(itFunc());
85-
} else {
86-
throw new Error("No base64 impl found!");
87-
}
84+
// The function requires us to select an alphabet, but we don't know if base64url was used so we convert.
85+
return fromBase64(base64.replace(/-/g, "+").replace(/_/g, "/"), { alphabet: "base64", lastChunkHandling: "loose" });
8886
}

src/client.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3899,28 +3899,28 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
38993899
}
39003900

39013901
private async restoreKeyBackup(
3902-
privKey: ArrayLike<number>,
3902+
privKey: Uint8Array,
39033903
targetRoomId: undefined,
39043904
targetSessionId: undefined,
39053905
backupInfo: IKeyBackupInfo,
39063906
opts?: IKeyBackupRestoreOpts,
39073907
): Promise<IKeyBackupRestoreResult>;
39083908
private async restoreKeyBackup(
3909-
privKey: ArrayLike<number>,
3909+
privKey: Uint8Array,
39103910
targetRoomId: string,
39113911
targetSessionId: undefined,
39123912
backupInfo: IKeyBackupInfo,
39133913
opts?: IKeyBackupRestoreOpts,
39143914
): Promise<IKeyBackupRestoreResult>;
39153915
private async restoreKeyBackup(
3916-
privKey: ArrayLike<number>,
3916+
privKey: Uint8Array,
39173917
targetRoomId: string,
39183918
targetSessionId: string,
39193919
backupInfo: IKeyBackupInfo,
39203920
opts?: IKeyBackupRestoreOpts,
39213921
): Promise<IKeyBackupRestoreResult>;
39223922
private async restoreKeyBackup(
3923-
privKey: ArrayLike<number>,
3923+
privKey: Uint8Array,
39243924
targetRoomId: string | undefined,
39253925
targetSessionId: string | undefined,
39263926
backupInfo: IKeyBackupInfo,

src/common-crypto/CryptoBackend.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ export interface CryptoBackend extends SyncCryptoCallbacks, CryptoApi {
108108
* @param backupInfo - The backup information
109109
* @param privKey - The private decryption key.
110110
*/
111-
getBackupDecryptor(backupInfo: KeyBackupInfo, privKey: ArrayLike<number>): Promise<BackupDecryptor>;
111+
getBackupDecryptor(backupInfo: KeyBackupInfo, privKey: Uint8Array): Promise<BackupDecryptor>;
112112

113113
/**
114114
* Import a list of room keys restored from backup

src/crypto-api/recovery-key.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ const KEY_SIZE = 32;
2626
* @param key
2727
*/
2828
export function encodeRecoveryKey(key: ArrayLike<number>): string | undefined {
29-
const buf = Buffer.alloc(OLM_RECOVERY_KEY_PREFIX.length + key.length + 1);
29+
const buf = new Uint8Array(OLM_RECOVERY_KEY_PREFIX.length + key.length + 1);
3030
buf.set(OLM_RECOVERY_KEY_PREFIX, 0);
3131
buf.set(key, OLM_RECOVERY_KEY_PREFIX.length);
3232

src/crypto-api/verification.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ export interface VerificationRequest
155155
* @param qrCodeData - the decoded QR code.
156156
* @returns A verifier; call `.verify()` on it to wait for the other side to complete the verification flow.
157157
*/
158-
scanQRCode(qrCodeData: Uint8Array): Promise<Verifier>;
158+
scanQRCode(qrCodeData: Uint8ClampedArray): Promise<Verifier>;
159159

160160
/**
161161
* The verifier which is doing the actual verification, once the method has been established.
@@ -170,15 +170,15 @@ export interface VerificationRequest
170170
*
171171
* @deprecated Not supported in Rust Crypto. Use {@link VerificationRequest#generateQRCode} instead.
172172
*/
173-
getQRCodeBytes(): Buffer | undefined;
173+
getQRCodeBytes(): Uint8ClampedArray | undefined;
174174

175175
/**
176176
* Generate the data for a QR code allowing the other device to verify this one, if it supports it.
177177
*
178178
* Only returns data once `phase` is {@link VerificationPhase.Ready} and the other party can scan a QR code;
179179
* otherwise returns `undefined`.
180180
*/
181-
generateQRCode(): Promise<Buffer | undefined>;
181+
generateQRCode(): Promise<Uint8ClampedArray | undefined>;
182182

183183
/**
184184
* If this request has been cancelled, the cancellation code (e.g `m.user`) which is responsible for cancelling

src/crypto/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1846,7 +1846,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
18461846
/**
18471847
* Implementation of {@link CryptoBackend#getBackupDecryptor}.
18481848
*/
1849-
public async getBackupDecryptor(backupInfo: KeyBackupInfo, privKey: ArrayLike<number>): Promise<BackupDecryptor> {
1849+
public async getBackupDecryptor(backupInfo: KeyBackupInfo, privKey: Uint8Array): Promise<BackupDecryptor> {
18501850
if (!(privKey instanceof Uint8Array)) {
18511851
throw new Error(`getBackupDecryptor expects Uint8Array`);
18521852
}

0 commit comments

Comments
 (0)