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"