Skip to content

Commit

Permalink
Binary-encode media proxy URLs rather than base64'ing a JSON (#498)
Browse files Browse the repository at this point in the history
Saves us 80 bytes for the test data (down from 208 to 128),
while leaving room for future changes.
  • Loading branch information
tadzik authored Aug 13, 2024
1 parent 5b65c25 commit 36e6eb8
Show file tree
Hide file tree
Showing 2 changed files with 48 additions and 26 deletions.
15 changes: 9 additions & 6 deletions spec/unit/media-proxy.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ describe("MediaProxy", function() {
beforeEach(async function () {
mediaProxy = new MediaProxy({
publicUrl,
ttl: 60,
ttl: 60 * 1000,
signingKey: await signingKey,
}, new MatrixClient('https://example.com', 'test_access_token'));
})
Expand All @@ -27,10 +27,13 @@ describe("MediaProxy", function() {
});

it('can decode a media url', async () => {
const url = await mediaProxy.generateMediaUrl('mxc://example.com/some_media');
const token = url.pathname.slice('/my-cs-path/v1/media/download'.length);
console.log(token);
const now = Date.now();
const mxc = 'mxc://example.com/some_media';
const url = await mediaProxy.generateMediaUrl(mxc);
const token = url.pathname.slice('/my-cs-path/v1/media/download/'.length);
const data = await mediaProxy.verifyMediaToken(token);
console.log(data);
expect('mxc://' + data.mxc).toBe(mxc);
expect(data.endDt).toBeGreaterThanOrEqual(now + 60 * 1000);
expect(data.endDt).toBeLessThanOrEqual(now + 61 * 1000);
});
});
});
59 changes: 39 additions & 20 deletions src/components/media-proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,36 +74,55 @@ export class MediaProxy {
}

async getMediaToken(metadata: MediaMetadata) {
const data = Buffer.from(JSON.stringify(metadata));
const sig = Buffer.from(
await subtleCrypto.sign(ALGORITHM, this.opts.signingKey, data)
).toString('base64url');
return Buffer.from(JSON.stringify({...metadata, signature: sig})).toString('base64url');
// V1 token format:
// - At offset zero: a single byte, numeric int, indicating a token version.
// Version 0 is reserved for future use, for the remote possibility we run out of versions in an int8 :)
// - At offset 1: the SHA-512 HMAC signature of the payload (64 bytes)
// - At offset 65: MediaMetadata.endDt, encoded as a Big-Endian double (matching JS' `number` type).
// An undefined endDt is encoded as a -1. 8 bytes.
// - At offset 73: the MXC of the media content, until the end of the buffer.
// The payload, for the purpose of generating the signature, is the byte-encoded endDt concatenated with the byte-encoded MXC.

Check failure on line 84 in src/components/media-proxy.ts

View workflow job for this annotation

GitHub Actions / lint

This line has a length of 134. Maximum allowed is 120
const version = Buffer.allocUnsafe(1);
version.writeInt8(1);

const dt = Buffer.allocUnsafe(8);
dt.writeDoubleBE(metadata.endDt ?? -1);

const mxcBuf = Buffer.from(metadata.mxc);

const payload = Buffer.concat([dt, mxcBuf]);
const sig = Buffer.from(await subtleCrypto.sign(ALGORITHM, this.opts.signingKey, payload));

const token = Buffer.concat([version, sig, dt, mxcBuf]);
return token.toString('base64url');
}

async verifyMediaToken(token: string ): Promise<MediaMetadata> {
let data: MediaMetadata&{signature: string};
try {
data = JSON.parse(Buffer.from(token, 'base64url').toString('utf-8'));
}
catch (ex) {
throw new ApiError("Media token is invalid", ErrCode.BadValue);
}
const signature = Buffer.from(data.signature, 'base64url');
if (!signature) {
throw new ApiError("Signature missing from metadata", ErrCode.BadValue);
async verifyMediaToken(token: string): Promise<MediaMetadata> {
const buf = Buffer.from(token, 'base64url');
let cursor = 0;
const version = buf.readInt8(cursor++);
if (version !== 1) {
throw new ApiError(`Unrecognized version of media token (${version})`, ErrCode.BadValue);
}
const signedJson = {...data, signature: undefined};
const signedData = Buffer.from(JSON.stringify(signedJson));

const sig = buf.subarray(cursor, cursor += 64);
const dtBuf = buf.subarray(cursor, cursor += 8);
const mxcBuf = buf.subarray(cursor);

try {
if (!subtleCrypto.verify(ALGORITHM, this.opts.signingKey, signedData, signature)) {
if (!subtleCrypto.verify(ALGORITHM, this.opts.signingKey, Buffer.concat([dtBuf, mxcBuf]), sig)) {
throw new Error('Signature did not match');
}
}
catch (ex) {
throw new ApiError('Media token signature is invalid', ErrCode.BadValue)
}
return signedJson;

const dt = dtBuf.readDoubleBE();
return {
mxc: mxcBuf.toString(),
endDt: dt === -1 ? undefined : dt,
};
}


Expand Down

0 comments on commit 36e6eb8

Please sign in to comment.