diff --git a/src/background-service.ts b/src/background-service.ts index 060397d575ee..ee5af6e79fd1 100644 --- a/src/background-service.ts +++ b/src/background-service.ts @@ -12,7 +12,13 @@ import elliptic from 'elliptic' import * as CryptoService from './extension/background-script/CryptoService' import * as WelcomeService from './extension/background-script/WelcomeService' import * as PeopleService from './extension/background-script/PeopleService' +import { decryptFromMessageWithProgress } from './extension/background-script/CryptoServices/decryptFrom' Object.assign(window, { CryptoService, WelcomeService, PeopleService }) +Object.assign(window, { + ServicesWithProgress: { + decryptFrom: decryptFromMessageWithProgress, + }, +}) require('./extension/service') require('./provider.worker') diff --git a/src/components/InjectedComponents/DecryptedPost.tsx b/src/components/InjectedComponents/DecryptedPost.tsx index e348f3999d3d..ab96c8d5347b 100644 --- a/src/components/InjectedComponents/DecryptedPost.tsx +++ b/src/components/InjectedComponents/DecryptedPost.tsx @@ -1,18 +1,21 @@ -import React, { useCallback, useState, useEffect } from 'react' +import React, { useCallback, useState, useEffect, useRef } from 'react' import AsyncComponent from '../../utils/components/AsyncComponent' import { AdditionalContent } from './AdditionalPostContent' import { useShareMenu } from './SelectPeopleDialog' import { sleep } from '../../utils/utils' -import Services from '../../extension/service' +import { ServicesWithProgress } from '../../extension/service' import { geti18nString } from '../../utils/i18n' import { makeStyles } from '@material-ui/styles' import { Box, Link, useMediaQuery, useTheme, Button, SnackbarContent } from '@material-ui/core' import { Person } from '../../database' -import { Identifier, PersonIdentifier, PostIVIdentifier } from '../../database/type' +import { Identifier, PersonIdentifier } from '../../database/type' import { NotSetupYetPrompt } from '../shared/NotSetupYetPrompt' -import { MessageCenter, TypedMessages } from '../../utils/messages' import { Payload } from '../../utils/type-transform/Payload' -import { SuccessDecryption } from '../../extension/background-script/CryptoServices/decryptFrom' +import { + SuccessDecryption, + FailureDecryption, + DecryptionProgress, +} from '../../extension/background-script/CryptoServices/decryptFrom' interface DecryptPostSuccessProps { data: { signatureVerifyResult: boolean; content: string } @@ -66,13 +69,13 @@ function DecryptPostSuccess({ data, people, ...props }: DecryptPostSuccessProps) ) } -function DecryptPostAwaiting(props: { type?: DecryptingStatus }) { +function DecryptPostAwaiting(props: { type?: DecryptionProgress }) { const key = { finding_post_key: 'decrypted_postbox_decrypting_finding_post_key', finding_person_public_key: 'decrypted_postbox_decrypting_finding_person_key', undefined: 'decrypted_postbox_decrypting', } as const - return + return } const useDecryptPostFailedStyles = makeStyles({ @@ -81,16 +84,16 @@ const useDecryptPostFailedStyles = makeStyles({ maxWidth: '50em', }, }) -export function DecryptPostFailed({ error, retry }: { error: Error; retry: () => void }) { +export function DecryptPostFailed({ error, retry }: { error: Error; retry?: () => void }) { const styles = useDecryptPostFailedStyles() if (error && error.message === geti18nString('service_not_setup_yet')) { return } - const button = ( + const button = retry ? ( - ) + ) : null return } @@ -102,18 +105,20 @@ interface DecryptPostProps { encryptedText: string people: Person[] alreadySelectedPreviously: Person[] - payload: Payload | null - requestAppendRecipients(to: Person[]): Promise } -type DecryptingStatus = undefined | 'finding_person_public_key' | 'finding_post_key' function DecryptPost(props: DecryptPostProps) { const { postBy, whoAmI, encryptedText, people, alreadySelectedPreviously, requestAppendRecipients } = props - const { payload } = props const [decryptedResult, setDecryptedResult] = useState(null) - const [decryptingStatus, setDecryptingStatus] = useState(undefined) + const [decryptingStatus, setDecryptingStatus] = useState( + undefined, + ) const [__, forceReDecrypt] = useState() + const cancelTask = useRef<() => void>(() => {}) + useEffect(() => { + cancelTask.current() + }) const rAD = useCallback( async (people: Person[]) => { @@ -122,33 +127,6 @@ function DecryptPost(props: DecryptPostProps) { }, [requestAppendRecipients], ) - useEffect(() => { - let listener = (data: TypedMessages['decryptionStatusUpdated']) => { - if (!payload) return - const id = new PostIVIdentifier(postBy.network, payload.iv) - if (id.equals(data.post)) { - switch (data.status) { - case 'finding_person_public_key': - setDecryptingStatus('finding_person_public_key') - break - case 'finding_post_key': - setDecryptingStatus('finding_post_key') - break - case 'found_person_public_key': - setDecryptingStatus('finding_post_key') - forceReDecrypt(Math.random()) - break - case 'new_post_key': - setDecryptingStatus('finding_post_key') - forceReDecrypt(Math.random()) - break - default: - break - } - } - } - return MessageCenter.on('decryptionStatusUpdated', listener) - }, [postBy, payload]) if (decryptedResult) { return ( ) } + const awaitingComponent = + decryptingStatus && 'error' in decryptingStatus ? ( + + ) : ( + + ) return ( Services.Crypto.decryptFrom(encryptedText, postBy, whoAmI)} + promise={async () => { + cancelTask.current() + const iter = ServicesWithProgress.decryptFrom(encryptedText, postBy, whoAmI) + cancelTask.current = () => iter.throw!(new Error('Client aborted')) + let last = await iter.next() + while (!last.done) { + setDecryptingStatus(last.value) + last = await iter.next() + } + return last.value + }} dependencies={[ __, encryptedText, @@ -170,21 +164,21 @@ function DecryptPost(props: DecryptPostProps) { Identifier.IdentifiersToString(people.map(x => x.identifier)), Identifier.IdentifiersToString(alreadySelectedPreviously.map(x => x.identifier)), ]} - awaitingComponent={} - completeComponent={_props => { - if ('error' in _props.data) { + awaitingComponent={awaitingComponent} + completeComponent={result => { + if ('error' in result.data) { return ( forceReDecrypt(Math.random())} - error={new Error(_props.data.error)} + error={new Error(result.data.error)} /> ) } - setDecryptedResult(_props.data) - props.onDecrypted(_props.data.content) + setDecryptedResult(result.data) + props.onDecrypted(result.data.content) return ( { setAlreadySelectedPreviously(alreadySelectedPreviously.concat(people)) diff --git a/src/extension/background-script/CryptoServices/decryptFrom.ts b/src/extension/background-script/CryptoServices/decryptFrom.ts index e59c68e32db9..984dd3dc83a0 100644 --- a/src/extension/background-script/CryptoServices/decryptFrom.ts +++ b/src/extension/background-script/CryptoServices/decryptFrom.ts @@ -11,15 +11,22 @@ import { PersonIdentifier, PostIVIdentifier } from '../../../database/type' import { queryPostDB, updatePostDB } from '../../../database/post' import { addPerson } from './addPerson' import { MessageCenter } from '../../../utils/messages' +type Progress = { + progress: 'finding_person_public_key' | 'finding_post_key' +} type Success = { signatureVerifyResult: boolean content: string } -export type SuccessDecryption = Success - type Failure = { error: string } +export type SuccessDecryption = Success +export type FailureDecryption = Failure +export type DecryptionProgress = Progress +type ReturnOfDecryptFromMessageWithProgress = AsyncIterator & { + [Symbol.asyncIterator](): AsyncIterator +} /** * Decrypt message from a user @@ -27,11 +34,12 @@ type Failure = { * @param by Post by * @param whoAmI My username */ -export async function decryptFrom( +export async function* decryptFromMessageWithProgress( encrypted: string, by: PersonIdentifier, whoAmI: PersonIdentifier, -): Promise { +): ReturnOfDecryptFromMessageWithProgress { + // If any of parameters is changed, we will not handle it. const data = deconstructPayload(encrypted)! if (!data) { try { @@ -43,39 +51,45 @@ export async function decryptFrom( const version = data.version if (version === -40 || version === -39) { const { encryptedText, iv, ownersAESKeyEncrypted, signature, version } = data - const postIVIdentifier = new PostIVIdentifier(by.network, iv) const unverified = [version === -40 ? '2/4' : '3/4', ownersAESKeyEncrypted, iv, encryptedText].join('|') const cryptoProvider = version === -40 ? Alpha40 : Alpha39 const [cachedPostResult, setPostCache] = await decryptFromCache(data, by) let byPerson = await queryPersonDB(by) - if (!byPerson || !byPerson.publicKey) { - MessageCenter.emit('decryptionStatusUpdated', { - post: postIVIdentifier, - status: 'finding_person_public_key', - }) + let iterations = 0 + while (byPerson === null || !byPerson.publicKey) { + iterations += 1 + if (iterations < 10) yield { progress: 'finding_person_public_key' } + else return { error: geti18nString('service_others_key_not_found', by.userId) } byPerson = await addPerson(by).catch(() => null) - } - if (!byPerson || !byPerson.publicKey) { - if (cachedPostResult) return { signatureVerifyResult: false, content: cachedPostResult } - const undo = Gun2.subscribePersonFromGun2(by, data => { - if (data && (data.provePostId || '').length > 0) { - publishMessagePeopleFound(postIVIdentifier) - } - removeListeners() - }) - const undo2 = MessageCenter.on('newPerson', data => { - if (data.identifier.equals(by)) { - publishMessagePeopleFound(postIVIdentifier) - removeListeners() - } - }) - const removeListeners = () => { - undo() - undo2() + + if (!byPerson || !byPerson.publicKey) { + if (cachedPostResult) return { signatureVerifyResult: false, content: cachedPostResult } + let rejectGun = () => {} + let rejectDatabase = () => {} + const awaitGun = new Promise((resolve, reject) => { + const undo = Gun2.subscribePersonFromGun2(by, data => { + if (data && (data.provePostId || '').length > 0) { + undo() + resolve() + rejectGun = () => (undo(), reject()) + } + }) + }) + const awaitDatabase = new Promise((resolve, reject) => { + const undo = MessageCenter.on('newPerson', data => { + if (data.identifier.equals(by)) { + undo() + resolve() + rejectDatabase = () => (undo(), reject()) + } + }) + }) + await Promise.race([awaitGun, awaitDatabase]) + .then(() => (rejectDatabase(), rejectGun())) + .catch(() => null) } - return { error: geti18nString('service_others_key_not_found', by.userId) } } const mine = await getMyPrivateKey(whoAmI) @@ -119,49 +133,80 @@ export async function decryptFrom( ), content: cachedPostResult, } - MessageCenter.emit('decryptionStatusUpdated', { - post: postIVIdentifier, - status: 'finding_post_key', - }) - const aesKeyEncrypted = - version === -40 - ? // eslint-disable-next-line import/no-deprecated - await Gun1.queryPostAESKey(iv, whoAmI.userId) - : await Gun2.queryPostKeysOnGun2(iv, mine.publicKey) + yield { progress: 'finding_post_key' } + const aesKeyEncrypted: Array = [] + if (version === -40) { + // Deprecated payload + // eslint-disable-next-line import/no-deprecated + const result = await Gun1.queryPostAESKey(iv, whoAmI.userId) + if (result === undefined) return { error: geti18nString('service_not_share_target') } + aesKeyEncrypted.push(result) + } else if (version === -39) { + const keys = await Gun2.queryPostKeysOnGun2(iv, mine.publicKey) + aesKeyEncrypted.push(...keys) + } - // TODO: Replace this error with: - // You do not have the necessary private key to decrypt this message. - // What to do next: You can ask your friend to visit your profile page, so that their Maskbook extension will detect and add you to recipients. - // ? after the auto-share with friends is done. - if (aesKeyEncrypted === undefined) { - const undo = Gun2.subscribePostKeysOnGun2(iv, mine.publicKey, data => { - MessageCenter.emit('decryptionStatusUpdated', { - post: postIVIdentifier, - status: 'new_post_key', - }) - undo() - }) - return { - error: geti18nString('service_not_share_target'), + // If we can decrypt with current info, just do it. + try { + // ! DO NOT remove the await here. Or the catch block will be always skipped. + return await decryptWith(aesKeyEncrypted) + } catch (e) { + if (e.message === geti18nString('service_not_share_target')) { + console.debug(e) + // TODO: Replace this error with: + // You do not have the necessary private key to decrypt this message. + // What to do next: You can ask your friend to visit your profile page, so that their Maskbook extension will detect and add you to recipients. + // ? after the auto-share with friends is done. + yield { error: geti18nString('service_not_share_target') } as Failure + } else { + // Unknown error + throw e } } - const [contentArrayBuffer, postAESKey] = await cryptoProvider.decryptMessage1ToNByOther({ - version, - AESKeyEncrypted: aesKeyEncrypted, - authorsPublicKeyECDH: byPerson.publicKey, - encryptedContent: encryptedText, - privateKeyECDH: mine.privateKey, - iv, + + // Failed, we have to wait for the future info from gun. + return new Promise((resolve, reject) => { + const undo = Gun2.subscribePostKeysOnGun2(iv, mine.publicKey, async key => { + console.log('New key received, trying', key) + try { + const result = await decryptWith(key) + undo() + resolve(result) + } catch (e) { + console.debug(e) + } + }) }) - // Store the key to speed up next time decrypt - setPostCache(postAESKey) - const content = decodeText(contentArrayBuffer) - try { - if (!signature) throw new TypeError('No signature') - const signatureVerifyResult = await cryptoProvider.verify(unverified, signature, byPerson.publicKey) - return { signatureVerifyResult, content } - } catch { - return { signatureVerifyResult: false, content } + + async function decryptWith( + key: + | Alpha39.PublishedAESKey + | Alpha40.PublishedAESKey + | Array, + ) { + const [contentArrayBuffer, postAESKey] = await cryptoProvider.decryptMessage1ToNByOther({ + version, + AESKeyEncrypted: key, + authorsPublicKeyECDH: byPerson!.publicKey!, + encryptedContent: encryptedText, + privateKeyECDH: mine!.privateKey, + iv, + }) + + // Store the key to speed up next time decrypt + setPostCache(postAESKey) + const content = decodeText(contentArrayBuffer) + try { + if (!signature) throw new TypeError('No signature') + const signatureVerifyResult = await cryptoProvider.verify( + unverified, + signature, + byPerson!.publicKey!, + ) + return { signatureVerifyResult, content } + } catch { + return { signatureVerifyResult: false, content } + } } } } catch (e) { @@ -173,11 +218,16 @@ export async function decryptFrom( } return { error: geti18nString('service_unknown_payload') } } -function publishMessagePeopleFound(postIdByIV: PostIVIdentifier) { - MessageCenter.emit('decryptionStatusUpdated', { - post: postIdByIV, - status: 'found_person_public_key', - }) + +export async function decryptFrom( + ...args: Parameters +): Promise { + const iter = decryptFromMessageWithProgress(...args) + let yielded = await iter.next() + while (!yielded.done) { + yielded = await iter.next() + } + return yielded.value } async function decryptFromCache(postPayload: Payload, by: PersonIdentifier) { diff --git a/src/extension/service.ts b/src/extension/service.ts index 7d2a4a4722be..81e6dc26510d 100644 --- a/src/extension/service.ts +++ b/src/extension/service.ts @@ -1,4 +1,4 @@ -import { AsyncCall } from '@holoflows/kit/es/util/AsyncCall' +import { AsyncCall, AsyncGeneratorCall } from '@holoflows/kit/es/util/AsyncCall' import { GetContext, OnlyRunInContext } from '@holoflows/kit/es/Extension/Context' import * as MockService from './mock-service' import Serialization from '../utils/type-transform/Serialization' @@ -23,6 +23,26 @@ if (!('Services' in globalThis)) { register(Reflect.get(globalThis, 'WelcomeService'), 'Welcome', MockService.WelcomeService) register(Reflect.get(globalThis, 'PeopleService'), 'People', MockService.PeopleService) } +interface ServicesWithProgress { + // Sorry you should add import at '../background-service.ts' + decryptFrom: typeof import('./background-script/CryptoServices/decryptFrom').decryptFromMessageWithProgress +} + +const logOptions: AsyncCallOptions['log'] = { + beCalled: true, + localError: false, + remoteError: true, + sendLocalStack: true, + type: 'pretty', +} +export const ServicesWithProgress = AsyncGeneratorCall( + Reflect.get(globalThis, 'ServicesWithProgress'), + { + key: 'services+progress', + log: logOptions, + serializer: Serialization, + }, +) Object.assign(globalThis, { PersonIdentifier, @@ -33,13 +53,6 @@ Object.assign(globalThis, { }) //#region type Service = Record Promise> -const logOptions: AsyncCallOptions['log'] = { - beCalled: true, - localError: false, - remoteError: true, - sendLocalStack: true, - type: 'pretty', -} function register(service: T, name: keyof Services, mock?: Partial) { if (OnlyRunInContext(['content', 'options', 'debugging', 'background'], false)) { console.log(`Service ${name} registered in ${GetContext()}`) diff --git a/src/network/gun/version.2/post.ts b/src/network/gun/version.2/post.ts index 34a6c935b158..3f0d28832de6 100644 --- a/src/network/gun/version.2/post.ts +++ b/src/network/gun/version.2/post.ts @@ -43,7 +43,7 @@ export async function queryPostKeysOnGun2( export function subscribePostKeysOnGun2( postSalt: string, partitionByCryptoKey: CryptoKey, - callback: (data: PostOnGun2) => void, + callback: (data: SharedAESKeyGun2) => void, ) { hashPostSalt(postSalt).then(postHash => { hashCryptoKey(partitionByCryptoKey).then(keyHash => { @@ -51,7 +51,8 @@ export function subscribePostKeysOnGun2( // @ts-ignore .get(keyHash) .map() - .on((data: PostOnGun2) => { + .on((data: SharedAESKeyGun2) => { + // @ts-ignore const { _, ...data2 } = Object.assign({}, data) callback(data2) }) diff --git a/src/social-network/defaults/injectPostInspector.tsx b/src/social-network/defaults/injectPostInspector.tsx index 86ca8569da0a..931afde30667 100644 --- a/src/social-network/defaults/injectPostInspector.tsx +++ b/src/social-network/defaults/injectPostInspector.tsx @@ -5,7 +5,6 @@ import { renderInShadowRoot } from '../../utils/jss/renderInShadowRoot' import { PersonIdentifier } from '../../database/type' import { useValueRef } from '../../utils/hooks/useValueRef' import { PostInspector } from '../../components/InjectedComponents/PostInspector' -import { Payload } from '../../utils/type-transform/Payload' export function injectPostInspectorDefault(config: InjectPostInspectorDefaultConfig) { const { injectionPoint, zipPost } = config @@ -15,7 +14,6 @@ export function injectPostInspectorDefault(config: InjectPostInspectorDefaultCon const onDecrypted = (val: string) => (current.decryptedPostContent.value = val) return renderInShadowRoot( postBy: ValueRef postContent: ValueRef - payload: ValueRef }) { - const { onDecrypted, zipPost, postBy, postID, postContent, payload } = props + const { onDecrypted, zipPost, postBy, postID, postContent } = props const id = useValueRef(postID) const by = useValueRef(postBy) const content = useValueRef(postContent) - return ( - - ) + return } diff --git a/src/utils/messages.ts b/src/utils/messages.ts index 6a736c39707d..3c83b7fca3be 100644 --- a/src/utils/messages.ts +++ b/src/utils/messages.ts @@ -1,13 +1,8 @@ import { MessageCenter as MC } from '@holoflows/kit/es' import { Person } from '../database' -import { PostIVIdentifier } from '../database/type' import Serialization from './type-transform/Serialization' interface UIEvent { - decryptionStatusUpdated: { - post: PostIVIdentifier - status: 'finding_person_public_key' | 'finding_post_key' | 'found_person_public_key' | 'new_post_key' - } closeActiveTab: undefined } interface KeyStoreEvent { diff --git a/yarn.lock b/yarn.lock index 92301c20c602..9d2538195b6b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1051,7 +1051,7 @@ "@holoflows/kit@https://github.com/DimensionDev/holoflows-kit": version "0.4.0" - resolved "https://github.com/DimensionDev/holoflows-kit#dded411cb8336cea0dcd64a4a3e8b00793ad2d06" + resolved "https://github.com/DimensionDev/holoflows-kit#ce69f8a329817d55ec24e6335080037935a7b129" dependencies: "@types/lodash-es" "^4.1.4" concurrent-lock "^1.0.7"