From 605c2e2510f4598f3a21b5bedc4851800956c8ea Mon Sep 17 00:00:00 2001 From: Jack Works Date: Mon, 14 Mar 2022 12:01:41 +0800 Subject: [PATCH 1/8] feat: add decryptor from new infra --- .../src/encryption/DecryptionTypes.ts | 1 + .../mask/background/database/persona/app.ts | 4 + .../mask/background/database/persona/db.ts | 1 + .../background/database/persona/helper.ts | 224 +++++++++++++++++ .../mask/background/database/persona/web.ts | 7 + .../mask/background/database/post/helper.ts | 29 +++ .../background/services/crypto/decryption.ts | 237 ++++++++++++++++++ .../mask/background/services/crypto/index.ts | 4 +- packages/mask/src/database/Persona/helpers.ts | 61 +---- .../CryptoServices/verifyOthersProve.ts | 3 +- .../background-script/IdentityService.ts | 2 +- .../background-script/WelcomeService.ts | 2 +- 12 files changed, 509 insertions(+), 66 deletions(-) create mode 100644 packages/mask/background/database/persona/helper.ts create mode 100644 packages/mask/background/database/post/helper.ts create mode 100644 packages/mask/background/services/crypto/decryption.ts diff --git a/packages/encryption/src/encryption/DecryptionTypes.ts b/packages/encryption/src/encryption/DecryptionTypes.ts index f202300eeb55..5b1ae35125cb 100644 --- a/packages/encryption/src/encryption/DecryptionTypes.ts +++ b/packages/encryption/src/encryption/DecryptionTypes.ts @@ -152,6 +152,7 @@ export enum ErrorReasons { // Not used in this library. UnrecognizedAuthor = '[@masknet/encryption] No author is recognized which is required for the image steganography decoding.', CurrentProfileDoesNotConnectedToPersona = '[@masknet/encryption] Cannot decrypt by E2E because no persona is linked with the current profile.', + NoPayloadFound = '[@masknet/encryption] No payload found in this material.', } export class DecryptError extends Error { static Reasons = ErrorReasons diff --git a/packages/mask/background/database/persona/app.ts b/packages/mask/background/database/persona/app.ts index 907c065f112e..6dd823b1e84d 100644 --- a/packages/mask/background/database/persona/app.ts +++ b/packages/mask/background/database/persona/app.ts @@ -30,6 +30,10 @@ import type { } from '@masknet/public-api' import { MaskMessages } from '../../../shared' import { convertPersonaHexPublicKey } from './util' + +export async function createPersonaDBReadonlyAccess(action: () => Promise) { + await action() +} export async function consistentPersonaDBWriteAccess(action: () => Promise) { await action() } diff --git a/packages/mask/background/database/persona/db.ts b/packages/mask/background/database/persona/db.ts index fae1a9249f96..0756bb0bb839 100644 --- a/packages/mask/background/database/persona/db.ts +++ b/packages/mask/background/database/persona/db.ts @@ -12,6 +12,7 @@ export const { queryPersonaByProfileDB, createPersonaDB, attachProfileDB, + createPersonaDBReadonlyAccess, consistentPersonaDBWriteAccess, updatePersonaDB, createOrUpdatePersonaDB, diff --git a/packages/mask/background/database/persona/helper.ts b/packages/mask/background/database/persona/helper.ts new file mode 100644 index 000000000000..033450ae466c --- /dev/null +++ b/packages/mask/background/database/persona/helper.ts @@ -0,0 +1,224 @@ +import { concatArrayBuffer, decodeArrayBuffer } from '@dimensiondev/kit' +import { + AESCryptoKey, + AESJsonWebKey, + ECKeyIdentifier, + ECKeyIdentifierFromJsonWebKey, + EC_Private_JsonWebKey, + EC_Public_CryptoKey, + EC_Public_JsonWebKey, + IdentifierMap, + PersonaIdentifier, + ProfileIdentifier, +} from '@masknet/shared-base' +import { + attachProfileDB, + consistentPersonaDBWriteAccess, + createOrUpdatePersonaDB, + createPersonaDB, + createPersonaDBReadonlyAccess, + FullPersonaDBTransaction, + LinkedProfileDetails, + PersonaRecord, + queryPersonaByProfileDB, + queryPersonasWithPrivateKey, + queryProfileDB, +} from './db' + +// #region Local key helpers +/** + * If has local key of a profile in the database. + * @param id Profile Identifier + */ +export async function hasLocalKeyOf(id: ProfileIdentifier) { + let has = false + await createPersonaDBReadonlyAccess(async (tx) => { + const result = await getLocalKeyOf(id, tx) + has = !!result + }) + return has +} + +/** + * Try to decrypt data using local key. + * + * @param authorHint Author of the local key + * @param data Data to be decrypted + * @param iv IV + */ +export async function decryptByLocalKey( + authorHint: ProfileIdentifier | null, + data: Uint8Array, + iv: Uint8Array, +): Promise { + const candidateKeys: AESJsonWebKey[] = [] + + if (authorHint) { + await createPersonaDBReadonlyAccess(async (tx) => { + const key = await getLocalKeyOf(authorHint, tx) + key && candidateKeys.push(key) + }) + // TODO: We may push every local key we owned to the candidate list so we can also decrypt when authorHint is null, but that might be a performance pitfall when localKey field is not indexed. + } + + let check = () => {} + return Promise.any( + candidateKeys.map(async (key): Promise => { + const k = await crypto.subtle.importKey('jwk', key, { name: 'AES-GCM', length: 256 }, false, ['decrypt']) + check() + + const result: Uint8Array = await crypto.subtle.decrypt({ iv, name: 'AES-GCM' }, k, data) + check = abort + return result + }), + ) +} + +async function getLocalKeyOf(id: ProfileIdentifier, tx: FullPersonaDBTransaction<'readonly'>) { + const profile = await queryProfileDB(id, tx) + if (!profile) return + if (profile.localKey) return profile.localKey + if (!profile.linkedPersona) return + + const persona = await queryPersonaByProfileDB(id, tx) + return persona?.localKey +} +// #endregion + +// #region ECDH +export async function deriveAESByECDH(pub: EC_Public_CryptoKey, extractable = true) { + const curve = (pub.algorithm as EcKeyAlgorithm).namedCurve || '' + const sameCurvePrivateKeys = new IdentifierMap(new Map(), ECKeyIdentifier) + + await createPersonaDBReadonlyAccess(async (tx) => { + const personas = await queryPersonasWithPrivateKey(tx) + for (const persona of personas) { + if (!persona.privateKey) continue + if (persona.privateKey.crv !== curve) continue + sameCurvePrivateKeys.set(persona.identifier, persona.privateKey) + } + }) + + const deriveResult = new IdentifierMap(new Map(), ECKeyIdentifier) + const result = await Promise.allSettled( + [...sameCurvePrivateKeys].map(async ([id, key]) => { + const k = await crypto.subtle.importKey( + 'jwk', + key, + { name: 'ECDH', namedCurve: key.crv! } as EcKeyAlgorithm, + false, + ['deriveKey'], + ) + const result = await crypto.subtle.deriveKey( + { name: 'ECDH', public: pub }, + k, + { name: 'AES-GCM', length: 256 }, + extractable, + ['encrypt', 'decrypt'], + ) + deriveResult.set(id, result as AESCryptoKey) + }), + ) + const failed = result.filter((x): x is PromiseRejectedResult => x.status === 'rejected') + if (failed.length) { + console.warn('Failed to ECDH', ...failed.map((x) => x.reason)) + } + return deriveResult +} + +const KEY = decodeArrayBuffer('KEY') +const IV = decodeArrayBuffer('IV') +export async function deriveAESByECDH_version38_or_older( + pub: EC_Public_CryptoKey, + iv: Uint8Array, + extractable = false, +) { + const deriveResult = (await deriveAESByECDH(pub, true)).__raw_map__ + type R = [key: AESCryptoKey, iv: Uint8Array] + const next_map = new Map() + + for (const [id, key] of deriveResult) { + const derivedKeyRaw = await crypto.subtle.exportKey('raw', key) + const _a = concatArrayBuffer(derivedKeyRaw, iv) + const nextAESKeyMaterial = await crypto.subtle.digest('SHA-256', concatArrayBuffer(_a, iv, KEY)) + const iv_pre = new Uint8Array(await crypto.subtle.digest('SHA-256', concatArrayBuffer(_a, iv, IV))) + const nextIV = new Uint8Array(16) + for (let i = 0; i <= 16; i += 1) { + // eslint-disable-next-line no-bitwise + nextIV[i] = iv_pre[i] ^ iv_pre[16 + i] + } + const nextAESKey = await crypto.subtle.importKey( + 'raw', + nextAESKeyMaterial, + { name: 'AES-GCM', length: 256 }, + extractable, + ['encrypt', 'decrypt'], + ) + next_map.set(id, [nextAESKey as AESCryptoKey, nextIV]) + } + + return new IdentifierMap(next_map, ECKeyIdentifier) +} +// #endregion + +// #region normal functions +export async function createPersonaByJsonWebKey(options: { + publicKey: EC_Public_JsonWebKey + privateKey: EC_Private_JsonWebKey + localKey?: AESJsonWebKey + nickname?: string + mnemonic?: PersonaRecord['mnemonic'] + uninitialized?: boolean +}): Promise { + const identifier = ECKeyIdentifierFromJsonWebKey(options.publicKey) + const record: PersonaRecord = { + createdAt: new Date(), + updatedAt: new Date(), + identifier: identifier, + linkedProfiles: new IdentifierMap(new Map(), ProfileIdentifier), + publicKey: options.publicKey, + privateKey: options.privateKey, + nickname: options.nickname, + mnemonic: options.mnemonic, + localKey: options.localKey, + hasLogout: false, + uninitialized: options.uninitialized, + } + await consistentPersonaDBWriteAccess((t) => createPersonaDB(record, t)) + return identifier +} + +export async function createProfileWithPersona( + profileID: ProfileIdentifier, + data: LinkedProfileDetails, + keys: { + nickname?: string + publicKey: EC_Public_JsonWebKey + privateKey?: EC_Private_JsonWebKey + localKey?: AESJsonWebKey + mnemonic?: PersonaRecord['mnemonic'] + }, +): Promise { + const ec_id = ECKeyIdentifierFromJsonWebKey(keys.publicKey) + const rec: PersonaRecord = { + createdAt: new Date(), + updatedAt: new Date(), + identifier: ec_id, + linkedProfiles: new IdentifierMap(new Map(), ProfileIdentifier), + nickname: keys.nickname, + publicKey: keys.publicKey, + privateKey: keys.privateKey, + localKey: keys.localKey, + mnemonic: keys.mnemonic, + hasLogout: false, + } + await consistentPersonaDBWriteAccess(async (t) => { + await createOrUpdatePersonaDB(rec, { explicitUndefinedField: 'ignore', linkedProfiles: 'merge' }, t) + await attachProfileDB(profileID, ec_id, data, t) + }) +} +// #endregion + +function abort() { + throw new Error('Cancelled') +} diff --git a/packages/mask/background/database/persona/web.ts b/packages/mask/background/database/persona/web.ts index c4f337b015cc..0e38c7f84c37 100644 --- a/packages/mask/background/database/persona/web.ts +++ b/packages/mask/background/database/persona/web.ts @@ -145,6 +145,13 @@ export async function createRelationsTransaction() { return createTransaction(database, 'readwrite')('relations') } +export async function createPersonaDBReadonlyAccess( + action: (t: FullPersonaDBTransaction<'readonly'>) => Promise, +) { + const database = await db() + const transaction = createTransaction(database, 'readonly')('personas', 'profiles', 'relations') + await action(transaction) +} // @deprecated Please create a transaction directly export async function consistentPersonaDBWriteAccess( action: (t: FullPersonaDBTransaction<'readwrite'>) => Promise, diff --git a/packages/mask/background/database/post/helper.ts b/packages/mask/background/database/post/helper.ts new file mode 100644 index 000000000000..c7bad98079cc --- /dev/null +++ b/packages/mask/background/database/post/helper.ts @@ -0,0 +1,29 @@ +import type { AESCryptoKey, PostIVIdentifier } from '@masknet/shared-base' +import { CryptoKeyToJsonWebKey } from '../../../utils-pure' +import { createTransaction } from '../utils/openDB' +import { PostDBAccess, queryPostDB, createPostDB, updatePostDB, PostRecord } from './index' + +export async function savePostKeyToDB( + id: PostIVIdentifier, + key: AESCryptoKey, + extraInfo: Omit, +): Promise { + const jwk = await CryptoKeyToJsonWebKey(key) + { + const t = createTransaction(await PostDBAccess(), 'readwrite')('post') + const post = await queryPostDB(id, t) + if (!post) { + await createPostDB( + { + identifier: id, + postCryptoKey: jwk, + foundAt: new Date(), + ...extraInfo, + }, + t, + ) + } else { + await updatePostDB({ ...post, postCryptoKey: jwk }, 'override', t) + } + } +} diff --git a/packages/mask/background/services/crypto/decryption.ts b/packages/mask/background/services/crypto/decryption.ts new file mode 100644 index 000000000000..10d571ad9cbb --- /dev/null +++ b/packages/mask/background/services/crypto/decryption.ts @@ -0,0 +1,237 @@ +import { encodeArrayBuffer, unreachable } from '@dimensiondev/kit' +import { + decrypt, + parsePayload, + DecryptProgressKind, + PublicKeyAlgorithmEnum, + DecryptProgress, + SocialNetworkEnum, + SocialNetworkEnumToProfileDomain, + AsymmetryCryptoKey, + socialNetworkDecoder, + steganographyDecodeImage, + DecryptError, + ErrorReasons, +} from '@masknet/encryption' +import { + AESCryptoKey, + EC_Public_CryptoKey, + EC_Public_JsonWebKey, + IdentifierMap, + PostIVIdentifier, + ProfileIdentifier, +} from '@masknet/shared-base' +import { noop } from 'lodash-unified' +import { queryPersonaByProfileDB } from '../../database/persona/db' +import { + createProfileWithPersona, + decryptByLocalKey, + deriveAESByECDH, + deriveAESByECDH_version38_or_older, + hasLocalKeyOf, +} from '../../database/persona/helper' +import { queryPostDB } from '../../database/post' +import { savePostKeyToDB } from '../../database/post/helper' +import { GUN_queryPostKey_version39Or38, GUN_queryPostKey_version40 } from '../../network/gun/encryption/queryPostKey' + +export type DecryptionInfo = { + type: DecryptProgressKind.Info + iv?: Uint8Array + claimedAuthor?: ProfileIdentifier + publicShared?: boolean +} +export type DecryptionProgress = DecryptProgress | DecryptionInfo + +export interface DecryptionContext { + currentSocialNetwork: SocialNetworkEnum + currentProfile: ProfileIdentifier | null + authorHint: ProfileIdentifier | null + postURL: string | undefined +} +export type SocialNetworkEncodedPayload = + | { type: 'text'; text: string } + | { type: 'image'; image: Blob } + | { type: 'image-url'; image: string } +const downloadImage = (url: string): Promise => fetch(url).then((x) => x.arrayBuffer()) + +/** + * + * @param encoded If the encoded content is a text, it should only contain 1 payload. Extra payload will be ignored. + * @param context + */ +export async function* decryptionWithSocialNetworkDecoding( + encoded: SocialNetworkEncodedPayload, + context: DecryptionContext, +): AsyncGenerator { + let decoded: string + if (encoded.type === 'text') { + decoded = socialNetworkDecoder(context.currentSocialNetwork, encoded.text)[0] + } else { + if (!context.authorHint || context.authorHint.isUnknown) { + return yield new DecryptError(ErrorReasons.UnrecognizedAuthor, undefined) + } + const result = await steganographyDecodeImage(encoded.image, { + pass: context.authorHint.toText(), + downloadImage, + }) + const val = socialNetworkDecoder(context.currentSocialNetwork, result)[0] + decoded = val[0] + } + + if (!decoded) return yield new DecryptError(ErrorReasons.NoPayloadFound, undefined) + yield* decryption(decoded, context) +} +export async function* decryption(payload: string | Uint8Array, context: DecryptionContext) { + const parse = await parsePayload(payload) + if (parse.err) return null + + const { currentSocialNetwork, postURL } = context + let { currentProfile, authorHint } = context + if (currentProfile?.isUnknown) currentProfile = null + if (authorHint?.isUnknown) authorHint = null + + // #region Identify the PostIdentifier + const iv = parse.val.encryption.unwrapOr(null)?.iv.unwrapOr(null) + { + if (!iv) return null + // iv is required to identify the post and it also used in comment encryption. + const info: DecryptionInfo = { type: DecryptProgressKind.Info, iv } + yield info + } + const id = new PostIVIdentifier( + SocialNetworkEnumToProfileDomain(currentSocialNetwork), + encodeArrayBuffer(iv.buffer), + ) + // #endregion + + // TODO: read in-memory cache to avoid db lookup + + // #region store author public key into the database + try { + const id = parse.unwrap().author.unwrap().unwrap() + const cacheKey = id.toText() + if (!hasStoredAuthorPublicKey.has(cacheKey)) { + storeAuthorPublicKey(id, authorHint, parse.unwrap().authorPublicKey.unwrap().unwrap()).catch(noop) + hasStoredAuthorPublicKey.add(cacheKey) + } + } catch {} + // #endregion + + const progress = decrypt( + { message: parse.val }, + { + getPostKeyCache: getPostKeyCache.bind(null, id), + setPostKeyCache: (key) => { + return savePostKeyToDB(id, key, { + // public post will not call this function. + // and recipients only will be set when posting/appending recipients. + recipients: new IdentifierMap(new Map()), + postBy: + authorHint || + parse.safeUnwrap().author.unwrapOr(null)?.unwrapOr(null) || + ProfileIdentifier.unknown, + url: postURL, + }) + }, + hasLocalKeyOf: hasLocalKeyOf, + decryptByLocalKey: decryptByLocalKey, + async deriveAESKey(pub) { + return Array.from((await deriveAESByECDH(pub)).values()) + }, + async deriveAESKey_version38_or_older(pub, iv) { + return Array.from((await deriveAESByECDH_version38_or_older(pub, iv)).values()) + }, + queryAuthorPublicKey(author, signal) { + return queryPublicKey(author || authorHint, false, signal) + }, + // TODO: get a gun instance + async *queryPostKey_version37() { + throw new Error('TODO') + }, + async *queryPostKey_version38(iv, signal) { + const author = await queryPublicKey(context.currentProfile, true) + if (!author) throw new DecryptError(ErrorReasons.CurrentProfileDoesNotConnectedToPersona, undefined) + yield* GUN_queryPostKey_version39Or38( + -38, + iv, + author, + getNetworkHint(context.currentSocialNetwork), + signal || new AbortController().signal, + ) + }, + async *queryPostKey_version39(iv, signal) { + const author = await queryPublicKey(context.currentProfile, true) + if (!author) throw new DecryptError(ErrorReasons.CurrentProfileDoesNotConnectedToPersona, undefined) + yield* GUN_queryPostKey_version39Or38( + -39, + iv, + author, + getNetworkHint(context.currentSocialNetwork), + signal || new AbortController().signal, + ) + }, + async queryPostKey_version40(iv) { + if (!currentProfile) return null + return GUN_queryPostKey_version40(iv, currentProfile.userId) + }, + }, + ) + + yield* progress + return null +} + +function getNetworkHint(x: SocialNetworkEnum) { + if (x === SocialNetworkEnum.Facebook) return '' + if (x === SocialNetworkEnum.Twitter) return 'twitter-' + if (x === SocialNetworkEnum.Minds) return 'minds-' + if (x === SocialNetworkEnum.Instagram) return 'instagram-' + if (x === SocialNetworkEnum.Unknown) + throw new TypeError('[@masknet/encryption] Current SNS network is not correctly configured.') + unreachable(x) +} + +async function getPostKeyCache(id: PostIVIdentifier) { + const post = await queryPostDB(id) + if (!post?.postCryptoKey) return null + const k = await crypto.subtle.importKey('jwk', post.postCryptoKey, { name: 'AES-GCM', length: 256 }, false, [ + 'decrypt', + ]) + return k as AESCryptoKey +} + +const hasStoredAuthorPublicKey = new Set() +async function storeAuthorPublicKey( + payloadAuthor: ProfileIdentifier, + postAuthor: ProfileIdentifier | null, + pub: AsymmetryCryptoKey, +) { + if (!payloadAuthor.equals(postAuthor)) { + // ! Author detected is not equal to AuthorHint. + // ! Skip store the public key because it might be a security problem. + return + } + if (pub.algr !== PublicKeyAlgorithmEnum.secp256k1) { + throw new Error('TODO: support other curves') + } + return createProfileWithPersona( + payloadAuthor, + { connectionConfirmState: 'confirmed' }, + { + publicKey: (await crypto.subtle.exportKey('jwk', pub.key)) as EC_Public_JsonWebKey, + }, + ) +} + +async function queryPublicKey(author: ProfileIdentifier | null, extractable = false, signal?: AbortSignal) { + if (!author) return null + const persona = await queryPersonaByProfileDB(author) + if (!persona) return null + return (await crypto.subtle.importKey( + 'jwk', + persona.publicKey, + { name: 'ECDH', namedCurve: persona.publicKey.crv! } as EcKeyAlgorithm, + extractable, + ['deriveKey'], + )) as EC_Public_CryptoKey +} diff --git a/packages/mask/background/services/crypto/index.ts b/packages/mask/background/services/crypto/index.ts index 1ef6b67e0fd2..b5dc25c3a910 100644 --- a/packages/mask/background/services/crypto/index.ts +++ b/packages/mask/background/services/crypto/index.ts @@ -1,3 +1 @@ -// import {} from '@masknet/encryption' -// await Promise.resolve() -export {} +export { type DecryptionInfo, type DecryptionProgress, decryption } from './decryption' diff --git a/packages/mask/src/database/Persona/helpers.ts b/packages/mask/src/database/Persona/helpers.ts index 1a087192abf6..1a7071285603 100644 --- a/packages/mask/src/database/Persona/helpers.ts +++ b/packages/mask/src/database/Persona/helpers.ts @@ -10,12 +10,8 @@ import { deletePersonaDB, safeDeletePersonaDB, queryPersonaByProfileDB, - createPersonaDB, - attachProfileDB, - LinkedProfileDetails, consistentPersonaDBWriteAccess, updatePersonaDB, - createOrUpdatePersonaDB, queryProfilesPagedDB, } from '../../../background/database/persona/db' import { queryAvatarDataURL } from '../../../background/database/avatar-cache/avatar' @@ -32,8 +28,8 @@ import { type EC_Public_JsonWebKey, type EC_Private_JsonWebKey, type AESJsonWebKey, - ECKeyIdentifierFromJsonWebKey, } from '@masknet/shared-base' +import { createPersonaByJsonWebKey } from '../../../background/database/persona/helper' export async function profileRecordToProfile(record: ProfileRecord): Promise { const rec = { ...record } @@ -229,61 +225,6 @@ export async function createPersonaByMnemonicV2(mnemonicWord: string, nickname: }) } -export async function createPersonaByJsonWebKey(options: { - publicKey: EC_Public_JsonWebKey - privateKey: EC_Private_JsonWebKey - localKey?: AESJsonWebKey - nickname?: string - mnemonic?: PersonaRecord['mnemonic'] - uninitialized?: boolean -}): Promise { - const identifier = ECKeyIdentifierFromJsonWebKey(options.publicKey) - const record: PersonaRecord = { - createdAt: new Date(), - updatedAt: new Date(), - identifier: identifier, - linkedProfiles: new IdentifierMap(new Map(), ProfileIdentifier), - publicKey: options.publicKey, - privateKey: options.privateKey, - nickname: options.nickname, - mnemonic: options.mnemonic, - localKey: options.localKey, - hasLogout: false, - uninitialized: options.uninitialized, - } - await consistentPersonaDBWriteAccess((t) => createPersonaDB(record, t)) - return identifier -} -export async function createProfileWithPersona( - profileID: ProfileIdentifier, - data: LinkedProfileDetails, - keys: { - nickname?: string - publicKey: EC_Public_JsonWebKey - privateKey?: EC_Private_JsonWebKey - localKey?: AESJsonWebKey - mnemonic?: PersonaRecord['mnemonic'] - }, -): Promise { - const ec_id = ECKeyIdentifierFromJsonWebKey(keys.publicKey) - const rec: PersonaRecord = { - createdAt: new Date(), - updatedAt: new Date(), - identifier: ec_id, - linkedProfiles: new IdentifierMap(new Map(), ProfileIdentifier), - nickname: keys.nickname, - publicKey: keys.publicKey, - privateKey: keys.privateKey, - localKey: keys.localKey, - mnemonic: keys.mnemonic, - hasLogout: false, - } - await consistentPersonaDBWriteAccess(async (t) => { - await createOrUpdatePersonaDB(rec, { explicitUndefinedField: 'ignore', linkedProfiles: 'merge' }, t) - await attachProfileDB(profileID, ec_id, data, t) - }) -} - export async function queryLocalKey(i: ProfileIdentifier | PersonaIdentifier): Promise { if (i instanceof ProfileIdentifier) { const profile = await queryProfileDB(i) diff --git a/packages/mask/src/extension/background-script/CryptoServices/verifyOthersProve.ts b/packages/mask/src/extension/background-script/CryptoServices/verifyOthersProve.ts index 47745da75604..d866d3afdb85 100644 --- a/packages/mask/src/extension/background-script/CryptoServices/verifyOthersProve.ts +++ b/packages/mask/src/extension/background-script/CryptoServices/verifyOthersProve.ts @@ -1,6 +1,7 @@ import { type ProfileIdentifier, decompressSecp256k1Key, ECKeyIdentifierFromJsonWebKey } from '@masknet/shared-base' import { decodePublicKeyWorker } from '../../../social-network/utils/text-payload-worker' -import { createProfileWithPersona, queryPersonaRecord } from '../../../database' +import { queryPersonaRecord } from '../../../database' +import { createProfileWithPersona } from '../../../../background/database/persona/helper' export async function verifyOthersProve(bio: string | { raw: string }, others: ProfileIdentifier): Promise { const compressedX = typeof bio === 'string' ? (await decodePublicKeyWorker(others.network))(bio) : [bio.raw] diff --git a/packages/mask/src/extension/background-script/IdentityService.ts b/packages/mask/src/extension/background-script/IdentityService.ts index 6c4ba9387f59..50d0d9f85b68 100644 --- a/packages/mask/src/extension/background-script/IdentityService.ts +++ b/packages/mask/src/extension/background-script/IdentityService.ts @@ -2,8 +2,8 @@ import * as bip39 from 'bip39' import { validateMnemonic } from 'bip39' import { decode, encode } from '@msgpack/msgpack' import { decodeArrayBuffer, decodeText, encodeArrayBuffer } from '@dimensiondev/kit' +import { createPersonaByJsonWebKey } from '../../../background/database/persona/helper' import { - createPersonaByJsonWebKey, loginPersona, personaRecordToPersona, queryAvatarDataURL, diff --git a/packages/mask/src/extension/background-script/WelcomeService.ts b/packages/mask/src/extension/background-script/WelcomeService.ts index 20e173baba5b..65a4160c4298 100644 --- a/packages/mask/src/extension/background-script/WelcomeService.ts +++ b/packages/mask/src/extension/background-script/WelcomeService.ts @@ -1,7 +1,7 @@ import { encodeText, delay } from '@dimensiondev/kit' import { type DashboardRoutes, PopupRoutes } from '@masknet/shared-base' import { recover_ECDH_256k1_KeyPair_ByMnemonicWord } from '../../utils/mnemonic-code' -import { createPersonaByJsonWebKey } from '../../database' +import { createPersonaByJsonWebKey } from '../../../background/database/persona/helper' import { attachProfileDB, LinkedProfileDetails } from '../../../background/database/persona/db' import { deriveLocalKeyFromECDHKey } from '../../utils/mnemonic-code/localKeyGenerate' import type { PersonaIdentifier, ProfileIdentifier, AESJsonWebKey } from '@masknet/shared-base' From 99c503bb0f909b7a247aa4e1e48ec82183477e47 Mon Sep 17 00:00:00 2001 From: Jack Works Date: Mon, 14 Mar 2022 13:39:32 +0800 Subject: [PATCH 2/8] refactor: use new decryption --- .../src/encryption/DecryptionTypes.ts | 4 +- packages/encryption/src/index.ts | 1 + packages/encryption/src/payload/types.ts | 20 +-- .../background/services/crypto/decryption.ts | 26 +-- .../mask/background/services/crypto/index.ts | 2 +- .../src/components/DataSource/usePostInfo.ts | 2 +- .../DecryptedPost/DecryptedPost.tsx | 154 ++++++++++-------- .../InjectedComponents/PostInspector.tsx | 21 +-- .../CryptoServices/decryptFrom.ts | 13 +- .../mask/src/extension/service-generator.ts | 1 + .../browser-action/index.ts | 3 +- .../facebook.com/base.ts | 3 +- .../instagram.com/base.ts | 3 +- .../social-network-adaptor/minds.com/base.ts | 3 +- .../social-network-adaptor/opensea.io/base.ts | 3 +- .../twitter.com/base.ts | 3 +- packages/mask/src/social-network/types.ts | 4 +- packages/mask/src/social-network/ui.ts | 3 +- .../utils/create-post-context.ts | 29 ++-- packages/plugin-infra/package.json | 1 + packages/plugin-infra/src/PostContext.ts | 23 +-- packages/plugin-infra/tsconfig.json | 3 +- pnpm-lock.yaml | 2 + 23 files changed, 180 insertions(+), 147 deletions(-) diff --git a/packages/encryption/src/encryption/DecryptionTypes.ts b/packages/encryption/src/encryption/DecryptionTypes.ts index 5b1ae35125cb..b02f25eb5ab7 100644 --- a/packages/encryption/src/encryption/DecryptionTypes.ts +++ b/packages/encryption/src/encryption/DecryptionTypes.ts @@ -1,6 +1,6 @@ import type { TypedMessage } from '@masknet/typed-message' import type { ProfileIdentifier, AESCryptoKey, EC_Public_CryptoKey } from '@masknet/shared-base' -import type { PayloadParseResult } from '../payload' +import type { PayloadParseResult, SupportedPayloadVersions } from '../payload' import { registerSerializableClass } from '@masknet/shared-base' export interface DecryptOptions { @@ -129,6 +129,8 @@ export interface DecryptReportedInfo { iv?: Uint8Array claimedAuthor?: ProfileIdentifier publicShared?: boolean + version?: SupportedPayloadVersions + ownersKeyEncrypted?: Uint8Array } export interface DecryptIntermediateProgress { type: DecryptProgressKind.Progress diff --git a/packages/encryption/src/index.ts b/packages/encryption/src/index.ts index b38c59a7c6e6..105319f0b31d 100644 --- a/packages/encryption/src/index.ts +++ b/packages/encryption/src/index.ts @@ -10,6 +10,7 @@ export { type Signature, type PayloadParseResult, type PayloadWellFormed, + type SupportedPayloadVersions, } from './payload' export { decrypt, diff --git a/packages/encryption/src/payload/types.ts b/packages/encryption/src/payload/types.ts index b108fd0adbdd..7e663e06a408 100644 --- a/packages/encryption/src/payload/types.ts +++ b/packages/encryption/src/payload/types.ts @@ -15,12 +15,7 @@ export declare namespace PayloadParseResult { E extends CryptoException | PayloadException = CryptoException | PayloadException, > = Result> export interface Payload { - /** - * Version starts from -42 but -42 and -41 are dropped. - * - * The latest version is -37. - */ - readonly version: -40 | -39 | -38 | -37 + readonly version: SupportedPayloadVersions readonly signature: OptionalField /** * The claimed author of this payload. @@ -56,12 +51,7 @@ export declare namespace PayloadParseResult { /** Well formed payload that can be encoded into the latest version */ export declare namespace PayloadWellFormed { export interface Payload { - /** - * Version starts from -42 but -42 and -41 are dropped. - * - * The latest version is -37. - */ - readonly version: -40 | -39 | -38 | -37 + readonly version: SupportedPayloadVersions readonly signature: Option /** * The claimed author of this payload. @@ -122,6 +112,12 @@ export enum SocialNetworkEnum { Instagram = 2, Minds = 3, } +/** + * Version starts from -42 but -42 and -41 are dropped. + * + * The latest version is -37. + */ +export type SupportedPayloadVersions = -37 | -38 | -39 | -40 const SocialNetworkEnumToDomain: Record = { [SocialNetworkEnum.Unknown]: 'localhost', [SocialNetworkEnum.Facebook]: 'facebook.com', diff --git a/packages/mask/background/services/crypto/decryption.ts b/packages/mask/background/services/crypto/decryption.ts index 10d571ad9cbb..32b934ead48f 100644 --- a/packages/mask/background/services/crypto/decryption.ts +++ b/packages/mask/background/services/crypto/decryption.ts @@ -12,6 +12,7 @@ import { steganographyDecodeImage, DecryptError, ErrorReasons, + DecryptReportedInfo, } from '@masknet/encryption' import { AESCryptoKey, @@ -34,14 +35,6 @@ import { queryPostDB } from '../../database/post' import { savePostKeyToDB } from '../../database/post/helper' import { GUN_queryPostKey_version39Or38, GUN_queryPostKey_version40 } from '../../network/gun/encryption/queryPostKey' -export type DecryptionInfo = { - type: DecryptProgressKind.Info - iv?: Uint8Array - claimedAuthor?: ProfileIdentifier - publicShared?: boolean -} -export type DecryptionProgress = DecryptProgress | DecryptionInfo - export interface DecryptionContext { currentSocialNetwork: SocialNetworkEnum currentProfile: ProfileIdentifier | null @@ -62,7 +55,7 @@ const downloadImage = (url: string): Promise => fetch(url).then((x) export async function* decryptionWithSocialNetworkDecoding( encoded: SocialNetworkEncodedPayload, context: DecryptionContext, -): AsyncGenerator { +): AsyncGenerator { let decoded: string if (encoded.type === 'text') { decoded = socialNetworkDecoder(context.currentSocialNetwork, encoded.text)[0] @@ -74,8 +67,7 @@ export async function* decryptionWithSocialNetworkDecoding( pass: context.authorHint.toText(), downloadImage, }) - const val = socialNetworkDecoder(context.currentSocialNetwork, result)[0] - decoded = val[0] + decoded = socialNetworkDecoder(context.currentSocialNetwork, result)[0] } if (!decoded) return yield new DecryptError(ErrorReasons.NoPayloadFound, undefined) @@ -95,7 +87,17 @@ export async function* decryption(payload: string | Uint8Array, context: Decrypt { if (!iv) return null // iv is required to identify the post and it also used in comment encryption. - const info: DecryptionInfo = { type: DecryptProgressKind.Info, iv } + const info: DecryptReportedInfo = { + type: DecryptProgressKind.Info, + iv, + version: parse.val.version, + } + if (parse.val.encryption.ok) { + const val = parse.val.encryption.val + info.publicShared = val.type === 'public' + if (val.type === 'E2E' && val.ownersAESKeyEncrypted.ok) + info.ownersKeyEncrypted = val.ownersAESKeyEncrypted.val + } yield info } const id = new PostIVIdentifier( diff --git a/packages/mask/background/services/crypto/index.ts b/packages/mask/background/services/crypto/index.ts index b5dc25c3a910..662dde46ca6b 100644 --- a/packages/mask/background/services/crypto/index.ts +++ b/packages/mask/background/services/crypto/index.ts @@ -1 +1 @@ -export { type DecryptionInfo, type DecryptionProgress, decryption } from './decryption' +export { decryptionWithSocialNetworkDecoding } from './decryption' diff --git a/packages/mask/src/components/DataSource/usePostInfo.ts b/packages/mask/src/components/DataSource/usePostInfo.ts index 51e502f65e66..43fdc666e360 100644 --- a/packages/mask/src/components/DataSource/usePostInfo.ts +++ b/packages/mask/src/components/DataSource/usePostInfo.ts @@ -2,7 +2,7 @@ import { activatedSocialNetworkUI } from '../../social-network' import { usePostInfoDetails } from '@masknet/plugin-infra' import type { ProfileIdentifier } from '@masknet/shared-base' -export { usePostInfo, PostInfoProvider, usePostInfoDetails, usePostInfoSharedPublic } from '@masknet/plugin-infra' +export { usePostInfo, PostInfoProvider, usePostInfoDetails } from '@masknet/plugin-infra' export function usePostLink() { const id = usePostInfoDetails.snsID() const identifier = usePostInfoDetails.identifier() diff --git a/packages/mask/src/components/InjectedComponents/DecryptedPost/DecryptedPost.tsx b/packages/mask/src/components/InjectedComponents/DecryptedPost/DecryptedPost.tsx index 89501b91242f..21a42504e831 100644 --- a/packages/mask/src/components/InjectedComponents/DecryptedPost/DecryptedPost.tsx +++ b/packages/mask/src/components/InjectedComponents/DecryptedPost/DecryptedPost.tsx @@ -1,6 +1,11 @@ import { Fragment, useEffect, useMemo, useReducer } from 'react' -import { makeTypedMessageTuple, TypedMessageTuple } from '@masknet/typed-message' -import type { ProfileIdentifier, Payload } from '@masknet/shared-base' +import { + extractTextFromTypedMessage, + makeTypedMessageTuple, + TypedMessage, + TypedMessageTuple, +} from '@masknet/typed-message' +import type { ProfileIdentifier } from '@masknet/shared-base' import { or } from '@masknet/theme' import { ServicesWithProgress } from '../../../extension/service' @@ -13,14 +18,13 @@ import type { import { DecryptPostSuccess, DecryptPostSuccessProps } from './DecryptedPostSuccess' import { DecryptPostAwaiting, DecryptPostAwaitingProps } from './DecryptPostAwaiting' import { DecryptPostFailed, DecryptPostFailedProps } from './DecryptPostFailed' -import { - usePostClaimedAuthor, - usePostInfoDetails, - usePostInfoSharedPublic, - usePostInfo, -} from '../../DataSource/usePostInfo' -import { asyncIteratorWithResult } from '../../../utils/type-transform/asyncIteratorHelpers' -import { delay } from '@dimensiondev/kit' +import { usePostClaimedAuthor, usePostInfoDetails, usePostInfo } from '../../DataSource/usePostInfo' +import { delay, encodeArrayBuffer } from '@dimensiondev/kit' +import { activatedSocialNetworkUI } from '../../../social-network' +import type { DecryptionContext, SocialNetworkEncodedPayload } from '../../../../background/services/crypto/decryption' +import { DecryptProgressKind } from '@masknet/encryption' +import type { PostContext } from '@masknet/plugin-infra' +import { Some } from 'ts-results' function progressReducer( state: { key: string; progress: SuccessDecryption | FailureDecryption | DecryptionProgress }[], @@ -69,7 +73,6 @@ export function DecryptPost(props: DecryptPostProps) { const authorInPayload = usePostClaimedAuthor() const current = usePostInfo()! const currentPostBy = usePostInfoDetails.author() - const decryptedPayloadForImage = usePostInfoDetails.decryptedPayloadForImage() const postBy = or(authorInPayload, currentPostBy) const postMetadataImages = usePostInfoDetails.postMetadataImages() const Success = props.successComponent || DecryptPostSuccess @@ -93,70 +96,60 @@ export function DecryptPost(props: DecryptPostProps) { // pass 1: // decrypt post content and image attachments - const decryptedPayloadForImageAlpha38 = decryptedPayloadForImage?.version === -38 ? decryptedPayloadForImage : null - const sharedPublic = usePostInfoSharedPublic() || decryptedPayloadForImageAlpha38?.sharedPublic || false + const sharedPublic = usePostInfoDetails.publicShared() useEffect(() => { const signal = new AbortController() - async function makeProgress( - key: string, - decryptionProcess: ReturnType, - ) { - const refreshProgress = (progress: SuccessDecryption | FailureDecryption | DecryptionProgress) => - dispatch({ - type: 'refresh', - key, - progress, - }) - for await (const process of asyncIteratorWithResult(decryptionProcess)) { - if (signal.signal.aborted) - return decryptionProcess.return?.({ type: 'error', internal: true, error: 'aborted' }) - if (process.done) { - if (process.value.type === 'success') { - current.iv.value = process.value.iv - current.decryptedPayloadForImage.value = process.value.decryptedPayloadForImage - } - return refreshProgress(process.value) - } - const status = process.value - refreshProgress(status) - if (status.type === 'progress') { - if (status.progress === 'intermediate_success') refreshProgress(status.data) - else if (status.progress === 'iv_decrypted') current.iv.value = status.iv - else if (status.progress === 'payload_decrypted') - current.decryptedPayloadForImage.value = status.decryptedPayloadForImage - } - } - } - const postURL = current.url.getCurrentValue()?.toString() - if (deconstructedPayload.ok) + if (deconstructedPayload.ok) { makeProgress( - 'post text', - ServicesWithProgress.decryptFromText(deconstructedPayload.val, postBy, whoAmI.network, whoAmI, postURL), + postURL, + postBy, + whoAmI, + { type: 'text', text: extractTextFromTypedMessage(current.rawMessage.getCurrentValue()).unwrapOr('') }, + (message, iv) => { + dispatch({ + type: 'refresh', + key: 'text', + progress: { + type: 'success', + content: message, + internal: false, + through: [], + iv: encodeArrayBuffer(iv), + }, + }) + }, + current.decryptedReport, + signal.signal, ) + } postMetadataImages.forEach((url) => { if (signal.signal.aborted) return - makeProgress(url, ServicesWithProgress.decryptFromImageUrl(url, postBy, whoAmI.network, whoAmI, postURL)) + makeProgress( + postURL, + postBy, + whoAmI, + { type: 'image-url', image: url }, + (message, iv) => { + dispatch({ + type: 'refresh', + key: url, + progress: { + type: 'success', + content: message, + internal: false, + through: [], + iv: encodeArrayBuffer(iv), + }, + }) + }, + current.decryptedReport, + signal.signal, + ) }) return () => signal.abort() - }, [ - current.iv, - deconstructedPayload.ok, - (deconstructedPayload.val as Payload)?.encryptedText, - postBy.toText(), - postMetadataImages.join(), - whoAmI.toText(), - ]) - - // pass 2: - // decrypt rest attachments which depend on post content - // const decryptedPostContent = progress.find((p) => p.key === postContent) - // useEffect(() => { - // if (decryptedPostContent?.progress.type !== 'success') return - // // TODO: - // // decrypt shuffled image here - // }, [decryptedPostContent]) + }, [deconstructedPayload.ok, postBy.toText(), postMetadataImages.join(), whoAmI.toText()]) // pass 3: // invoke callback @@ -218,3 +211,34 @@ export function DecryptPost(props: DecryptPostProps) { } } } + +async function makeProgress( + postURL: string | undefined, + authorHint: ProfileIdentifier, + currentProfile: ProfileIdentifier, + payload: SocialNetworkEncodedPayload, + done: (message: TypedMessage, iv: Uint8Array) => void, + reporter: PostContext['decryptedReport'], + signal: AbortSignal, +) { + const context: DecryptionContext = { + postURL, + authorHint, + currentProfile, + currentSocialNetwork: activatedSocialNetworkUI.encryptionNetwork, + } + let iv: Uint8Array | undefined + for await (const progress of ServicesWithProgress.decryptionWithSocialNetworkDecoding(payload, context)) { + if (signal.aborted) return + if (progress.type === DecryptProgressKind.Success) { + done(progress.content, iv || new Uint8Array()) + } else if (progress.type === DecryptProgressKind.Info) { + iv ??= progress.iv + if (progress.ownersKeyEncrypted) + reporter({ ownersAESKeyEncrypted: encodeArrayBuffer(progress.ownersKeyEncrypted) }) + if (progress.iv) reporter({ iv: encodeArrayBuffer(progress.iv) }) + if (progress.version) reporter({ version: progress.version }) + if (typeof progress.publicShared === 'boolean') reporter({ sharedPublic: Some(progress.publicShared) }) + } + } +} diff --git a/packages/mask/src/components/InjectedComponents/PostInspector.tsx b/packages/mask/src/components/InjectedComponents/PostInspector.tsx index a40101905e92..065cfa390927 100644 --- a/packages/mask/src/components/InjectedComponents/PostInspector.tsx +++ b/packages/mask/src/components/InjectedComponents/PostInspector.tsx @@ -2,7 +2,7 @@ import { useState, useEffect } from 'react' import { useAsync } from 'react-use' import { DecryptPost } from './DecryptedPost/DecryptedPost' import Services from '../../extension/service' -import { ProfileIdentifier, type PayloadAlpha40_Or_Alpha39, type PayloadAlpha38 } from '@masknet/shared-base' +import { ProfileIdentifier } from '@masknet/shared-base' import type { TypedMessageTuple } from '@masknet/typed-message' import type { Profile } from '../../database' import { useCurrentIdentity, useFriendsList } from '../DataSource/useActivatedUI' @@ -28,7 +28,9 @@ export interface PostInspectorProps { export function PostInspector(props: PostInspectorProps) { const postBy = usePostInfoDetails.author() const encryptedPost = usePostInfoDetails.containingMaskPayload() - const decryptedPayloadForImage = usePostInfoDetails.decryptedPayloadForImage() + const ownersKeyEncrypted = usePostInfoDetails.ownersKeyEncrypted() + const version = usePostInfoDetails.version() + const iv = usePostInfoDetails.iv() const postImages = usePostInfoDetails.postMetadataImages() const isDebugging = useValueRef(debugModeSetting) const whoAmI = useCurrentIdentity() @@ -48,21 +50,14 @@ export function PostInspector(props: PostInspectorProps) { { - const val = (postImages ? decryptedPayloadForImage : encryptedPost.val) as - | PayloadAlpha40_Or_Alpha39 - | PayloadAlpha38 - - const { iv, version } = val - const ownersAESKeyEncrypted = - val.version === -38 ? val.AESKeyEncrypted : val.ownersAESKeyEncrypted - setAlreadySelectedPreviously(alreadySelectedPreviously.concat(profile)) return Services.Crypto.appendShareTarget( version, - ownersAESKeyEncrypted, + ownersKeyEncrypted, iv, profile.map((x) => x.identifier), whoAmI!.identifier, diff --git a/packages/mask/src/extension/background-script/CryptoServices/decryptFrom.ts b/packages/mask/src/extension/background-script/CryptoServices/decryptFrom.ts index 73cbdaac4f9b..d2d8a152c185 100644 --- a/packages/mask/src/extension/background-script/CryptoServices/decryptFrom.ts +++ b/packages/mask/src/extension/background-script/CryptoServices/decryptFrom.ts @@ -30,7 +30,6 @@ type Progress = ( | { progress: 'finding_person_public_key' | 'finding_post_key' | 'init' | 'decode_post' } | { progress: 'intermediate_success'; data: Success } | { progress: 'iv_decrypted'; iv: string } - | { progress: 'payload_decrypted'; decryptedPayloadForImage: Payload } ) & { type: 'progress' /** if this is true, this progress should not cause UI change. */ @@ -40,7 +39,6 @@ type SuccessThrough = 'author_key_not_found' | 'post_key_cached' | 'normal_decry type Success = { type: 'success' iv: string - decryptedPayloadForImage: Payload content: TypedMessage through: SuccessThrough[] internal: boolean @@ -57,17 +55,11 @@ type ReturnOfDecryptPostContentWithProgress = AsyncGenerator() const makeSuccessResultF = - ( - cacheKey: string, - iv: string, - decryptedPayloadForImage: Payload, - cryptoProvider: typeof cryptoProviderTable[keyof typeof cryptoProviderTable], - ) => + (cacheKey: string, iv: string, cryptoProvider: typeof cryptoProviderTable[keyof typeof cryptoProviderTable]) => (rawEncryptedContent: string, through: Success['through']): Success => { const success: Success = { through, iv, - decryptedPayloadForImage, content: cryptoProvider.typedMessageParse(rawEncryptedContent), type: 'success', internal: false, @@ -142,10 +134,9 @@ async function* decryptFromPayloadWithProgress_raw( if (version === -40 || version === -39 || version === -38) { const { encryptedText, iv, version } = data const cryptoProvider = cryptoProviderTable[version] - const makeSuccessResult = makeSuccessResultF(cacheKey, iv, data, cryptoProvider) + const makeSuccessResult = makeSuccessResultF(cacheKey, iv, cryptoProvider) const ownersAESKeyEncrypted = data.version === -38 ? data.AESKeyEncrypted : data.ownersAESKeyEncrypted - yield { type: 'progress', progress: 'payload_decrypted', decryptedPayloadForImage: data, internal: true } yield { type: 'progress', progress: 'iv_decrypted', iv: iv, internal: true } // ? Early emit the cache. const [cachedPostResult, setPostCache] = await decryptFromCache(data, author, discoverURL) diff --git a/packages/mask/src/extension/service-generator.ts b/packages/mask/src/extension/service-generator.ts index d49953d0cdd9..42f79befb178 100644 --- a/packages/mask/src/extension/service-generator.ts +++ b/packages/mask/src/extension/service-generator.ts @@ -1 +1,2 @@ export { decryptFromText, decryptFromImageUrl } from './background-script/CryptoServices/decryptFrom' +export { decryptionWithSocialNetworkDecoding } from '../../background/services/crypto' diff --git a/packages/mask/src/social-network-adaptor/browser-action/index.ts b/packages/mask/src/social-network-adaptor/browser-action/index.ts index 8245c74a4e6d..5e4106d6053d 100644 --- a/packages/mask/src/social-network-adaptor/browser-action/index.ts +++ b/packages/mask/src/social-network-adaptor/browser-action/index.ts @@ -1,10 +1,11 @@ import { defineSocialNetworkUI, definedSocialNetworkUIs, SocialNetworkUI, SocialNetwork } from '../../social-network' import { isEnvironment, Environment, ValueRef } from '@dimensiondev/holoflows-kit' import { IdentifierMap } from '@masknet/shared-base' +import { SocialNetworkEnum } from '@masknet/encryption' const base: SocialNetwork.Base = { networkIdentifier: 'localhost', - name: '', + encryptionNetwork: SocialNetworkEnum.Unknown, declarativePermissions: { origins: [] }, shouldActivate(location) { return isEnvironment(Environment.ManifestAction) diff --git a/packages/mask/src/social-network-adaptor/facebook.com/base.ts b/packages/mask/src/social-network-adaptor/facebook.com/base.ts index 6335b60223a9..9ef3fa4aa1d9 100644 --- a/packages/mask/src/social-network-adaptor/facebook.com/base.ts +++ b/packages/mask/src/social-network-adaptor/facebook.com/base.ts @@ -1,3 +1,4 @@ +import { SocialNetworkEnum } from '@masknet/encryption' import type { SocialNetwork, SocialNetworkWorker } from '../../social-network/types' const origins = ['https://www.facebook.com/*', 'https://m.facebook.com/*', 'https://facebook.com/*'] @@ -5,7 +6,7 @@ const origins = ['https://www.facebook.com/*', 'https://m.facebook.com/*', 'http export const FACEBOOK_ID = 'facebook.com' export const facebookBase: SocialNetwork.Base = { networkIdentifier: FACEBOOK_ID, - name: 'facebook', + encryptionNetwork: SocialNetworkEnum.Facebook, declarativePermissions: { origins }, shouldActivate(location) { return location.hostname.endsWith(FACEBOOK_ID) diff --git a/packages/mask/src/social-network-adaptor/instagram.com/base.ts b/packages/mask/src/social-network-adaptor/instagram.com/base.ts index 8220422ce186..54bbad8276e2 100644 --- a/packages/mask/src/social-network-adaptor/instagram.com/base.ts +++ b/packages/mask/src/social-network-adaptor/instagram.com/base.ts @@ -1,10 +1,11 @@ +import { SocialNetworkEnum } from '@masknet/encryption' import type { SocialNetwork, SocialNetworkWorker } from '../../social-network/types' const id = 'instagram.com' const origins = ['https://www.instagram.com/*', 'https://m.instagram.com/*', 'https://instagram.com/*'] export const instagramBase: SocialNetwork.Base = { networkIdentifier: id, - name: 'instagram', + encryptionNetwork: SocialNetworkEnum.Instagram, declarativePermissions: { origins }, shouldActivate(location) { return location.host.endsWith(id) diff --git a/packages/mask/src/social-network-adaptor/minds.com/base.ts b/packages/mask/src/social-network-adaptor/minds.com/base.ts index d68eef969776..e827f3b6372b 100644 --- a/packages/mask/src/social-network-adaptor/minds.com/base.ts +++ b/packages/mask/src/social-network-adaptor/minds.com/base.ts @@ -1,10 +1,11 @@ +import { SocialNetworkEnum } from '@masknet/encryption' import type { SocialNetwork, SocialNetworkWorker } from '../../social-network/types' export const MINDS_ID = 'minds.com' const origins = ['https://www.minds.com/*', 'https://minds.com/*', 'https://cdn.minds.com/*'] export const mindsBase: SocialNetwork.Base = { networkIdentifier: MINDS_ID, - name: 'minds', + encryptionNetwork: SocialNetworkEnum.Minds, declarativePermissions: { origins }, shouldActivate(location) { return location.hostname.endsWith('minds.com') diff --git a/packages/mask/src/social-network-adaptor/opensea.io/base.ts b/packages/mask/src/social-network-adaptor/opensea.io/base.ts index 38ea7dd27a48..6757cb5464da 100644 --- a/packages/mask/src/social-network-adaptor/opensea.io/base.ts +++ b/packages/mask/src/social-network-adaptor/opensea.io/base.ts @@ -1,10 +1,11 @@ +import { SocialNetworkEnum } from '@masknet/encryption' import type { SocialNetwork, SocialNetworkWorker } from '../../social-network/types' const OPENSEA_ID = 'opensea.io' const origins = ['https://opensea.io/*'] export const openseaBase: SocialNetwork.Base = { networkIdentifier: OPENSEA_ID, - name: 'opensea', + encryptionNetwork: SocialNetworkEnum.Unknown, declarativePermissions: { origins }, shouldActivate(location) { return location.host.endsWith(OPENSEA_ID) diff --git a/packages/mask/src/social-network-adaptor/twitter.com/base.ts b/packages/mask/src/social-network-adaptor/twitter.com/base.ts index a85a61db85cd..6f6a912ac250 100644 --- a/packages/mask/src/social-network-adaptor/twitter.com/base.ts +++ b/packages/mask/src/social-network-adaptor/twitter.com/base.ts @@ -1,10 +1,11 @@ +import { SocialNetworkEnum } from '@masknet/encryption' import type { SocialNetwork, SocialNetworkWorker } from '../../social-network/types' const id = 'twitter.com' const origins = ['https://mobile.twitter.com/*', 'https://twitter.com/*'] export const twitterBase: SocialNetwork.Base = { networkIdentifier: id, - name: 'twitter', + encryptionNetwork: SocialNetworkEnum.Twitter, declarativePermissions: { origins }, shouldActivate(location) { return location.hostname.endsWith('twitter.com') diff --git a/packages/mask/src/social-network/types.ts b/packages/mask/src/social-network/types.ts index ab4172151655..912d56885522 100644 --- a/packages/mask/src/social-network/types.ts +++ b/packages/mask/src/social-network/types.ts @@ -1,5 +1,5 @@ import type { ValueRef } from '@dimensiondev/holoflows-kit' -import type { GrayscaleAlgorithm } from '@masknet/encryption' +import type { GrayscaleAlgorithm, SocialNetworkEnum } from '@masknet/encryption' import type { PostInfo } from '@masknet/plugin-infra' import type { Identifier, @@ -55,7 +55,7 @@ export namespace SocialNetwork { * !!! THIS SHOULD NOT BE USED TO CONSTRUCT A NEW ProfileIdentifier !!! */ networkIdentifier: string - name: string + encryptionNetwork: SocialNetworkEnum /** * This field _will_ be overwritten by SocialNetworkUI.permissions */ diff --git a/packages/mask/src/social-network/ui.ts b/packages/mask/src/social-network/ui.ts index f00be8ef66cd..16492c39db1d 100644 --- a/packages/mask/src/social-network/ui.ts +++ b/packages/mask/src/social-network/ui.ts @@ -15,6 +15,7 @@ import { createPluginHost } from '../plugin-infra/host' import { definedSocialNetworkUIs } from './define' import { setupShadowRootPortal, MaskMessages } from '../utils' import { delay, waitDocumentReadyState } from '@dimensiondev/kit' +import { SocialNetworkEnum } from '@masknet/encryption' const definedSocialNetworkUIsResolved = new Map() export let activatedSocialNetworkUI: SocialNetworkUI.Definition = { @@ -31,7 +32,7 @@ export let activatedSocialNetworkUI: SocialNetworkUI.Definition = { }, injection: {}, networkIdentifier: 'localhost', - name: '', + encryptionNetwork: SocialNetworkEnum.Unknown, shouldActivate: () => false, utils: { createPostContext: null! }, notReadyForProduction: true, diff --git a/packages/mask/src/social-network/utils/create-post-context.ts b/packages/mask/src/social-network/utils/create-post-context.ts index f838e01279c0..8e6561790e5f 100644 --- a/packages/mask/src/social-network/utils/create-post-context.ts +++ b/packages/mask/src/social-network/utils/create-post-context.ts @@ -23,6 +23,7 @@ import { Err, Result } from 'ts-results' import type { Subscription } from 'use-subscription' import { activatedSocialNetworkUI } from '../' import { resolveFacebookLink } from '../../social-network-adaptor/facebook.com/utils/resolveFacebookLink' +import type { SupportedPayloadVersions } from '@masknet/encryption' export function createSNSAdaptorSpecializedPostContext(create: PostContextSNSActions) { return function createPostContext(opt: PostContextCreation): PostContext { @@ -104,8 +105,15 @@ export function createSNSAdaptorSpecializedPostContext(create: PostContextSNSAct return () => void [a(), b()] }, }) + const iv = new ValueRef(null) + const isPublicShared = new ValueRef(undefined) + const ownersAESKeyEncrypted = new ValueRef(undefined) + const version = new ValueRef(undefined) return { - ...author, + author: author.author, + avatarURL: author.avatarURL, + nickname: author.nickname, + snsID: author.snsID, get rootNode() { return opt.rootElement.realCurrent @@ -139,15 +147,16 @@ export function createSNSAdaptorSpecializedPostContext(create: PostContextSNSAct postContent: SubscriptionFromValueRef(postContent), containingMaskPayload: SubscriptionFromValueRef(postPayload), - decryptedPayloadForImage: new ValueRef(null), - iv: new ValueRef(null), - publicShared: debug({ - getCurrentValue: () => - postPayload.value - .map((val) => val.version === -38 && val.sharedPublic) - .unwrapOr(undefined), - subscribe: (sub) => postPayload.addListener(sub), - }), + iv, + publicShared: SubscriptionFromValueRef(isPublicShared), + ownersKeyEncrypted: SubscriptionFromValueRef(ownersAESKeyEncrypted), + version: SubscriptionFromValueRef(version), + decryptedReport(opts) { + if (opts.iv) iv.value = opts.iv + if (opts.sharedPublic?.some) isPublicShared.value = opts.sharedPublic.val + if (opts.ownersAESKeyEncrypted) ownersAESKeyEncrypted.value = opts.ownersAESKeyEncrypted + if (opts.version) version.value = opts.version + }, } } } diff --git a/packages/plugin-infra/package.json b/packages/plugin-infra/package.json index 38d8c1b95f87..577947633bfb 100644 --- a/packages/plugin-infra/package.json +++ b/packages/plugin-infra/package.json @@ -7,6 +7,7 @@ "scripts": {}, "dependencies": { "@dimensiondev/holoflows-kit": "^0.9.0-20210902104757-7c3d0d0", + "@masknet/encryption": "workspace:*", "@masknet/shared": "workspace:*", "@masknet/shared-base": "workspace:*", "@masknet/theme": "workspace:*", diff --git a/packages/plugin-infra/src/PostContext.ts b/packages/plugin-infra/src/PostContext.ts index 4f9aff400bf8..c5cefaa8a883 100644 --- a/packages/plugin-infra/src/PostContext.ts +++ b/packages/plugin-infra/src/PostContext.ts @@ -8,9 +8,10 @@ import { } from '@masknet/shared-base' import type { TypedMessageTuple } from '@masknet/typed-message' import { ValueRef, LiveSelector, DOMProxy } from '@dimensiondev/holoflows-kit' -import type { Result } from 'ts-results' +import type { Result, Some } from 'ts-results' import { createContext, createElement, memo, useContext } from 'react' import { Subscription, useSubscription } from 'use-subscription' +import type { SupportedPayloadVersions } from '@masknet/encryption' export interface PostContextSNSActions { /** Parse payload into Payload */ payloadParser(raw: string): Result @@ -77,14 +78,22 @@ export interface PostContext extends PostContextAuthor { // #endregion // #region Post payload discovered in the rawMessage readonly containingMaskPayload: Subscription> - /** @deprecated Use postPayload instead */ - readonly decryptedPayloadForImage: ValueRef // TODO: should be a Subscription readonly iv: ValueRef /** * undefined => payload not found */ readonly publicShared: Subscription + /** @deprecated */ + readonly ownersKeyEncrypted: Subscription + /** @deprecated */ + readonly version: Subscription + decryptedReport(content: { + sharedPublic?: Some + iv?: string + ownersAESKeyEncrypted?: string + version?: SupportedPayloadVersions + }): void // #endregion } export type PostInfo = PostContext @@ -136,11 +145,3 @@ function isSubscription(x: any): x is Subscription { Boolean((x as Subscription).getCurrentValue && (x as Subscription).subscribe) ) } - -export function usePostInfoSharedPublic(): boolean { - const info = usePostInfoDetails.containingMaskPayload() - if (info.err) return false - const payload = info.val - if (payload.version !== -38) return false - return !!payload.sharedPublic -} diff --git a/packages/plugin-infra/tsconfig.json b/packages/plugin-infra/tsconfig.json index d754a36a1237..d035ba3046bd 100644 --- a/packages/plugin-infra/tsconfig.json +++ b/packages/plugin-infra/tsconfig.json @@ -11,7 +11,8 @@ { "path": "../shared-base" }, { "path": "../theme" }, { "path": "../web3-shared/base" }, - { "path": "../typed-message/base" } + { "path": "../typed-message/base" }, + { "path": "../encryption" } ], "ts-node": { "swc": true, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ac58c06ccf23..eb3f8f95f500 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -582,6 +582,7 @@ importers: packages/plugin-infra: specifiers: '@dimensiondev/holoflows-kit': ^0.9.0-20210902104757-7c3d0d0 + '@masknet/encryption': workspace:* '@masknet/shared': workspace:* '@masknet/shared-base': workspace:* '@masknet/theme': workspace:* @@ -597,6 +598,7 @@ importers: use-subscription: ^1.5.1 dependencies: '@dimensiondev/holoflows-kit': 0.9.0-20210902104757-7c3d0d0 + '@masknet/encryption': link:../encryption '@masknet/shared': link:../shared '@masknet/shared-base': link:../shared-base '@masknet/theme': link:../theme From e5a923fe658b56c605b83370284d36cf4959e1d1 Mon Sep 17 00:00:00 2001 From: Jack Works Date: Mon, 14 Mar 2022 14:05:09 +0800 Subject: [PATCH 3/8] refactor: remove unused code --- .../DecryptedPost/DecryptPostFailed.tsx | 5 - .../DecryptedPost/DecryptedPost.tsx | 2 - .../InjectedComponents/PostComments.tsx | 8 +- packages/mask/src/crypto/crypto-alpha-39.ts | 4 +- packages/mask/src/crypto/crypto-alpha-40.ts | 69 --- .../CryptoServices/decryptFrom.ts | 415 +----------------- .../CryptoServices/verifyOthersProve.ts | 29 -- .../mask/src/extension/service-generator.ts | 1 - .../twitter.com/shared.ts | 8 - .../defaults/inject/CommentBox.tsx | 7 +- packages/mask/src/social-network/types.ts | 2 - .../utils/text-payload-worker.ts | 10 - packages/mask/src/utils/constants.ts | 3 - packages/mask/src/utils/index.ts | 1 - .../type-transform/asyncIteratorHelpers.ts | 58 --- 15 files changed, 18 insertions(+), 604 deletions(-) delete mode 100644 packages/mask/src/extension/background-script/CryptoServices/verifyOthersProve.ts delete mode 100644 packages/mask/src/utils/constants.ts diff --git a/packages/mask/src/components/InjectedComponents/DecryptedPost/DecryptPostFailed.tsx b/packages/mask/src/components/InjectedComponents/DecryptedPost/DecryptPostFailed.tsx index 860f5b03a5a6..87451cb073f7 100644 --- a/packages/mask/src/components/InjectedComponents/DecryptedPost/DecryptPostFailed.tsx +++ b/packages/mask/src/components/InjectedComponents/DecryptedPost/DecryptPostFailed.tsx @@ -1,10 +1,8 @@ import { memo } from 'react' import { useI18N } from '../../../utils' import { AdditionalContent } from '../AdditionalPostContent' -import { DecryptFailedReason } from '../../../utils/constants' import type { ProfileIdentifier } from '@masknet/shared-base' import { wrapAuthorDifferentMessage } from './authorDifferentMessage' -import MaskPostExtraInfoWrapper from '../../../plugins/MaskPluginWrapper' export interface DecryptPostFailedProps { error: Error @@ -17,9 +15,6 @@ export const DecryptPostFailed = memo(function DecryptPostFailed(props: DecryptP const { author, postedBy, error } = props const { t } = useI18N() - if (error?.message === DecryptFailedReason.MyCryptoKeyNotFound) { - return - } return ( x.iv).unwrapOr(iv) const dec = useAsync(async () => { const decryptedText = extractTextFromTypedMessage(postContent).unwrap() - if (!postIV || !decryptedText) throw new Error('Decrypt comment failed') - const result = await Services.Crypto.decryptComment(postIV, decryptedText, comment) + if (!iv || !decryptedText) throw new Error('Decrypt comment failed') + const result = await Services.Crypto.decryptComment(iv, decryptedText, comment) if (result === null) throw new Error('Decrypt result empty') return result - }, [postIV, postContent, comment]) + }, [iv, postContent, comment]) useEffect(() => void (dec.value && needZip()), [dec.value, needZip]) if (dec.value) return {dec.value} diff --git a/packages/mask/src/crypto/crypto-alpha-39.ts b/packages/mask/src/crypto/crypto-alpha-39.ts index 58f8bfcddb62..924e53c6d36e 100644 --- a/packages/mask/src/crypto/crypto-alpha-39.ts +++ b/packages/mask/src/crypto/crypto-alpha-39.ts @@ -79,8 +79,8 @@ export async function encrypt1ToN(info: { } } -export { encrypt1To1, decryptMessage1To1 } from './crypto-alpha-40' -export { decryptMessage1ToNByOther, decryptMessage1ToNByMyself, extractAESKeyInMessage } from './crypto-alpha-40' +export { encrypt1To1 } from './crypto-alpha-40' +export { extractAESKeyInMessage } from './crypto-alpha-40' export { encryptWithAES, decryptWithAES } from './crypto-alpha-40' export { encryptComment, decryptComment } from './crypto-alpha-40' export { typedMessageStringify, typedMessageParse } from './crypto-alpha-40' diff --git a/packages/mask/src/crypto/crypto-alpha-40.ts b/packages/mask/src/crypto/crypto-alpha-40.ts index b9183f0bf5da..316a1f62560f 100644 --- a/packages/mask/src/crypto/crypto-alpha-40.ts +++ b/packages/mask/src/crypto/crypto-alpha-40.ts @@ -11,7 +11,6 @@ import { concatArrayBuffer, memoizePromise, } from '@dimensiondev/kit' -import { i18n } from '../../shared-ui/locales_legacy' import { CryptoWorker } from '../modules/workers' import type { EC_Private_JsonWebKey, EC_Public_JsonWebKey, AESJsonWebKey } from '@masknet/shared-base' import { makeTypedMessageText } from '@masknet/typed-message' @@ -165,58 +164,6 @@ export async function encrypt1ToN(info: { } // #endregion // #region decrypt text -/** - * Decrypt 1 to 1 - */ -export async function decryptMessage1To1(info: { - version: -40 | -39 | -38 - encryptedContent: string | ArrayBuffer - salt: string | ArrayBuffer - /** Your private key */ - privateKeyECDH: EC_Private_JsonWebKey - /** If you are the author, this should be the receiver's public key. - * Otherwise, this should be the author's public key */ - anotherPublicKeyECDH: EC_Public_JsonWebKey -}): Promise { - const { anotherPublicKeyECDH, version, salt, encryptedContent, privateKeyECDH } = info - const encrypted = typeof encryptedContent === 'string' ? decodeArrayBuffer(encryptedContent) : encryptedContent - - const { iv, key } = await deriveAESKey(privateKeyECDH, anotherPublicKeyECDH, salt) - return CryptoWorker.decrypt_aes_gcm(key, iv, encrypted) -} -/** - * Decrypt 1 to N message that send by other - */ -export async function decryptMessage1ToNByOther(info: { - version: -40 | -39 | -38 - encryptedContent: string | ArrayBuffer - privateKeyECDH: EC_Private_JsonWebKey - authorsPublicKeyECDH: EC_Public_JsonWebKey - AESKeyEncrypted: PublishedAESKey | PublishedAESKey[] - iv: ArrayBuffer | string -}): Promise<[ArrayBuffer, AESJsonWebKey]> { - const { encryptedContent, privateKeyECDH, authorsPublicKeyECDH, iv } = info - const AESKeyEncrypted = Array.isArray(info.AESKeyEncrypted) ? info.AESKeyEncrypted : [info.AESKeyEncrypted] - - let resolvedAESKey: string | null = null - await Promise.all( - AESKeyEncrypted.map(async (key) => { - try { - const result = await decryptMessage1To1({ - version: -40, - salt: key.salt, - encryptedContent: key.encryptedKey, - anotherPublicKeyECDH: authorsPublicKeyECDH, - privateKeyECDH: privateKeyECDH, - }) - resolvedAESKey = decodeText(result) - } catch {} - }), - ) - if (resolvedAESKey === null) throw new Error(i18n.t('service_not_share_target')) - const aesKey: AESJsonWebKey = JSON.parse(resolvedAESKey) - return [await decryptWithAES({ aesKey, iv, encrypted: encryptedContent }), aesKey] -} export async function extractAESKeyInMessage( version: -40 | -39 | -38, encodedEncryptedKey: string | ArrayBuffer, @@ -231,22 +178,6 @@ export async function extractAESKeyInMessage( ) return decryptedAESKeyJWK } -/** - * Decrypt 1 to N message that send by myself - */ -export async function decryptMessage1ToNByMyself(info: { - version: -40 | -39 | -38 - encryptedContent: string | ArrayBuffer - /** This should be included in the message */ - encryptedAESKey: string | ArrayBuffer - myLocalKey: AESJsonWebKey - iv: string | ArrayBuffer -}): Promise<[ArrayBuffer, AESJsonWebKey]> { - const { encryptedContent, myLocalKey, iv, encryptedAESKey } = info - const decryptedAESKey = await extractAESKeyInMessage(-40, encryptedAESKey, iv, myLocalKey) - const post = await decryptWithAES({ aesKey: decryptedAESKey, encrypted: encryptedContent, iv }) - return [post, decryptedAESKey] -} /** * Decrypt the content encrypted by AES */ diff --git a/packages/mask/src/extension/background-script/CryptoServices/decryptFrom.ts b/packages/mask/src/extension/background-script/CryptoServices/decryptFrom.ts index d2d8a152c185..a2f1fddef97b 100644 --- a/packages/mask/src/extension/background-script/CryptoServices/decryptFrom.ts +++ b/packages/mask/src/extension/background-script/CryptoServices/decryptFrom.ts @@ -1,417 +1,22 @@ -import * as Alpha40 from '../../../crypto/crypto-alpha-40' -import * as Alpha39 from '../../../crypto/crypto-alpha-39' -import { decodeArrayBuffer, decodeText, encodeArrayBuffer, delay, abortSignalTimeout } from '@dimensiondev/kit' -import { deconstructPayload } from '../../../utils/type-transform/Payload' -import { i18n } from '../../../../shared-ui/locales_legacy' -import { queryPersonaRecord, queryLocalKey } from '../../../database' -import { ProfileIdentifier, PostIVIdentifier, EC_Public_CryptoKey } from '@masknet/shared-base' -import { PostRecord, queryPostDB, updatePostDB } from '../../../../background/database/post' -import { getNetworkWorker, getNetworkWorkerUninitialized } from '../../../social-network/worker' -import { cryptoProviderTable } from './cryptoProviderTable' -import type { PersonaRecord } from '../../../../background/database/persona/db' -import { verifyOthersProve } from './verifyOthersProve' -import { publicSharedAESKey } from '../../../crypto/crypto-alpha-38' -import { DecryptFailedReason } from '../../../utils/constants' -import { asyncIteratorWithResult, memorizeAsyncGenerator } from '../../../utils/type-transform/asyncIteratorHelpers' -import { DecryptStaticECDH_PostKey, steganographyDecodeImage } from '@masknet/encryption' import type { TypedMessage } from '@masknet/typed-message' -import type { AESJsonWebKey, Payload } from '@masknet/shared-base' -import stringify from 'json-stable-stringify' -import { MaskMessages } from '../../../utils/messages' -import { Err, Ok, Result } from 'ts-results' -import { decodeTextPayloadWorker } from '../../../social-network/utils/text-payload-worker' -import { steganographyDownloadImage } from './utils' -import { - GUN_queryPostKey_version39Or38, - GUN_queryPostKey_version40, -} from '../../../../background/network/gun/encryption/queryPostKey' -type Progress = ( - | { progress: 'finding_person_public_key' | 'finding_post_key' | 'init' | 'decode_post' } - | { progress: 'intermediate_success'; data: Success } - | { progress: 'iv_decrypted'; iv: string } -) & { - type: 'progress' - /** if this is true, this progress should not cause UI change. */ - internal: boolean -} -type SuccessThrough = 'author_key_not_found' | 'post_key_cached' | 'normal_decrypted' -type Success = { +export type SuccessDecryption = { type: 'success' iv: string content: TypedMessage - through: SuccessThrough[] internal: boolean } -type Failure = { +export type FailureDecryption = { error: string type: 'error' internal: boolean } -export type SuccessDecryption = Success -export type FailureDecryption = Failure -export type DecryptionProgress = Progress -type ReturnOfDecryptPostContentWithProgress = AsyncGenerator - -const successDecryptionCache = new Map() -const makeSuccessResultF = - (cacheKey: string, iv: string, cryptoProvider: typeof cryptoProviderTable[keyof typeof cryptoProviderTable]) => - (rawEncryptedContent: string, through: Success['through']): Success => { - const success: Success = { - through, - iv, - content: cryptoProvider.typedMessageParse(rawEncryptedContent), - type: 'success', - internal: false, - } - successDecryptionCache.set(cacheKey, success) - return success - } - -function makeProgress( - progress: Exclude | Success, - internal = false, -): Progress { - if (typeof progress === 'string') return { type: 'progress', progress, internal } - return { type: 'progress', progress: 'intermediate_success', data: progress, internal } -} -function makeError(error: string | Error, internal = false): Failure { - if (typeof error === 'string') return { type: 'error', error, internal } - return makeError(error.message, internal) -} -/** - * Decrypt message from a user - * @param post post - * @param author Post by - * @param authorNetworkHint When the author is unknown, the decryption (to public) won't die - * @param whoAmI My username - * @param publicShared Is this post public shared - * - * @description - * The decrypt process: - * - * ## Prepare - * a. if unknown payload, throw - * b. if unknown payload version, throw - * - * ## Decrypt for version -38, -39 and -40 - * a. read the cache `cachedPostResult` - * b. find author's public key (See: `findAuthorPublicKey` function) - * 0. if it is in the payload, use the key in the payload. - * 1. if there is cache, return the cache - * 2. if try N times but not finding the key, throw - * c. if there is cache, return the cache - * d. try to decrypt by `author` with `decryptAsAuthor` - * e. try to decrypt by `whoAmI` with `decryptAsAuthor` - * f. if `author` === `whoAmI`, throw - * g. find key for `whoAmI` on Gun - * h. try to decrypt by the key on Gun - * i. return a Promise - * 0. if version === -40, throws - * 1. listen to future new keys on Gun - * 2. try to decrypt with that key - */ -async function* decryptFromPayloadWithProgress_raw( - post: Payload, - author: ProfileIdentifier, - authorNetworkHint: string, - whoAmI: ProfileIdentifier, - discoverURL: string | undefined, -): ReturnOfDecryptPostContentWithProgress { - const cacheKey = stringify(post) - if (successDecryptionCache.has(cacheKey)) return successDecryptionCache.get(cacheKey)! - yield makeProgress('init') - - const authorNetworkWorker = Result.wrap(() => - getNetworkWorkerUninitialized(author.isUnknown ? authorNetworkHint : author.network), - ).andThen((x) => (x ? Ok(x) : Err(new Error('Worker not found')))) - if (authorNetworkWorker.err) return makeError(authorNetworkWorker.val as Error) - - const data = post - const { version } = data - const sharePublic = data.version === -38 ? data.sharedPublic ?? false : false - - if (version === -40 || version === -39 || version === -38) { - const { encryptedText, iv, version } = data - const cryptoProvider = cryptoProviderTable[version] - const makeSuccessResult = makeSuccessResultF(cacheKey, iv, cryptoProvider) - const ownersAESKeyEncrypted = data.version === -38 ? data.AESKeyEncrypted : data.ownersAESKeyEncrypted - - yield { type: 'progress', progress: 'iv_decrypted', iv: iv, internal: true } - // ? Early emit the cache. - const [cachedPostResult, setPostCache] = await decryptFromCache(data, author, discoverURL) - if (cachedPostResult) { - yield makeProgress(makeSuccessResult(cachedPostResult, ['post_key_cached'])) - } - - // ? If the author's key is in the payload, store it. - if (data.version === -38 && data.authorPublicKey && !author.isUnknown) { - await verifyOthersProve({ raw: data.authorPublicKey }, author).catch(console.error) - } - // ? Find author's public key. - let authorPersona!: PersonaRecord - for await (const _ of asyncIteratorWithResult(findAuthorPublicKey(author, !!cachedPostResult))) { - if (author.isUnknown) break - if (!_.done) { - yield _.value - continue - } - const result = _.value - if (result === 'out of chance') - return makeError(i18n.t('service_others_key_not_found', { name: author.userId })) - else if (result === 'use cache') - return makeSuccessResult(cachedPostResult!, ['author_key_not_found', 'post_key_cached']) - else authorPersona = result - } - - // ? Get my public & private key. - const queryWhoAmI = () => queryPersonaRecord(whoAmI) - const mine = await queryWhoAmI().then((x) => x || delay(1000).then(queryWhoAmI)) - if (!mine?.privateKey) return makeError(DecryptFailedReason.MyCryptoKeyNotFound) - - const { privateKey: minePrivate } = mine - const minePublic = crypto.subtle.importKey('jwk', mine.publicKey, { name: 'ECDH', namedCurve: 'K-256' }, true, [ - 'deriveKey', - 'deriveBits', - ]) as Promise - if (cachedPostResult) return makeSuccessResult(cachedPostResult, ['post_key_cached']) - - let lastError: unknown - /** - * ? try to decrypt as I am the author - * ? then try to decrypt as whoAmI - * ? then try to go through a normal decrypt process - */ - try { - const a = decryptAsAuthor(author) - const b = decryptAsAuthor(whoAmI) - // ! Don't remove the await - return await a.catch(() => b) - } catch (error) { - lastError = error - } - - if (author.equals(whoAmI)) { - // if the decryption process goes here, - // that means it is failed to decrypt by local identities. - // If remove this if block, Mask will search the key - // for the post even that post by myself. - if (lastError instanceof DOMException) return handleDOMException(lastError) - console.error(lastError) - return makeError(i18n.t('service_self_key_decryption_failed')) - } - - yield makeProgress('finding_post_key') - const aesKeyEncrypted: Array = [] - if (version === -40) { - const result = await GUN_queryPostKey_version40(new Uint8Array(decodeArrayBuffer(iv)), whoAmI.userId) - if (result === null) return makeError(i18n.t('service_not_share_target')) - aesKeyEncrypted.push(result) - } else if (version === -39 || version === -38) { - const collector = GUN_queryPostKey_version39Or38( - version, - new Uint8Array(decodeArrayBuffer(iv)), - await minePublic, - authorNetworkWorker.val.gunNetworkHint, - abortSignalTimeout(2000), - ) - try { - for await (const key of collector) aesKeyEncrypted.push(key) - } catch {} - } - // If we can decrypt with current info, just do it. - try { - // ! Do not remove the await here. - return await decryptWith(aesKeyEncrypted) - } catch (error) { - if (error instanceof Error && error.message === i18n.t('service_not_share_target')) { - console.debug(error) - // 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 Mask extension will detect and add you to recipients. - // ? after the auto-share with friends is done. - yield makeError(error) - } else { - return handleDOMException(error) - } - } - - // Failed, we have to wait for the future info from gun. - if (version === -40) return makeError(i18n.t('service_not_share_target')) - const subscription = GUN_queryPostKey_version39Or38( - version, - new Uint8Array(decodeArrayBuffer(iv)), - await minePublic, - authorNetworkWorker.val.gunNetworkHint, - new AbortController().signal, - ) - for await (const aes of subscription) { - console.log('New key received, trying', aes) - try { - return await decryptWith(aes) - } catch (error) { - console.debug(error) - } - } - return makeError(i18n.t('service_not_share_target')) - - function convertKey(key: DecryptStaticECDH_PostKey | DecryptStaticECDH_PostKey[]): Alpha40.PublishedAESKey[] { - if (!Array.isArray(key)) return convertKey([key]) - const result: Alpha40.PublishedAESKey[] = [] - for (const k of key) { - result.push({ - salt: encodeArrayBuffer(k.postKeyIV), - encryptedKey: encodeArrayBuffer(k.encryptedPostKey), - }) - } - return [] - } - async function decryptWith(key: DecryptStaticECDH_PostKey | DecryptStaticECDH_PostKey[]): Promise { - const [contentArrayBuffer, postAESKey] = await cryptoProvider.decryptMessage1ToNByOther({ - version, - AESKeyEncrypted: convertKey(key), - authorsPublicKeyECDH: authorPersona.publicKey, - encryptedContent: encryptedText, - privateKeyECDH: minePrivate!, - iv, - }) - - // Store the key to speed up next time decrypt - setPostCache(postAESKey) - const content = decodeText(contentArrayBuffer) - return makeSuccessResult(content, ['normal_decrypted']) - } - - async function decryptAsAuthor(authorIdentifier: ProfileIdentifier) { - const localKey = sharePublic ? publicSharedAESKey : await queryLocalKey(authorIdentifier) - if (!localKey) throw new Error(`Local key for identity ${authorIdentifier.toText()} not found`) - const [contentArrayBuffer, postAESKey] = await cryptoProvider.decryptMessage1ToNByMyself({ - version, - encryptedAESKey: ownersAESKeyEncrypted, - encryptedContent: encryptedText, - myLocalKey: localKey, - iv, - }) - // Store the key to speed up next time decrypt - setPostCache(postAESKey) - const content = decodeText(contentArrayBuffer) - return makeSuccessResult(content, ['normal_decrypted']) - } - } - return makeError(i18n.t('service_unknown_payload')) -} - -async function* decryptFromImageUrlWithProgress_raw( - url: string, - author: ProfileIdentifier, - authorNetworkHint: string, - whoAmI: ProfileIdentifier, - discoverURL: string | undefined, -): ReturnOfDecryptPostContentWithProgress { - if (successDecryptionCache.has(url)) return successDecryptionCache.get(url)! - yield makeProgress('decode_post', true) - const post = await steganographyDecodeImage(url, { - pass: author.toText(), - downloadImage: steganographyDownloadImage, - }) - if (!post.startsWith('\u{1F3BC}') && !/https:\/\/.+\..+\/(\?PostData_v\d=)?%20(.+)%40/.test(post)) - return makeError(i18n.t('service_decode_image_payload_failed'), true) - const worker = await Result.wrapAsync(() => getNetworkWorker(author)) - if (worker.err) return makeError(worker.val as Error) - const payload = deconstructPayload(post, await decodeTextPayloadWorker(author)) - if (payload.err) return makeError(payload.val) - return yield* decryptFromText(payload.val, author, authorNetworkHint, whoAmI, discoverURL) -} - -export const decryptFromText = memorizeAsyncGenerator( - decryptFromPayloadWithProgress_raw, - (encrypted, author, _, whoAmI) => - JSON.stringify([encrypted.iv, encrypted.encryptedText, author.toText(), whoAmI.toText()]), - 1000 * 30, -) - -export const decryptFromImageUrl = memorizeAsyncGenerator( - decryptFromImageUrlWithProgress_raw, - (url, author, _, whoAmI) => JSON.stringify([url, author.toText(), whoAmI.toText()]), - 1000 * 30, -) - -function handleDOMException(e: unknown) { - if (e instanceof DOMException) { - return makeError(i18n.t('service_decryption_failed')) - } else throw e -} -async function* findAuthorPublicKey( - by: ProfileIdentifier, - hasCache: boolean, - maxIteration = 10, -): AsyncGenerator { - let author = await queryPersonaRecord(by) - let iterations = 0 - while (!author?.publicKey) { - iterations += 1 - if (iterations < maxIteration) yield makeProgress('finding_person_public_key') - else return 'out of chance' as const - - author = await queryPersonaRecord(by).catch(() => null) - - if (!author?.publicKey) { - if (hasCache) return 'use cache' as const - const abort = new AbortController() - const databasePromise = new Promise((resolve, reject) => { - abort.signal.addEventListener('abort', () => { - undo() - reject() - }) - const undo = MaskMessages.events.profilesChanged.on((data) => { - for (const x of data) { - if (x.reason === 'delete') continue - if (x.of.equals(by)) { - undo() - resolve() - break - } - } - }) - }) - await Promise.race([databasePromise]) - .then(() => abort.abort()) - .catch(() => null) - } - } - if (author?.publicKey) return author - return 'out of chance' -} - -async function decryptFromCache(postPayload: Payload, by: ProfileIdentifier, discoverURL: string | undefined) { - const { encryptedText, iv, version } = postPayload - const cryptoProvider = version === -40 ? Alpha40 : Alpha39 - - const postIdentifier = new PostIVIdentifier(by.network, iv) - const cachedKey = await queryPostDB(postIdentifier) - if (cachedKey && !cachedKey.url && discoverURL) { - await updatePostDB({ identifier: postIdentifier, url: discoverURL }, 'append') - } - const setCache = (postAESKey: AESJsonWebKey) => { - const postUpdate: Partial & Pick = { - identifier: postIdentifier, - postCryptoKey: postAESKey, - postBy: by, - } - if (discoverURL) postUpdate.url = discoverURL - updatePostDB(postUpdate, 'append') - } - if (cachedKey?.postCryptoKey) { - try { - const result = decodeText( - await cryptoProvider.decryptWithAES({ - aesKey: cachedKey.postCryptoKey, - encrypted: encryptedText, - iv: iv, - }), - ) - return [result, setCache] as const - } catch {} - } - return [undefined, setCache] as const +export type DecryptionProgress = ( + | { progress: 'finding_person_public_key' | 'finding_post_key' | 'init' | 'decode_post' } + | { progress: 'intermediate_success'; data: SuccessDecryption } + | { progress: 'iv_decrypted'; iv: string } +) & { + type: 'progress' + /** if this is true, this progress should not cause UI change. */ + internal: boolean } diff --git a/packages/mask/src/extension/background-script/CryptoServices/verifyOthersProve.ts b/packages/mask/src/extension/background-script/CryptoServices/verifyOthersProve.ts deleted file mode 100644 index d866d3afdb85..000000000000 --- a/packages/mask/src/extension/background-script/CryptoServices/verifyOthersProve.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { type ProfileIdentifier, decompressSecp256k1Key, ECKeyIdentifierFromJsonWebKey } from '@masknet/shared-base' -import { decodePublicKeyWorker } from '../../../social-network/utils/text-payload-worker' -import { queryPersonaRecord } from '../../../database' -import { createProfileWithPersona } from '../../../../background/database/persona/helper' - -export async function verifyOthersProve(bio: string | { raw: string }, others: ProfileIdentifier): Promise { - const compressedX = typeof bio === 'string' ? (await decodePublicKeyWorker(others.network))(bio) : [bio.raw] - if (!compressedX) return false - const publicKey = compressedX - .map((x) => { - try { - return decompressSecp256k1Key(x) - } catch { - return null - } - }) - .filter((x) => x)[0] - if (!publicKey) return false - // TODO: use json schema / other ways to verify the JWK - // or - // throw new Error(i18n.t('service_key_parse_failed')) - // if privateKey, we should possibly not recreate it - const hasPrivate = - (await queryPersonaRecord(ECKeyIdentifierFromJsonWebKey(publicKey)))?.privateKey || - (await queryPersonaRecord(others))?.privateKey - if (!hasPrivate) await createProfileWithPersona(others, { connectionConfirmState: 'pending' }, { publicKey }) - // TODO: unhandled case: if the profile is connected but a different key. - return true -} diff --git a/packages/mask/src/extension/service-generator.ts b/packages/mask/src/extension/service-generator.ts index 42f79befb178..6f63d120f84e 100644 --- a/packages/mask/src/extension/service-generator.ts +++ b/packages/mask/src/extension/service-generator.ts @@ -1,2 +1 @@ -export { decryptFromText, decryptFromImageUrl } from './background-script/CryptoServices/decryptFrom' export { decryptionWithSocialNetworkDecoding } from '../../background/services/crypto' diff --git a/packages/mask/src/social-network-adaptor/twitter.com/shared.ts b/packages/mask/src/social-network-adaptor/twitter.com/shared.ts index 015ae96e54a3..dcb4ef53063f 100644 --- a/packages/mask/src/social-network-adaptor/twitter.com/shared.ts +++ b/packages/mask/src/social-network-adaptor/twitter.com/shared.ts @@ -16,14 +16,6 @@ export const twitterShared: SocialNetwork.Shared & SocialNetwork.Base = { getHomePage: () => 'https://twitter.com', getProfilePage: (userId) => `https://twitter.com/${userId}`, isValidUsername: usernameValidator, - publicKeyEncoding: { - encoder(text) { - return twitterEncoding.publicKeyEncoder(text) - }, - decoder(text) { - return twitterEncoding.publicKeyDecoder(text) - }, - }, textPayloadPostProcessor: { encoder(text) { return twitterEncoding.payloadEncoder(text) diff --git a/packages/mask/src/social-network/defaults/inject/CommentBox.tsx b/packages/mask/src/social-network/defaults/inject/CommentBox.tsx index 31e859c05f0e..94a25f51fee7 100644 --- a/packages/mask/src/social-network/defaults/inject/CommentBox.tsx +++ b/packages/mask/src/social-network/defaults/inject/CommentBox.tsx @@ -29,21 +29,20 @@ export const injectCommentBoxDefaultFactory = function ( ) { const CommentBoxUI = memo(function CommentBoxUI({ dom }: { dom: HTMLElement | null }) { const info = usePostInfo() - const payload = usePostInfoDetails.containingMaskPayload() const postContent = usePostInfoDetails.rawMessagePiped() const { classes } = useCustomStyles() const iv = usePostInfoDetails.iv() const props = additionPropsToCommentBox(classes) const onCallback = useCallback( async (content) => { - const postIV = iv || payload.unwrap().iv const decryptedText = extractTextFromTypedMessage(postContent).unwrap() - const encryptedComment = await Services.Crypto.encryptComment(postIV, decryptedText, content) + const encryptedComment = await Services.Crypto.encryptComment(iv!, decryptedText, content) onPasteToCommentBox(encryptedComment, info!, dom).catch(console.error) }, - [payload, postContent, info, dom, iv], + [postContent, info, dom, iv], ) + if (!iv) return null if (!postContent.items.length) return null return }) diff --git a/packages/mask/src/social-network/types.ts b/packages/mask/src/social-network/types.ts index 912d56885522..cd3376887413 100644 --- a/packages/mask/src/social-network/types.ts +++ b/packages/mask/src/social-network/types.ts @@ -37,8 +37,6 @@ export namespace SocialNetwork { getPostURL?(post: PostIdentifier): URL | null /** Is this username valid in this network */ isValidUsername?(username: string): boolean - /** How to encode/decode public keys when it is put in the bio. */ - publicKeyEncoding?: PayloadEncoding /** How to encode/decode text payload (e.g. make it into a link so it will be shortened by SNS). */ textPayloadPostProcessor?: PayloadEncoding /** Given a text, return a URL that will allow user to share this text */ diff --git a/packages/mask/src/social-network/utils/text-payload-worker.ts b/packages/mask/src/social-network/utils/text-payload-worker.ts index bdf1509e376b..13be0c5a6fd2 100644 --- a/packages/mask/src/social-network/utils/text-payload-worker.ts +++ b/packages/mask/src/social-network/utils/text-payload-worker.ts @@ -1,15 +1,5 @@ import { getNetworkWorker, NetworkWorkerQuery } from '../worker' -export async function decodePublicKeyWorker(query: NetworkWorkerQuery): Promise<(text: string) => string[]> { - const f = (await getNetworkWorker(query)).utils.publicKeyEncoding?.decoder - if (f) return (x) => f(x).concat(x) - return (x) => [x] -} export async function encodeTextPayloadWorker(query: NetworkWorkerQuery): Promise<(text: string) => string> { const f = (await getNetworkWorker(query)).utils.textPayloadPostProcessor?.encoder return (x) => f?.(x) || x } -export async function decodeTextPayloadWorker(query: NetworkWorkerQuery): Promise<(text: string) => string[]> { - const f = (await getNetworkWorker(query)).utils.textPayloadPostProcessor?.decoder - if (f) return (x) => f(x).concat(x) - return (x) => [x] -} diff --git a/packages/mask/src/utils/constants.ts b/packages/mask/src/utils/constants.ts deleted file mode 100644 index 090383f89115..000000000000 --- a/packages/mask/src/utils/constants.ts +++ /dev/null @@ -1,3 +0,0 @@ -export enum DecryptFailedReason { - MyCryptoKeyNotFound = 'MyCryptoKeyNotFound', -} diff --git a/packages/mask/src/utils/index.ts b/packages/mask/src/utils/index.ts index 8336323e4d8a..c6a009552f39 100644 --- a/packages/mask/src/utils/index.ts +++ b/packages/mask/src/utils/index.ts @@ -5,7 +5,6 @@ export * from './shadow-root' export * from './suspends' export * from './type-transform' export * from './comparer' -export * from './constants' export * from './dom' export * from './getTextUILength' export * from './i18n-next-ui' diff --git a/packages/mask/src/utils/type-transform/asyncIteratorHelpers.ts b/packages/mask/src/utils/type-transform/asyncIteratorHelpers.ts index 86b302c742f3..4cf584a10402 100644 --- a/packages/mask/src/utils/type-transform/asyncIteratorHelpers.ts +++ b/packages/mask/src/utils/type-transform/asyncIteratorHelpers.ts @@ -1,61 +1,3 @@ -export async function* asyncIteratorWithResult( - iter: AsyncIterator, - callback?: (promise: Promise>) => void, -) { - let yielded: IteratorResult - do { - const p = iter.next() - callback?.(p) - yielded = await p - yield yielded - } while (yielded.done === false) - return -} - -export function memorizeAsyncGenerator( - f: (...args: Args) => AsyncGenerator, - getKey: (...args: Args) => string, - expireAfter: number, -): typeof f { - type Iter = AsyncGenerator - type IterResult = Promise> - const iterCache = new Map() - const progressCache = new WeakMap() - return async function* memorizedAsyncGenerator(...args: Args) { - const key = getKey(...args) - const iter = iterCache.get(key) ?? f(...args) - iterCache.set(key, iter) - const process = progressCache.get(iter) - if (!process) { - setTimeout(() => iterCache.delete(key), expireAfter) - const arr: IterResult[] = [] - progressCache.set(iter, arr) - try { - for await (const _ of asyncIteratorWithResult(iter, (x) => arr.push(x))) { - if (_.done) return _.value - yield _.value - } - } catch { - iterCache.delete(key) - } - } else { - let index = 0 - while (index < process.length) { - const p = await process[index] - if (p.done) return p.value - yield p.value - index += 1 - } - } - // cache not working - for await (const _ of asyncIteratorWithResult(f(...args))) { - if (_.done) return _.value - yield _.value - } - throw new Error('Unreachable') - } -} - export async function asyncIteratorToArray(it: AsyncIterable) { const arr: T[] = [] for await (const x of it) arr.push(x) From 44016414238096e4f4845b9099096d0f8b0cdc0c Mon Sep 17 00:00:00 2001 From: Jack Works Date: Mon, 14 Mar 2022 16:49:42 +0800 Subject: [PATCH 4/8] fix: link not decrypted --- .../encryption/src/encryption/Decryption.ts | 3 -- .../src/encryption/DecryptionTypes.ts | 1 - .../DecryptedPost/DecryptedPost.tsx | 43 ++++++++++++++++--- 3 files changed, 38 insertions(+), 9 deletions(-) diff --git a/packages/encryption/src/encryption/Decryption.ts b/packages/encryption/src/encryption/Decryption.ts index d4d9543830a4..c4411ae8048f 100644 --- a/packages/encryption/src/encryption/Decryption.ts +++ b/packages/encryption/src/encryption/Decryption.ts @@ -17,8 +17,6 @@ export * from './DecryptionTypes' const ErrorReasons = DecryptError.Reasons type Version = PayloadParseResult.Payload['version'] export async function* decrypt(options: DecryptOptions, io: DecryptIO): AsyncIterableIterator { - yield progress(DecryptProgressKind.Started) - const { author: _author, encrypted: _encrypted, encryption: _encryption, version } = options.message const { authorPublicKey: _authorPublicKey } = options.message @@ -219,7 +217,6 @@ async function importAESKeyFromJWKFromTextEncoder(aes_raw: Uint8Array) { } function progress(kind: DecryptProgressKind.Success, rest: Omit): DecryptSuccess -function progress(kind: DecryptProgressKind.Started): DecryptProgress function progress(kind: DecryptProgressKind, rest?: object): DecryptProgress { return { type: kind, ...rest } as any } diff --git a/packages/encryption/src/encryption/DecryptionTypes.ts b/packages/encryption/src/encryption/DecryptionTypes.ts index b02f25eb5ab7..5d1e09bca5b4 100644 --- a/packages/encryption/src/encryption/DecryptionTypes.ts +++ b/packages/encryption/src/encryption/DecryptionTypes.ts @@ -113,7 +113,6 @@ export interface DecryptEphemeralECDH_PostKey extends DecryptStaticECDH_PostKey ephemeralPublicKeySignature?: Uint8Array } export enum DecryptProgressKind { - Started = 'started', Success = 'success', Error = 'error', Info = 'info', diff --git a/packages/mask/src/components/InjectedComponents/DecryptedPost/DecryptedPost.tsx b/packages/mask/src/components/InjectedComponents/DecryptedPost/DecryptedPost.tsx index cdea18f256e8..c1de208f81e3 100644 --- a/packages/mask/src/components/InjectedComponents/DecryptedPost/DecryptedPost.tsx +++ b/packages/mask/src/components/InjectedComponents/DecryptedPost/DecryptedPost.tsx @@ -19,10 +19,10 @@ import { DecryptPostSuccess, DecryptPostSuccessProps } from './DecryptedPostSucc import { DecryptPostAwaiting, DecryptPostAwaitingProps } from './DecryptPostAwaiting' import { DecryptPostFailed, DecryptPostFailedProps } from './DecryptPostFailed' import { usePostClaimedAuthor, usePostInfoDetails, usePostInfo } from '../../DataSource/usePostInfo' -import { delay, encodeArrayBuffer } from '@dimensiondev/kit' +import { delay, encodeArrayBuffer, safeUnreachable } from '@dimensiondev/kit' import { activatedSocialNetworkUI } from '../../../social-network' import type { DecryptionContext, SocialNetworkEncodedPayload } from '../../../../background/services/crypto/decryption' -import { DecryptProgressKind } from '@masknet/encryption' +import { DecryptIntermediateProgressKind, DecryptProgressKind } from '@masknet/encryption' import type { PostContext } from '@masknet/plugin-infra' import { Some } from 'ts-results' @@ -75,6 +75,7 @@ export function DecryptPost(props: DecryptPostProps) { const currentPostBy = usePostInfoDetails.author() const postBy = or(authorInPayload, currentPostBy) const postMetadataImages = usePostInfoDetails.postMetadataImages() + const mentionedLinks = usePostInfoDetails.mentionedLinks() const Success = props.successComponent || DecryptPostSuccess const Awaiting = props.waitingComponent || DecryptPostAwaiting const Failed = props.failedComponent || DecryptPostFailed @@ -101,12 +102,35 @@ export function DecryptPost(props: DecryptPostProps) { useEffect(() => { const signal = new AbortController() const postURL = current.url.getCurrentValue()?.toString() + const report = + (key: string): ReportProgress => + (kind, message) => { + if (kind === 'e2e') { + dispatch({ + type: 'refresh', + key, + progress: { type: 'progress', progress: 'finding_post_key', internal: false }, + }) + } else { + dispatch({ + type: 'refresh', + key, + progress: { type: 'error', error: message, internal: false }, + }) + } + } if (deconstructedPayload.ok) { makeProgress( postURL, postBy, whoAmI, - { type: 'text', text: extractTextFromTypedMessage(current.rawMessage.getCurrentValue()).unwrapOr('') }, + { + type: 'text', + text: + extractTextFromTypedMessage(current.rawMessage.getCurrentValue()).unwrapOr('') + + ' ' + + mentionedLinks.join(' '), + }, (message, iv) => { dispatch({ type: 'refresh', @@ -120,6 +144,7 @@ export function DecryptPost(props: DecryptPostProps) { }) }, current.decryptedReport, + report('text'), signal.signal, ) } @@ -143,11 +168,12 @@ export function DecryptPost(props: DecryptPostProps) { }) }, current.decryptedReport, + report(url), signal.signal, ) }) return () => signal.abort() - }, [deconstructedPayload.ok, postBy.toText(), postMetadataImages.join(), whoAmI.toText()]) + }, [deconstructedPayload.ok, postBy.toText(), postMetadataImages.join(), whoAmI.toText(), mentionedLinks.join()]) // pass 3: // invoke callback @@ -210,6 +236,7 @@ export function DecryptPost(props: DecryptPostProps) { } } +type ReportProgress = (type: 'e2e' | 'error', message: string) => void async function makeProgress( postURL: string | undefined, authorHint: ProfileIdentifier, @@ -217,6 +244,7 @@ async function makeProgress( payload: SocialNetworkEncodedPayload, done: (message: TypedMessage, iv: Uint8Array) => void, reporter: PostContext['decryptedReport'], + reportProgress: ReportProgress, signal: AbortSignal, ) { const context: DecryptionContext = { @@ -237,6 +265,11 @@ async function makeProgress( if (progress.iv) reporter({ iv: encodeArrayBuffer(progress.iv) }) if (progress.version) reporter({ version: progress.version }) if (typeof progress.publicShared === 'boolean') reporter({ sharedPublic: Some(progress.publicShared) }) - } + } else if (progress.type === DecryptProgressKind.Progress) { + if (progress.event === DecryptIntermediateProgressKind.TryDecryptByE2E) reportProgress('e2e') + else safeUnreachable(progress.event) + } else if (progress.type === DecryptProgressKind.Error) { + console.log(progress.message) + } else safeUnreachable(progress) } } From 897c5548bdffff4d44f141737d3f301e01ccf8c4 Mon Sep 17 00:00:00 2001 From: Jack Works Date: Mon, 14 Mar 2022 17:29:23 +0800 Subject: [PATCH 5/8] fix: build error --- .../InjectedComponents/DecryptedPost/DecryptedPost.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mask/src/components/InjectedComponents/DecryptedPost/DecryptedPost.tsx b/packages/mask/src/components/InjectedComponents/DecryptedPost/DecryptedPost.tsx index c1de208f81e3..b3d4e193f671 100644 --- a/packages/mask/src/components/InjectedComponents/DecryptedPost/DecryptedPost.tsx +++ b/packages/mask/src/components/InjectedComponents/DecryptedPost/DecryptedPost.tsx @@ -266,7 +266,7 @@ async function makeProgress( if (progress.version) reporter({ version: progress.version }) if (typeof progress.publicShared === 'boolean') reporter({ sharedPublic: Some(progress.publicShared) }) } else if (progress.type === DecryptProgressKind.Progress) { - if (progress.event === DecryptIntermediateProgressKind.TryDecryptByE2E) reportProgress('e2e') + if (progress.event === DecryptIntermediateProgressKind.TryDecryptByE2E) reportProgress('e2e', '') else safeUnreachable(progress.event) } else if (progress.type === DecryptProgressKind.Error) { console.log(progress.message) From 9f238100d2c4e887b0cdc89d4cb7f90d88f71a3d Mon Sep 17 00:00:00 2001 From: Jack Works Date: Tue, 15 Mar 2022 11:13:42 +0800 Subject: [PATCH 6/8] chore: remove unused i18n --- packages/mask/shared-ui/locales/en-US.json | 4 ---- packages/mask/shared-ui/locales/ja-JP.json | 4 ---- packages/mask/shared-ui/locales/ko-KR.json | 4 ---- packages/mask/shared-ui/locales/qya-AA.json | 4 ---- packages/mask/shared-ui/locales/zh-CN.json | 4 ---- packages/mask/shared-ui/locales/zh-TW.json | 4 ---- 6 files changed, 24 deletions(-) diff --git a/packages/mask/shared-ui/locales/en-US.json b/packages/mask/shared-ui/locales/en-US.json index 5858f2017d8a..60e40f45df72 100644 --- a/packages/mask/shared-ui/locales/en-US.json +++ b/packages/mask/shared-ui/locales/en-US.json @@ -150,11 +150,7 @@ "select_specific_friends_dialog__title": "Select Specific Contacts", "service_decryption_failed": "Decryption failed.", "service_invalid_backup_file": "This does not seem like a backup of Mask.", - "service_not_share_target": "Mask does not find the post key. This post may not be intended to share with you.", - "service_others_key_not_found": "The public key of {{name}} cannot be found!", "service_publish_post_aes_key_failed": "Failed to publish the AES key!", - "service_self_key_decryption_failed": "Decryption failed. Maybe you selected a wrong identity or imported a key that was not used to encrypt this post.", - "service_decode_image_payload_failed": "Decode image failed.", "service_unknown_payload": "Unknown post version. You may need to update Mask.", "service_username_invalid": "Invalid Username", "settings_enable_debug": "Debug Mode", diff --git a/packages/mask/shared-ui/locales/ja-JP.json b/packages/mask/shared-ui/locales/ja-JP.json index aec1b8e2e302..d524c836b238 100644 --- a/packages/mask/shared-ui/locales/ja-JP.json +++ b/packages/mask/shared-ui/locales/ja-JP.json @@ -66,11 +66,7 @@ "select_specific_friends_dialog__title": "特定の連絡先を選ぶ", "service_decryption_failed": "復号に失敗", "service_invalid_backup_file": "これは Mask のバックアップではないかもしれません。", - "service_not_share_target": " Mask は投稿鍵を発見できませんでした。この投稿はあなたに共有されていない可能性があります。", - "service_others_key_not_found": "{{name}} の公開鍵が見つかりません!", "service_publish_post_aes_key_failed": "AES キーを公開するのに失敗しました。", - "service_self_key_decryption_failed": "復号に失敗しました。間違ったIDを選択したか、この投稿の暗号化に使用されていない鍵をインポートしたのかもしれません。", - "service_decode_image_payload_failed": "画像のデコードに失敗しました。", "service_unknown_payload": "投稿のバージョンが不明です。Mask のアップデートが必要かもしれません。", "service_username_invalid": "無効なユーザー名", "settings_enable_debug": "デバッグモード", diff --git a/packages/mask/shared-ui/locales/ko-KR.json b/packages/mask/shared-ui/locales/ko-KR.json index c65c704bf6d4..3161d8c48c3c 100644 --- a/packages/mask/shared-ui/locales/ko-KR.json +++ b/packages/mask/shared-ui/locales/ko-KR.json @@ -125,11 +125,7 @@ "select_specific_friends_dialog__title": "지정 연락처 선택", "service_decryption_failed": "해독 실패", "service_invalid_backup_file": "Mask의 백업이 아닌 것 같습니다.", - "service_not_share_target": "Mask가 이 게시물의 키를 찾지 못합니다. 이 게시물은 당신에게 공유하는 것이 아닌 것 같습니다.", - "service_others_key_not_found": "찾을 수 없는 {{name}}의 공개 키입니다.", "service_publish_post_aes_key_failed": "AES 키 공유 실패", - "service_self_key_decryption_failed": "해독 실패했습니다. 잘못된 신분이나 키를 불러온 것 같습니다.", - "service_decode_image_payload_failed": "이미지 디코딩 실패.", "service_unknown_payload": "알 수 없는 게시물 버전입니다. Mask를 업데이트하세요.", "service_username_invalid": "무효한 아이디입니다", "settings_enable_debug": "디버그 모드", diff --git a/packages/mask/shared-ui/locales/qya-AA.json b/packages/mask/shared-ui/locales/qya-AA.json index 10d133b5a232..c9f261d0bfe7 100644 --- a/packages/mask/shared-ui/locales/qya-AA.json +++ b/packages/mask/shared-ui/locales/qya-AA.json @@ -150,11 +150,7 @@ "select_specific_friends_dialog__title": "crwdns4341:0crwdne4341:0", "service_decryption_failed": "crwdns4343:0crwdne4343:0", "service_invalid_backup_file": "crwdns4345:0crwdne4345:0", - "service_not_share_target": "crwdns4347:0crwdne4347:0", - "service_others_key_not_found": "crwdns4349:0{{name}}crwdne4349:0", "service_publish_post_aes_key_failed": "crwdns4351:0crwdne4351:0", - "service_self_key_decryption_failed": "crwdns4353:0crwdne4353:0", - "service_decode_image_payload_failed": "crwdns4355:0crwdne4355:0", "service_unknown_payload": "crwdns4357:0crwdne4357:0", "service_username_invalid": "crwdns4359:0crwdne4359:0", "settings_enable_debug": "crwdns4427:0crwdne4427:0", diff --git a/packages/mask/shared-ui/locales/zh-CN.json b/packages/mask/shared-ui/locales/zh-CN.json index e5b445da1f4c..1a190aba9051 100644 --- a/packages/mask/shared-ui/locales/zh-CN.json +++ b/packages/mask/shared-ui/locales/zh-CN.json @@ -142,11 +142,7 @@ "select_specific_friends_dialog__title": "选择特定联系人", "service_decryption_failed": "解密失败", "service_invalid_backup_file": "这似乎不是Mask的备份。", - "service_not_share_target": "Mask没有找到该贴文的密钥。这个贴文可能没有分享给您查看。", - "service_others_key_not_found": "无法找到 {{name}} 的公钥 !", "service_publish_post_aes_key_failed": "AES密钥发布失败!", - "service_self_key_decryption_failed": "贴文解密失败。或许您选择了错误的身份或者导入的是未用于加密此贴文的密钥。", - "service_decode_image_payload_failed": "图片解码失败。", "service_unknown_payload": "未知的贴文版本。您可能需要更新Mask。", "service_username_invalid": "无效的用户名", "settings_enable_debug": "调试模式", diff --git a/packages/mask/shared-ui/locales/zh-TW.json b/packages/mask/shared-ui/locales/zh-TW.json index 8d1c48cdc0dd..878d1517ad88 100644 --- a/packages/mask/shared-ui/locales/zh-TW.json +++ b/packages/mask/shared-ui/locales/zh-TW.json @@ -98,11 +98,7 @@ "select_specific_friends_dialog__title": "選擇特定聯絡人", "service_decryption_failed": "解密失敗。", "service_invalid_backup_file": "這不是一個 Mask 的備份檔。", - "service_not_share_target": "Mask 無發找到貼文金鑰。 這篇文章或許不是要分享給你的。", - "service_others_key_not_found": "無法找到 {{name}} 的公鑰!", "service_publish_post_aes_key_failed": "發佈 AES 金鑰失敗!", - "service_self_key_decryption_failed": "解密失敗。可能您選擇了錯誤的身分或導入了一個不用於加密此貼文的金鑰。", - "service_decode_image_payload_failed": "圖片解碼失敗", "service_unknown_payload": "未知的貼文版本。您或許需要升級 Mask。", "service_username_invalid": "錯誤的使用者名稱", "settings_enable_debug": "除錯模式", From 0e7cace2d8e5ce0c5bd3217cc2b2804f685b3b5d Mon Sep 17 00:00:00 2001 From: Jack Works Date: Fri, 18 Mar 2022 12:22:23 +0800 Subject: [PATCH 7/8] fix: e2e decryption not reported --- packages/encryption/src/encryption/Decryption.ts | 4 ++++ .../tests/__snapshots__/encryption.ts.snap | 12 ++++++++++++ 2 files changed, 16 insertions(+) diff --git a/packages/encryption/src/encryption/Decryption.ts b/packages/encryption/src/encryption/Decryption.ts index fb0b92c8baa4..68b344cf4284 100644 --- a/packages/encryption/src/encryption/Decryption.ts +++ b/packages/encryption/src/encryption/Decryption.ts @@ -12,6 +12,8 @@ import { DecryptError, DecryptEphemeralECDH_PostKey, DecryptSuccess, + DecryptIntermediateProgress, + DecryptIntermediateProgressKind, } from './DecryptionTypes' import { deriveAESByECDH_version38OrOlderExtraSteps } from './v38-ecdh' export * from './DecryptionTypes' @@ -77,6 +79,7 @@ export async function* decrypt(options: DecryptOptions, io: DecryptIO): AsyncIte // #region // ! decrypt by ECDH const authorPublicKey = _authorPublicKey.unwrapOr(None) + yield progress(DecryptProgressKind.Progress, { event: DecryptIntermediateProgressKind.TryDecryptByE2E }) if (version === -37) { return yield* v37ECDHE(io, encryption, encrypted, options.signal) } else { @@ -235,6 +238,7 @@ function importAESKeyFromRaw(aes_raw: Uint8Array) { } function progress(kind: DecryptProgressKind.Success, rest: Omit): DecryptSuccess +function progress(kind: DecryptProgressKind.Progress, rest: Omit): DecryptSuccess function progress(kind: DecryptProgressKind, rest?: object): DecryptProgress { return { type: kind, ...rest } as any } diff --git a/packages/encryption/tests/__snapshots__/encryption.ts.snap b/packages/encryption/tests/__snapshots__/encryption.ts.snap index 24512ab2f93a..1eb5c2486eb2 100644 --- a/packages/encryption/tests/__snapshots__/encryption.ts.snap +++ b/packages/encryption/tests/__snapshots__/encryption.ts.snap @@ -2,6 +2,10 @@ exports[`v37 E2E encryption: decrypted as author 1`] = ` Array [ + Object { + "event": "E2E", + "type": "progress", + }, Object { "content": Object { "content": "hello world", @@ -17,6 +21,10 @@ Array [ exports[`v37 E2E encryption: decrypted as jack 1`] = ` Array [ + Object { + "event": "E2E", + "type": "progress", + }, Object { "content": Object { "content": "hello world", @@ -242,6 +250,10 @@ Array [ exports[`v38 E2E encryption: decrypted as jack 1`] = ` Array [ + Object { + "event": "E2E", + "type": "progress", + }, Object { "content": Object { "content": "hello world", From 1ca532c7795a831e10f75d7518083e3ff9e2f19a Mon Sep 17 00:00:00 2001 From: Jack Works Date: Fri, 18 Mar 2022 12:38:00 +0800 Subject: [PATCH 8/8] chore: add in memory cache --- .../encryption/src/encryption/Decryption.ts | 47 ++++++++++++++----- .../src/encryption/DecryptionTypes.ts | 1 + .../background/services/crypto/decryption.ts | 15 +++++- 3 files changed, 50 insertions(+), 13 deletions(-) diff --git a/packages/encryption/src/encryption/Decryption.ts b/packages/encryption/src/encryption/Decryption.ts index 68b344cf4284..e4141bb8462f 100644 --- a/packages/encryption/src/encryption/Decryption.ts +++ b/packages/encryption/src/encryption/Decryption.ts @@ -1,5 +1,5 @@ import { unreachable } from '@dimensiondev/kit' -import { decodeTypedMessageFromDocument, decodeTypedMessageV38ToV40Format } from '@masknet/typed-message' +import { decodeTypedMessageFromDocument, decodeTypedMessageV38ToV40Format, TypedMessage } from '@masknet/typed-message' import { AESCryptoKey, EC_Public_CryptoKey, andThenAsync } from '@masknet/shared-base' import { None, Result } from 'ts-results' import { AESAlgorithmEnum, PayloadParseResult } from '../payload' @@ -32,7 +32,7 @@ export async function* decrypt(options: DecryptOptions, io: DecryptIO): AsyncIte { const cacheKey = await io.getPostKeyCache().catch(() => null) const iv = encryption.iv.unwrapOr(null) - if (cacheKey && iv) return yield* decryptWithPostAESKey(version, cacheKey, iv, encrypted) + if (cacheKey && iv) return yield* decryptWithPostAESKey(version, cacheKey, iv, encrypted, options.onDecrypted) } if (encryption.type === 'public') { @@ -40,7 +40,13 @@ export async function* decrypt(options: DecryptOptions, io: DecryptIO): AsyncIte if (AESKey.err) return yield new DecryptError(ErrorReasons.PayloadBroken, AESKey.val) if (iv.err) return yield new DecryptError(ErrorReasons.PayloadBroken, iv.val) // Not calling setPostCache here. It's public post and saving key is wasting storage space. - return yield* decryptWithPostAESKey(version, AESKey.val.key as AESCryptoKey, iv.val, encrypted) + return yield* decryptWithPostAESKey( + version, + AESKey.val.key as AESCryptoKey, + iv.val, + encrypted, + options.onDecrypted, + ) } else if (encryption.type === 'E2E') { const { iv: _iv, ownersAESKeyEncrypted } = encryption if (_iv.err) return yield new DecryptError(ErrorReasons.PayloadBroken, _iv.val) @@ -57,7 +63,7 @@ export async function* decrypt(options: DecryptOptions, io: DecryptIO): AsyncIte const aes_raw = await io.decryptByLocalKey(author.unwrapOr(null), ownersAESKeyEncrypted.val, iv) const aes = await importAESKeyFromJWKFromTextEncoder(aes_raw) io.setPostKeyCache(aes.unwrap()).catch(() => {}) - return yield* decryptWithPostAESKey(version, aes.unwrap(), iv, encrypted) + return yield* decryptWithPostAESKey(version, aes.unwrap(), iv, encrypted, options.onDecrypted) } catch (err) { if (await hasAuthorLocalKey) { // If we fall into this branch, it means we failed to decrypt as author. @@ -81,7 +87,7 @@ export async function* decrypt(options: DecryptOptions, io: DecryptIO): AsyncIte const authorPublicKey = _authorPublicKey.unwrapOr(None) yield progress(DecryptProgressKind.Progress, { event: DecryptIntermediateProgressKind.TryDecryptByE2E }) if (version === -37) { - return yield* v37ECDHE(io, encryption, encrypted, options.signal) + return yield* v37ECDHE(io, encryption, encrypted, options.signal, options.onDecrypted) } else { // Static ECDH // to do static ECDH, we need to have the authors public key first. bail if not found. @@ -91,7 +97,15 @@ export async function* decrypt(options: DecryptOptions, io: DecryptIO): AsyncIte }) if (authorECPub.err) return yield new DecryptError(ErrorReasons.AuthorPublicKeyNotFound, authorECPub.val) if (!authorECPub.val) return yield new DecryptError(ErrorReasons.AuthorPublicKeyNotFound, undefined) - return yield* v38To40StaticECDH(version, io, authorECPub.val, iv, encrypted, options.signal) + return yield* v38To40StaticECDH( + version, + io, + authorECPub.val, + iv, + encrypted, + options.signal, + options.onDecrypted, + ) } // #endregion } @@ -103,6 +117,7 @@ async function* v37ECDHE( encryption: PayloadParseResult.EndToEndEncryption, encrypted: Uint8Array, signal: AbortSignal | undefined, + report: ((message: TypedMessage) => void) | undefined, ) { // checked before const iv = encryption.iv.unwrap() @@ -129,7 +144,7 @@ async function* v37ECDHE( type: 'ephemeral', derive: (key) => (key ? io.deriveAESKey(key) : inlinedECDHE_derived), } - return yield* decryptByECDH(-37, io, postKey(), ecdh, importAESKeyFromRaw, iv, encrypted) + return yield* decryptByECDH(-37, io, postKey(), ecdh, importAESKeyFromRaw, iv, encrypted, report) } async function* v38To40StaticECDH( @@ -139,6 +154,7 @@ async function* v38To40StaticECDH( iv: Uint8Array, encrypted: Uint8Array, signal: AbortSignal | undefined, + report: ((message: TypedMessage) => void) | undefined, ): AsyncIterableIterator { const postKey = { '-40': async function* (iv: Uint8Array) { @@ -153,7 +169,7 @@ async function* v38To40StaticECDH( type: 'static-v38-or-older', derive: (postKeyIV) => deriveAESByECDH_version38OrOlderExtraSteps(io.deriveAESKey, authorECPub, postKeyIV), } - return yield* decryptByECDH(version, io, postKey, ecdh, importAESKeyFromJWKFromTextEncoder, iv, encrypted) + return yield* decryptByECDH(version, io, postKey, ecdh, importAESKeyFromJWKFromTextEncoder, iv, encrypted, report) } type StaticV38OrOlderECDH = { type: 'static-v38-or-older' @@ -173,6 +189,7 @@ async function* decryptByECDH( postKeyDecoder: (raw: Uint8Array) => Promise>, iv: Uint8Array, encrypted: Uint8Array, + report: ((message: TypedMessage) => void) | undefined, ) { const { derive, type } = ecdhProvider for await (const _ of possiblePostKeyIterator) { @@ -195,7 +212,7 @@ async function* decryptByECDH( io.setPostKeyCache(possiblePostKey.val).catch(() => {}) // If we'd able to decrypt the raw message, we will stop here. // because try further key cannot resolve the problem of parseTypedMessage failed. - return yield* parseTypedMessage(version, decrypted.val) + return yield* parseTypedMessage(version, decrypted.val, report) } } return void (yield new DecryptError(ErrorReasons.NotShareTarget, undefined)) @@ -206,16 +223,24 @@ async function* decryptWithPostAESKey( postAESKey: AESCryptoKey, iv: Uint8Array, encrypted: Uint8Array, + report: ((message: TypedMessage) => void) | undefined, ): AsyncIterableIterator { const { err, val } = await decryptWithAES(AESAlgorithmEnum.A256GCM, postAESKey, iv, encrypted) if (err) return yield new DecryptError(ErrorReasons.DecryptFailed, val) - return yield* parseTypedMessage(version, val) + return yield* parseTypedMessage(version, val, report) } -async function* parseTypedMessage(version: Version, raw: Uint8Array): AsyncIterableIterator { +async function* parseTypedMessage( + version: Version, + raw: Uint8Array, + report: ((message: TypedMessage) => void) | undefined, +): AsyncIterableIterator { const { err, val } = version === -37 ? decodeTypedMessageFromDocument(raw) : decodeTypedMessageV38ToV40Format(raw, version) if (err) return yield new DecryptError(ErrorReasons.PayloadDecryptedButTypedMessageBroken, val) + try { + report?.(val) + } catch {} return yield progress(DecryptProgressKind.Success, { content: val }) } diff --git a/packages/encryption/src/encryption/DecryptionTypes.ts b/packages/encryption/src/encryption/DecryptionTypes.ts index 98a2d30e825a..032b1678eb15 100644 --- a/packages/encryption/src/encryption/DecryptionTypes.ts +++ b/packages/encryption/src/encryption/DecryptionTypes.ts @@ -5,6 +5,7 @@ import { registerSerializableClass } from '@masknet/shared-base' export interface DecryptOptions { message: PayloadParseResult.Payload + onDecrypted?(message: TypedMessage): void signal?: AbortSignal } export interface DecryptIO { diff --git a/packages/mask/background/services/crypto/decryption.ts b/packages/mask/background/services/crypto/decryption.ts index 6b5af69a5902..cc1c661dd07f 100644 --- a/packages/mask/background/services/crypto/decryption.ts +++ b/packages/mask/background/services/crypto/decryption.ts @@ -22,6 +22,7 @@ import { PostIVIdentifier, ProfileIdentifier, } from '@masknet/shared-base' +import type { TypedMessage } from '@masknet/typed-message' import { noop } from 'lodash-unified' import { queryPersonaByProfileDB } from '../../database/persona/db' import { @@ -72,6 +73,8 @@ export async function* decryptionWithSocialNetworkDecoding( if (!decoded) return yield new DecryptError(ErrorReasons.NoPayloadFound, undefined) yield* decryption(decoded, context) } + +const inMemoryCache = new IdentifierMap(new Map(), PostIVIdentifier) async function* decryption(payload: string | Uint8Array, context: DecryptionContext) { const parse = await parsePayload(payload) if (parse.err) return null @@ -105,7 +108,10 @@ async function* decryption(payload: string | Uint8Array, context: DecryptionCont ) // #endregion - // TODO: read in-memory cache to avoid db lookup + if (inMemoryCache.has(id)) { + const p: DecryptProgress = { type: DecryptProgressKind.Success, content: inMemoryCache.get(id)! } + return void (yield p) + } // #region store author public key into the database try { @@ -119,7 +125,12 @@ async function* decryption(payload: string | Uint8Array, context: DecryptionCont // #endregion const progress = decrypt( - { message: parse.val }, + { + message: parse.val, + onDecrypted(message) { + inMemoryCache.set(id, message) + }, + }, { getPostKeyCache: getPostKeyCache.bind(null, id), setPostKeyCache: (key) => {