From e5fa21b06defcbc1b611a8024722f141b1472136 Mon Sep 17 00:00:00 2001 From: Tak Wai Wong <64229756+tak-hntlabs@users.noreply.github.com> Date: Thu, 8 Aug 2024 07:14:54 -0700 Subject: [PATCH] Refactor spaceImage getter as getSpaceImage() async (#620) Should not be doing an async decrypt inside applySnapshot. Timing issue when getting the spaceImage from the streamStateView_Space. `spaceImage` can be undefined while the payload is being decrypted. Instead of doing the decryption inside `applySnapshot`, store the encrypted data only. The caller uses `await streamStateView_Space.spaceContent.getSpaceImage()` to get the decrypted chunked media. --- packages/sdk/src/space.test.ts | 27 +++----- packages/sdk/src/streamStateView_Space.ts | 77 ++++++++++++++--------- 2 files changed, 56 insertions(+), 48 deletions(-) diff --git a/packages/sdk/src/space.test.ts b/packages/sdk/src/space.test.ts index 7fd3fe7ba..694296515 100644 --- a/packages/sdk/src/space.test.ts +++ b/packages/sdk/src/space.test.ts @@ -166,7 +166,7 @@ describe('spaceTests', () => { }) // decrypt the snapshot and assert the image values - let encryptedData = + const encryptedData = spaceStream.view.snapshot?.content.case === 'spaceContent' ? spaceStream.view.snapshot.content.value.spaceImage?.data : undefined @@ -175,7 +175,7 @@ describe('spaceTests', () => { isEncryptedData(encryptedData) && encryptedData.algorithm === AES_GCM_DERIVED_ALGORITHM, ).toBe(true) - let decrypted = encryptedData + const decrypted = encryptedData ? await bobsClient.decryptSpaceImage(spaceId, encryptedData) : undefined expect( @@ -208,24 +208,13 @@ describe('spaceTests', () => { }) // decrypt the snapshot and assert the image values - encryptedData = - spaceStream.view.snapshot?.content.case === 'spaceContent' - ? spaceStream.view.snapshot.content.value.spaceImage?.data - : undefined + const spaceImage = await spaceStream.view.spaceContent.getSpaceImage() expect( - encryptedData !== undefined && - isEncryptedData(encryptedData) && - encryptedData.algorithm === AES_GCM_DERIVED_ALGORITHM, - ).toBe(true) - decrypted = encryptedData - ? await bobsClient.decryptSpaceImage(spaceId, encryptedData) - : undefined - expect( - decrypted !== undefined && - decrypted?.info?.mimetype === image2.mimetype && - decrypted?.info?.filename === image2.filename && - decrypted.encryption.case === 'derived' && - decrypted.encryption.value.context === spaceContractAddress, + spaceImage !== undefined && + spaceImage?.info?.mimetype === image2.mimetype && + spaceImage?.info?.filename === image2.filename && + spaceImage.encryption.case === 'derived' && + spaceImage.encryption.value.context === spaceContractAddress, ).toBe(true) }) }) diff --git a/packages/sdk/src/streamStateView_Space.ts b/packages/sdk/src/streamStateView_Space.ts index df6bac34d..37c6194ec 100644 --- a/packages/sdk/src/streamStateView_Space.ts +++ b/packages/sdk/src/streamStateView_Space.ts @@ -31,21 +31,17 @@ export type ParsedChannelProperties = { export class StreamStateView_Space extends StreamStateView_AbstractContent { readonly streamId: string readonly spaceChannelsMetadata = new Map() - private _spaceImage: ChunkedMedia | undefined + private spaceImage: ChunkedMedia | undefined + private encryptedSpaceImage: EncryptedData | undefined + private decryptionInProgress: + | { encryptedData: EncryptedData; promise: Promise } + | undefined constructor(streamId: string) { super() this.streamId = streamId } - get spaceImage(): ChunkedMedia | undefined { - return this._spaceImage - } - - private set spaceImage(value: ChunkedMedia | undefined) { - this._spaceImage = value - } - applySnapshot( eventHash: string, _snapshot: Snapshot, @@ -59,13 +55,7 @@ export class StreamStateView_Space extends StreamStateView_AbstractContent { } if (content.spaceImage?.data) { - this.decryptSpaceImage(content.spaceImage.data) - .then((media) => { - this.spaceImage = media - }) - .catch((err) => { - throw err - }) + this.encryptedSpaceImage = content.spaceImage.data } } @@ -135,13 +125,7 @@ export class StreamStateView_Space extends StreamStateView_AbstractContent { ) break case 'spaceImage': - this.decryptSpaceImage(payload.content.value) - .then((media) => { - this.spaceImage = media - }) - .catch((err) => { - throw err - }) + this.encryptedSpaceImage = payload.content.value break case undefined: break @@ -150,6 +134,47 @@ export class StreamStateView_Space extends StreamStateView_AbstractContent { } } + public async getSpaceImage(): Promise { + // if we have an encrypted space image, decrypt it + if (this.encryptedSpaceImage) { + const encryptedData = this.encryptedSpaceImage + this.encryptedSpaceImage = undefined + this.decryptionInProgress = { + promise: this.decryptSpaceImage(encryptedData), + encryptedData, + } + return this.decryptionInProgress.promise + } + + // if there isn't an updated encrypted space image, but a decryption is + // in progress, return the promise + if (this.decryptionInProgress) { + return this.decryptionInProgress.promise + } + + // always return the decrypted space image + return this.spaceImage + } + + private async decryptSpaceImage( + encryptedData: EncryptedData, + ): Promise { + try { + const spaceAddress = contractAddressFromSpaceId(this.streamId) + const context = spaceAddress.toLowerCase() + const plaintext = await decryptDerivedAESGCM(context, encryptedData) + const decryptedImage = ChunkedMedia.fromBinary(plaintext) + if (encryptedData === this.decryptionInProgress?.encryptedData) { + this.spaceImage = decryptedImage + } + return decryptedImage + } finally { + if (encryptedData === this.decryptionInProgress?.encryptedData) { + this.decryptionInProgress = undefined + } + } + } + private addSpacePayload_UpdateChannelAutojoin( payload: SpacePayload_UpdateChannelAutojoin, stateEmitter: TypedEmitter | undefined, @@ -189,12 +214,6 @@ export class StreamStateView_Space extends StreamStateView_AbstractContent { ) } - private async decryptSpaceImage(encryptedImage: EncryptedData): Promise { - const keyPhrase = contractAddressFromSpaceId(this.streamId) - const plaintext = await decryptDerivedAESGCM(keyPhrase, encryptedImage) - return ChunkedMedia.fromBinary(plaintext) - } - private addSpacePayload_Channel( _eventHash: string, payload: SpacePayload_ChannelMetadata | SpacePayload_ChannelUpdate,