From cc9bd5ce8dd84d71a2b85a65ef58251d81b4024e Mon Sep 17 00:00:00 2001 From: Jack Works Date: Fri, 18 Mar 2022 17:54:04 +0800 Subject: [PATCH 1/7] feat: use new encryption --- .../background/database/persona/helper.ts | 29 +++++- .../mask/background/database/post/helper.ts | 2 +- .../network/gun/encryption/queryPostKey.ts | 40 ++++++-- .../background/services/crypto/decryption.ts | 16 +--- .../background/services/crypto/encryption.ts | 93 +++++++++++++++++++ .../CompositionDialog/CompositionUI.tsx | 14 ++- .../CompositionDialog/TypedMessageEditor.tsx | 7 +- .../components/CompositionDialog/useSubmit.ts | 13 +-- .../background-script/CryptoService.ts | 3 +- .../facebook.com/automation/openComposeBox.ts | 4 +- .../minds.com/automation/openComposeBox.ts | 7 +- .../twitter.com/automation/openComposeBox.ts | 7 +- packages/mask/src/social-network/types.ts | 4 +- packages/shared-base/src/Messages/Mask.ts | 6 +- 14 files changed, 191 insertions(+), 54 deletions(-) create mode 100644 packages/mask/background/services/crypto/encryption.ts diff --git a/packages/mask/background/database/persona/helper.ts b/packages/mask/background/database/persona/helper.ts index b9d2c1b60861..2507501bcd4c 100644 --- a/packages/mask/background/database/persona/helper.ts +++ b/packages/mask/background/database/persona/helper.ts @@ -73,6 +73,19 @@ export async function decryptByLocalKey( ) } +export async function encryptByLocalKey(who: ProfileIdentifier, content: Uint8Array, iv: Uint8Array) { + let key: AESCryptoKey | undefined + await createPersonaDBReadonlyAccess(async (tx) => { + const jwk = await getLocalKeyOf(who, tx) + if (!jwk) return + const k = await crypto.subtle.importKey('jwk', jwk, { name: 'AES-GCM', length: 256 }, false, ['encrypt']) + key = k as AESCryptoKey + }) + if (!key) throw new Error('No local key found') + const result = await crypto.subtle.encrypt({ iv, name: 'AES-GCM' }, key, content) + return result as Uint8Array +} + async function getLocalKeyOf(id: ProfileIdentifier, tx: FullPersonaDBTransaction<'readonly'>) { const profile = await queryProfileDB(id, tx) if (!profile) return @@ -85,7 +98,7 @@ async function getLocalKeyOf(id: ProfileIdentifier, tx: FullPersonaDBTransaction // #endregion // #region ECDH -export async function deriveAESByECDH(pub: EC_Public_CryptoKey) { +export async function deriveAESByECDH(pub: EC_Public_CryptoKey, of?: ProfileIdentifier) { const curve = (pub.algorithm as EcKeyAlgorithm).namedCurve || '' const sameCurvePrivateKeys = new IdentifierMap(new Map(), ECKeyIdentifier) @@ -94,6 +107,7 @@ export async function deriveAESByECDH(pub: EC_Public_CryptoKey) { for (const persona of personas) { if (!persona.privateKey) continue if (persona.privateKey.crv !== curve) continue + if (of && !persona.linkedProfiles.has(of)) continue sameCurvePrivateKeys.set(persona.identifier, persona.privateKey) } }) @@ -184,6 +198,19 @@ export async function createProfileWithPersona( } // #endregion +export 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 +} + function abort() { throw new Error('Cancelled') } diff --git a/packages/mask/background/database/post/helper.ts b/packages/mask/background/database/post/helper.ts index c7bad98079cc..b1daa9195afd 100644 --- a/packages/mask/background/database/post/helper.ts +++ b/packages/mask/background/database/post/helper.ts @@ -6,7 +6,7 @@ import { PostDBAccess, queryPostDB, createPostDB, updatePostDB, PostRecord } fro export async function savePostKeyToDB( id: PostIVIdentifier, key: AESCryptoKey, - extraInfo: Omit, + extraInfo: Omit, ): Promise { const jwk = await CryptoKeyToJsonWebKey(key) { diff --git a/packages/mask/background/network/gun/encryption/queryPostKey.ts b/packages/mask/background/network/gun/encryption/queryPostKey.ts index d019f7a094d4..11ce12408e78 100644 --- a/packages/mask/background/network/gun/encryption/queryPostKey.ts +++ b/packages/mask/background/network/gun/encryption/queryPostKey.ts @@ -1,10 +1,11 @@ import { decodeArrayBuffer, encodeArrayBuffer } from '@dimensiondev/kit' -import type { DecryptStaticECDH_PostKey } from '@masknet/encryption' +import type { DecryptStaticECDH_PostKey, EncryptionResultE2EMap } from '@masknet/encryption' import type { EC_Public_CryptoKey, EC_Public_JsonWebKey } from '@masknet/shared-base' import { CryptoKeyToJsonWebKey } from '../../../../utils-pure' import { getGunData, pushToGunDataArray, subscribeGunMapData } from '@masknet/gun-utils' import { EventIterator } from 'event-iterator' import { noop } from 'lodash-unified' +import { queryPublicKey } from '../../../database/persona/helper' // !!! Change how this file access Gun will break the compatibility of v40 payload decryption. export async function GUN_queryPostKey_version40( @@ -86,15 +87,38 @@ namespace Version38Or39 { version: -39 | -38, postIV: Uint8Array, networkHint: string, - receiversKeys: PublishedAESKeyRecord[], + receiversKeys: PublishedAESKeyRecord[] | EncryptionResultE2EMap, ) { const postHash = await hashIV(networkHint, postIV) - // Store AES key to gun - receiversKeys.forEach(async ({ aesKey, receiverKey }) => { - const keyHash = await (version === -38 ? hashKey38 : hashKey39)(receiverKey) - console.log(`gun[${postHash}][${keyHash}].push(`, aesKey, ')') - pushToGunDataArray([postHash, keyHash], aesKey) - }) + if (Array.isArray(receiversKeys)) { + // Store AES key to gun + receiversKeys.forEach(async ({ aesKey, receiverKey }) => { + const keyHash = await (version === -38 ? hashKey38 : hashKey39)(receiverKey) + console.log(`gun[${postHash}][${keyHash}].push(`, aesKey, ')') + pushToGunDataArray([postHash, keyHash], aesKey) + }) + } else { + if (version === -39) throw new Error('unreachable') + for (const result of receiversKeys.values()) { + try { + if (result.status === 'rejected') continue + const { encryptedPostKey, target, ivToBePublished } = result.value + if (!ivToBePublished) throw new Error('Missing salt') + const pub = await queryPublicKey(target, true) + if (!pub) throw new Error('missing key') + const jwk = await CryptoKeyToJsonWebKey(pub) + const keyHash = await hashKey38(jwk) + const post: PublishedAESKeyRecord['aesKey'] = { + encryptedKey: encodeArrayBuffer(encryptedPostKey), + salt: encodeArrayBuffer(ivToBePublished), + } + console.log(`gun[${postHash}][${keyHash}].push(`, post, ')') + pushToGunDataArray([postHash, keyHash], post) + } catch (error) { + console.error('[@masknet/encryption] An error occurs when sending E2E keys', error) + } + } + } } type DataOnGun = { diff --git a/packages/mask/background/services/crypto/decryption.ts b/packages/mask/background/services/crypto/decryption.ts index e4aab3c2e42a..3448d88e71cf 100644 --- a/packages/mask/background/services/crypto/decryption.ts +++ b/packages/mask/background/services/crypto/decryption.ts @@ -16,7 +16,6 @@ import { } from '@masknet/encryption' import { AESCryptoKey, - EC_Public_CryptoKey, EC_Public_JsonWebKey, IdentifierMap, PostIVIdentifier, @@ -24,12 +23,12 @@ import { } from '@masknet/shared-base' import type { TypedMessage } from '@masknet/typed-message' import { noop } from 'lodash-unified' -import { queryPersonaByProfileDB } from '../../database/persona/db' import { createProfileWithPersona, decryptByLocalKey, deriveAESByECDH, hasLocalKeyOf, + queryPublicKey, } from '../../database/persona/helper' import { queryPostDB } from '../../database/post' import { savePostKeyToDB } from '../../database/post/helper' @@ -233,16 +232,3 @@ async function storeAuthorPublicKey( }, ) } - -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/encryption.ts b/packages/mask/background/services/crypto/encryption.ts new file mode 100644 index 000000000000..910440b05564 --- /dev/null +++ b/packages/mask/background/services/crypto/encryption.ts @@ -0,0 +1,93 @@ +import { EC_Public_CryptoKey, IdentifierMap, ProfileIdentifier, RecipientDetail } from '@masknet/shared-base' +import { isTypedMessageText, SerializableTypedMessages, TypedMessageText } from '@masknet/typed-message' +import { + EC_Key, + EC_KeyCurveEnum, + encrypt, + EncryptionResultE2EMap, + EncryptTargetE2E, + EncryptTargetPublic, +} from '@masknet/encryption' +import { encryptByLocalKey, deriveAESByECDH, queryPublicKey } from '../../database/persona/helper' +import { savePostKeyToDB } from '../../database/post/helper' +import { noop } from 'lodash-unified' +import { queryProfileDB } from '../../database/persona/db' +import { publishPostAESKey_version39Or38 } from '../../network/gun/encryption/queryPostKey' + +export async function encryptTo( + content: SerializableTypedMessages, + target: EncryptTargetPublic | EncryptTargetE2E, + whoAmI: ProfileIdentifier, +): Promise { + const { identifier, output, postKey, e2e } = await encrypt( + { + author: whoAmI, + message: content, + target, + version: -38, + }, + { + async deriveAESKey(pub) { + const result = Array.from((await deriveAESByECDH(pub, whoAmI)).values()) + if (result.length === 0) throw new Error('No key found') + return result[0] + }, + encryptByLocalKey: encryptByLocalKey.bind(null, whoAmI), + queryPublicKey: (id) => + queryPublicKey(id).then((key): EC_Key | null => + key ? { algr: EC_KeyCurveEnum.secp256k1, key } : null, + ), + }, + ) + + ;(async () => { + const profile = await queryProfileDB(whoAmI).catch(noop) + const usingPersona = profile?.linkedPersona + return savePostKeyToDB(identifier, postKey, { + postBy: whoAmI, + recipients: target.type === 'public' ? 'everyone' : e2eMapToRecipientDetails(e2e!), + encryptBy: usingPersona, + ...collectInterestedMeta(content), + }) + })().catch((error) => console.error('[@masknet/encryption] Failed to save post key to DB', error)) + + if (target.type === 'E2E') { + publishPostAESKey_version39Or38(-38, identifier.toIV(), whoAmI.network, e2e!) + } + if (typeof output !== 'string') throw new Error('version -37 is not enabled yet!') + return output +} + +function e2eMapToRecipientDetails(input: EncryptionResultE2EMap): IdentifierMap { + const result = new IdentifierMap(new Map(), ProfileIdentifier) + for (const [identifier] of input) { + result.set(identifier, { + reason: [{ type: 'direct', at: new Date() }], + }) + } + return result +} + +function collectInterestedMeta(content: SerializableTypedMessages) { + if (isTypedMessageText(content)) return { summary: getSummary(content), meta: content.meta } + return {} +} + +const SUMMARY_MAX_LENGTH = 40 +function getSummary(content: TypedMessageText) { + let result = '' + const sliceLength = content.content.length > SUMMARY_MAX_LENGTH ? SUMMARY_MAX_LENGTH + 1 : SUMMARY_MAX_LENGTH + + // UTF-8 aware summary + if (Intl.Segmenter) { + // it seems like using "en" can also split the word correctly. + const seg = new Intl.Segmenter('en') + for (const word of seg.segment(content.content)) { + if (result.length >= sliceLength) break + result += word.segment + } + } else { + result = content.content.slice(0, sliceLength) + } + return result +} diff --git a/packages/mask/src/components/CompositionDialog/CompositionUI.tsx b/packages/mask/src/components/CompositionDialog/CompositionUI.tsx index ef6a52dc00b5..18bb65e04ecd 100644 --- a/packages/mask/src/components/CompositionDialog/CompositionUI.tsx +++ b/packages/mask/src/components/CompositionDialog/CompositionUI.tsx @@ -1,7 +1,7 @@ import { forwardRef, useImperativeHandle, useMemo, useRef, useState, startTransition, useCallback } from 'react' import { Typography, Chip, Button } from '@mui/material' import { LoadingButton } from '@mui/lab' -import type { TypedMessage } from '@masknet/typed-message' +import type { SerializableTypedMessages, TypedMessage } from '@masknet/typed-message' import { useValueRef } from '@masknet/shared-base-ui' import { makeStyles } from '@masknet/theme' import { ImagePayloadIcon } from '@masknet/icons' @@ -18,6 +18,7 @@ import type { Profile } from '../../database' import { CompositionContext } from '@masknet/plugin-infra' import { DebugMetadataInspector } from '../shared/DebugMetadataInspector' import { Trans } from 'react-i18next' +import type { EncryptTargetE2E, EncryptTargetPublic } from '@masknet/encryption' const useStyles = makeStyles()({ root: { @@ -57,12 +58,12 @@ export interface CompositionProps { onQueryClipboardPermission?(): void } export interface SubmitComposition { - target: 'Everyone' | Profile[] - content: TypedMessage + target: EncryptTargetPublic | EncryptTargetE2E + content: SerializableTypedMessages encode: 'text' | 'image' } export interface CompositionRef { - setMessage(message: TypedMessage): void + setMessage(message: SerializableTypedMessages): void setEncryptionKind(kind: 'E2E' | 'Everyone'): void startPlugin(id: string): void reset(): void @@ -154,7 +155,10 @@ export const CompositionDialogUI = forwardRef( .onSubmit({ content: Editor.current.value, encode: encodingKind, - target: encryptionKind === 'E2E' ? recipients : 'Everyone', + target: + encryptionKind === 'E2E' + ? { type: 'E2E', target: recipients.map((x) => x.identifier) } + : { type: 'public' }, }) .finally(reset) }, [encodingKind, encryptionKind, recipients, props.onSubmit]) diff --git a/packages/mask/src/components/CompositionDialog/TypedMessageEditor.tsx b/packages/mask/src/components/CompositionDialog/TypedMessageEditor.tsx index dced75e264e9..d8c2b2541e77 100644 --- a/packages/mask/src/components/CompositionDialog/TypedMessageEditor.tsx +++ b/packages/mask/src/components/CompositionDialog/TypedMessageEditor.tsx @@ -4,6 +4,7 @@ import { makeTypedMessageText, TypedMessage, editTypedMessageMeta, + SerializableTypedMessages, } from '@masknet/typed-message' import { makeStyles } from '@masknet/theme' import { InputBase, Alert, Button } from '@mui/material' @@ -24,14 +25,14 @@ const useStyles = makeStyles()({ }, }) export interface TypedMessageEditorProps { - defaultValue?: TypedMessage + defaultValue?: SerializableTypedMessages onChange?(message: TypedMessage): void readonly?: boolean autoFocus?: boolean } export interface TypedMessageEditorRef { /** Current message, it is a getter/setter. */ - value: TypedMessage + value: SerializableTypedMessages /** The length of the current message. */ readonly estimatedLength: number /** Clean the editor. */ @@ -58,7 +59,7 @@ export const TypedMessageEditor = memo( currentValue.current = value const setMessage = useCallback( - (value: TypedMessage) => { + (value: SerializableTypedMessages) => { if (isTypedMessageEqual(currentValue.current, value)) return setValue(value) currentValue.current = value diff --git a/packages/mask/src/components/CompositionDialog/useSubmit.ts b/packages/mask/src/components/CompositionDialog/useSubmit.ts index 6f6c7ee6b9ad..77f9b11bb798 100644 --- a/packages/mask/src/components/CompositionDialog/useSubmit.ts +++ b/packages/mask/src/components/CompositionDialog/useSubmit.ts @@ -2,7 +2,7 @@ import { ProfileIdentifier } from '@masknet/shared-base' import { useCallback } from 'react' import Services from '../../extension/service' import { RedPacketMetadataReader } from '../../plugins/RedPacket/SNSAdaptor/helpers' -import type { ImageTemplateTypes } from '@masknet/encryption' +import { ImageTemplateTypes, socialNetworkEncoder } from '@masknet/encryption' import { activatedSocialNetworkUI, globalUIState } from '../../social-network' import { isTwitter } from '../../social-network-adaptor/twitter.com/base' import { useI18N } from '../../utils' @@ -27,12 +27,9 @@ export function useSubmit(onClose: () => void) { unreachable('Cannot figure out current profile' as never), ) - const [encrypted, token] = await Services.Crypto.encryptTo( - content, - target === 'Everyone' ? [] : target.map((x) => x.identifier), - whoAmI?.identifier ?? currentProfile, - target === 'Everyone', - ) + const _encrypted = await Services.Crypto.encryptTo(content, target, whoAmI?.identifier ?? currentProfile) + const encrypted = socialNetworkEncoder(activatedSocialNetworkUI.encryptionNetwork, _encrypted) + const redPacketPreText = isTwitter(activatedSocialNetworkUI) || isFacebook(activatedSocialNetworkUI) ? t('additional_post_box__encrypted_post_pre_red_packet_twitter_official_account', { @@ -58,8 +55,6 @@ export function useSubmit(onClose: () => void) { t('additional_post_box__encrypted_post_pre', { encrypted }), ) } - // This step write data on gun. There is nothing to write if it shared with public - if (target !== 'Everyone') Services.Crypto.publishPostAESKey(token) onClose() }, [t, whoAmI, onClose], diff --git a/packages/mask/src/extension/background-script/CryptoService.ts b/packages/mask/src/extension/background-script/CryptoService.ts index 34b5aec1ce7e..2963d3b09165 100644 --- a/packages/mask/src/extension/background-script/CryptoService.ts +++ b/packages/mask/src/extension/background-script/CryptoService.ts @@ -4,7 +4,8 @@ import { steganographyDownloadImage } from './CryptoServices/utils' assertEnvironment(Environment.ManifestBackground) export { encryptComment, decryptComment } from '../../crypto/crypto-alpha-40' -export { encryptTo, publishPostAESKey } from './CryptoServices/encryptTo' +// export { encryptTo, publishPostAESKey } from './CryptoServices/encryptTo' +export { encryptTo } from '../../../background/services/crypto/encryption' export { appendShareTarget } from './CryptoServices/appendShareTarget' export { getPartialSharedListOfPost } from './CryptoServices/getPartialSharedListOfPost' diff --git a/packages/mask/src/social-network-adaptor/facebook.com/automation/openComposeBox.ts b/packages/mask/src/social-network-adaptor/facebook.com/automation/openComposeBox.ts index 2959000832eb..2bd58acc75d6 100644 --- a/packages/mask/src/social-network-adaptor/facebook.com/automation/openComposeBox.ts +++ b/packages/mask/src/social-network-adaptor/facebook.com/automation/openComposeBox.ts @@ -1,7 +1,7 @@ import { LiveSelector } from '@dimensiondev/holoflows-kit' import { MaskMessages, CompositionRequest } from '../../../utils/messages' import { i18n } from '../../../../shared-ui/locales_legacy' -import { makeTypedMessageText, TypedMessage } from '@masknet/typed-message' +import { makeTypedMessageText, SerializableTypedMessages } from '@masknet/typed-message' import { delay, waitDocumentReadyState } from '@dimensiondev/kit' const nativeComposeButtonSelector = () => @@ -35,7 +35,7 @@ const nativeComposeDialogIndicatorSelector = () => ) export async function taskOpenComposeBoxFacebook( - content: string | TypedMessage, + content: string | SerializableTypedMessages, options?: CompositionRequest['options'], ) { await waitDocumentReadyState('interactive') diff --git a/packages/mask/src/social-network-adaptor/minds.com/automation/openComposeBox.ts b/packages/mask/src/social-network-adaptor/minds.com/automation/openComposeBox.ts index cc58f93bfb84..660925f17e3b 100644 --- a/packages/mask/src/social-network-adaptor/minds.com/automation/openComposeBox.ts +++ b/packages/mask/src/social-network-adaptor/minds.com/automation/openComposeBox.ts @@ -1,10 +1,13 @@ -import { makeTypedMessageText, TypedMessage } from '@masknet/typed-message' +import { makeTypedMessageText, SerializableTypedMessages } from '@masknet/typed-message' import { delay, waitDocumentReadyState } from '@dimensiondev/kit' import { i18n } from '../../../../shared-ui/locales_legacy' import { MaskMessages, CompositionRequest } from '../../../utils/messages' import { composeButtonSelector, composeDialogIndicatorSelector, composeTextareaSelector } from '../utils/selector' -export async function openComposeBoxMinds(content: string | TypedMessage, options?: CompositionRequest['options']) { +export async function openComposeBoxMinds( + content: string | SerializableTypedMessages, + options?: CompositionRequest['options'], +) { await waitDocumentReadyState('interactive') await delay(800) diff --git a/packages/mask/src/social-network-adaptor/twitter.com/automation/openComposeBox.ts b/packages/mask/src/social-network-adaptor/twitter.com/automation/openComposeBox.ts index 2d9ccb7cb236..a40eda021270 100644 --- a/packages/mask/src/social-network-adaptor/twitter.com/automation/openComposeBox.ts +++ b/packages/mask/src/social-network-adaptor/twitter.com/automation/openComposeBox.ts @@ -1,7 +1,10 @@ import { MaskMessages, CompositionRequest } from '../../../utils/messages' -import { makeTypedMessageText, TypedMessage } from '@masknet/typed-message' +import { makeTypedMessageText, SerializableTypedMessages } from '@masknet/typed-message' -export function openComposeBoxTwitter(content: string | TypedMessage, options?: CompositionRequest['options']) { +export function openComposeBoxTwitter( + content: string | SerializableTypedMessages, + options?: CompositionRequest['options'], +) { MaskMessages.events.requestComposition.sendToLocal({ reason: 'timeline', open: true, diff --git a/packages/mask/src/social-network/types.ts b/packages/mask/src/social-network/types.ts index cd3376887413..4eccad036093 100644 --- a/packages/mask/src/social-network/types.ts +++ b/packages/mask/src/social-network/types.ts @@ -9,7 +9,7 @@ import type { ProfileIdentifier, ReadonlyIdentifierMap, } from '@masknet/shared-base' -import type { TypedMessage } from '@masknet/typed-message' +import type { SerializableTypedMessages } from '@masknet/typed-message' import type { RenderFragmentsContextType } from '@masknet/typed-message/dom' import type { PaletteMode, Theme } from '@mui/material' import type { Subscription } from 'use-subscription' @@ -197,7 +197,7 @@ export namespace SocialNetworkUI { recover?: boolean } export interface MaskCompositionDialog { - open?(content: TypedMessage, options?: MaskCompositionDialogOpenOptions): void + open?(content: SerializableTypedMessages, options?: MaskCompositionDialogOpenOptions): void } export interface MaskCompositionDialogOpenOptions { target?: 'E2E' | 'Everyone' diff --git a/packages/shared-base/src/Messages/Mask.ts b/packages/shared-base/src/Messages/Mask.ts index b3d5fc4b1237..f5cf9664052c 100644 --- a/packages/shared-base/src/Messages/Mask.ts +++ b/packages/shared-base/src/Messages/Mask.ts @@ -1,4 +1,4 @@ -import type { TypedMessage } from '@masknet/typed-message' +import type { SerializableTypedMessages } from '@masknet/typed-message' import type { ProfileIdentifier, PersonaIdentifier } from '../Identifier/type' import type { RelationFavor } from '../Persona/type' import type { Appearance, LanguageOptions, DataProvider } from '../../../public-api/src/web' @@ -35,7 +35,7 @@ export interface MaskSNSEvents { // TODO: Maybe in-page UI related messages should use Context instead of messages? autoPasteFailed: AutoPasteFailedEvent requestComposition: CompositionRequest - replaceComposition: TypedMessage + replaceComposition: SerializableTypedMessages // TODO: move to plugin message profileTabUpdated: ProfileNFTsPageEvent // TODO: move to plugin message @@ -80,7 +80,7 @@ export interface UpdateEvent { export interface CompositionRequest { readonly reason: 'timeline' | 'popup' readonly open: boolean - readonly content?: TypedMessage + readonly content?: SerializableTypedMessages readonly options?: { target?: 'E2E' | 'Everyone' startupPlugin?: string From 2b821fd4dacdef33decfeebd9f886f66a698e839 Mon Sep 17 00:00:00 2001 From: Jack Works Date: Fri, 18 Mar 2022 18:38:24 +0800 Subject: [PATCH 2/7] fix: public key not extracable --- packages/mask/background/database/persona/helper.ts | 4 ++-- .../mask/background/network/gun/encryption/queryPostKey.ts | 2 +- packages/mask/background/services/crypto/decryption.ts | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/mask/background/database/persona/helper.ts b/packages/mask/background/database/persona/helper.ts index 2507501bcd4c..b24a2758ed3d 100644 --- a/packages/mask/background/database/persona/helper.ts +++ b/packages/mask/background/database/persona/helper.ts @@ -198,7 +198,7 @@ export async function createProfileWithPersona( } // #endregion -export async function queryPublicKey(author: ProfileIdentifier | null, extractable = false, signal?: AbortSignal) { +export async function queryPublicKey(author: ProfileIdentifier | null) { if (!author) return null const persona = await queryPersonaByProfileDB(author) if (!persona) return null @@ -206,7 +206,7 @@ export async function queryPublicKey(author: ProfileIdentifier | null, extractab 'jwk', persona.publicKey, { name: 'ECDH', namedCurve: persona.publicKey.crv! } as EcKeyAlgorithm, - extractable, + true, ['deriveKey'], )) as EC_Public_CryptoKey } diff --git a/packages/mask/background/network/gun/encryption/queryPostKey.ts b/packages/mask/background/network/gun/encryption/queryPostKey.ts index 11ce12408e78..dcda8d547093 100644 --- a/packages/mask/background/network/gun/encryption/queryPostKey.ts +++ b/packages/mask/background/network/gun/encryption/queryPostKey.ts @@ -104,7 +104,7 @@ namespace Version38Or39 { if (result.status === 'rejected') continue const { encryptedPostKey, target, ivToBePublished } = result.value if (!ivToBePublished) throw new Error('Missing salt') - const pub = await queryPublicKey(target, true) + const pub = await queryPublicKey(target) if (!pub) throw new Error('missing key') const jwk = await CryptoKeyToJsonWebKey(pub) const keyHash = await hashKey38(jwk) diff --git a/packages/mask/background/services/crypto/decryption.ts b/packages/mask/background/services/crypto/decryption.ts index 3448d88e71cf..8f6c97aa976d 100644 --- a/packages/mask/background/services/crypto/decryption.ts +++ b/packages/mask/background/services/crypto/decryption.ts @@ -150,14 +150,14 @@ async function* decryption(payload: string | Uint8Array, context: DecryptionCont return Array.from((await deriveAESByECDH(pub)).values()) }, queryAuthorPublicKey(author, signal) { - return queryPublicKey(author || authorHint, false, signal) + return queryPublicKey(author || authorHint) }, // TODO: get a gun instance async *queryPostKey_version37() { throw new Error('TODO') }, async *queryPostKey_version38(iv, signal) { - const author = await queryPublicKey(context.currentProfile, true) + const author = await queryPublicKey(context.currentProfile) if (!author) throw new DecryptError(DecryptErrorReasons.CurrentProfileDoesNotConnectedToPersona, undefined) yield* GUN_queryPostKey_version39Or38( @@ -169,7 +169,7 @@ async function* decryption(payload: string | Uint8Array, context: DecryptionCont ) }, async *queryPostKey_version39(iv, signal) { - const author = await queryPublicKey(context.currentProfile, true) + const author = await queryPublicKey(context.currentProfile) if (!author) throw new DecryptError(DecryptErrorReasons.CurrentProfileDoesNotConnectedToPersona, undefined) yield* GUN_queryPostKey_version39Or38( From edefd664e30ff9e33ce532855f922b1eb8b9424a Mon Sep 17 00:00:00 2001 From: Jack Works Date: Fri, 18 Mar 2022 19:24:51 +0800 Subject: [PATCH 3/7] fix: e2e bug --- packages/mask/background/network/gun/encryption/queryPostKey.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mask/background/network/gun/encryption/queryPostKey.ts b/packages/mask/background/network/gun/encryption/queryPostKey.ts index dcda8d547093..1deae068c16b 100644 --- a/packages/mask/background/network/gun/encryption/queryPostKey.ts +++ b/packages/mask/background/network/gun/encryption/queryPostKey.ts @@ -153,7 +153,7 @@ namespace Version38Or39 { const N = 2 const hash = await work(encodeArrayBuffer(iv), hashPair) - return networkHint + hash.substring(0, N) + return networkHint + '-' + hash.substring(0, N) } // The difference between V38 and V39 is: V39 is not stable (JSON.stringify) From 28d23ff2216bc1a4c4d188d0681647e05f076029 Mon Sep 17 00:00:00 2001 From: Jack Works Date: Fri, 18 Mar 2022 19:31:40 +0800 Subject: [PATCH 4/7] refactor: remove old encryptTo --- .../CryptoServices/encryptTo.ts | 131 ------------------ .../social-network/utils/text-payload-ui.ts | 5 - .../utils/text-payload-worker.ts | 5 - .../mask/src/utils/type-transform/Payload.ts | 22 +-- 4 files changed, 2 insertions(+), 161 deletions(-) delete mode 100644 packages/mask/src/extension/background-script/CryptoServices/encryptTo.ts delete mode 100644 packages/mask/src/social-network/utils/text-payload-worker.ts diff --git a/packages/mask/src/extension/background-script/CryptoServices/encryptTo.ts b/packages/mask/src/extension/background-script/CryptoServices/encryptTo.ts deleted file mode 100644 index baee77a242fa..000000000000 --- a/packages/mask/src/extension/background-script/CryptoServices/encryptTo.ts +++ /dev/null @@ -1,131 +0,0 @@ -import * as Alpha38 from '../../../crypto/crypto-alpha-38' -import { publishPostAESKey_version39Or38 } from '../../../../background/network/gun/encryption/queryPostKey' -import { decodeArrayBuffer, encodeArrayBuffer } from '@dimensiondev/kit' -import { constructAlpha38 } from '../../../utils/type-transform/Payload' -import { queryPrivateKey, queryLocalKey, queryProfile } from '../../../database' -import { ProfileIdentifier, PostIVIdentifier, PayloadLatest, compressSecp256k1Key } from '@masknet/shared-base' -import { isTypedMessageText, TypedMessage, TypedMessageText } from '@masknet/typed-message' -import { prepareRecipientDetail } from './prepareRecipientDetail' -import { getNetworkWorker } from '../../../social-network/worker' -import { createPostDB, PostRecord } from '../../../../background/database/post' -import { queryPersonaByProfileDB } from '../../../../background/database/persona/db' -import { i18n } from '../../../../shared-ui/locales_legacy' -import { encodeTextPayloadWorker } from '../../../social-network/utils/text-payload-worker' - -type EncryptedText = string -type OthersAESKeyEncryptedToken = string -/** - * This map stores . - */ -const OthersAESKeyEncryptedMap = new Map< - OthersAESKeyEncryptedToken, - [string, Alpha38.PublishedAESKeyRecordV39OrV38[]] ->() - -/** - * Encrypt to a user - * @param content Original text - * @param to Encrypt target - * @param whoAmI Encrypt source - * @returns Will return a tuple of [encrypted: string, token: string] where - * - `encrypted` is the encrypted string - * - `token` is used to call `publishPostAESKey` before post the content - */ -export async function encryptTo( - content: TypedMessage, - to: ProfileIdentifier[], - whoAmI: ProfileIdentifier, - publicShared: boolean, -): Promise<[EncryptedText, OthersAESKeyEncryptedToken]> { - if (publicShared) to = [] - const [recipients, toKey] = await prepareRecipientDetail(to) - - const usingPersona = await queryProfile(whoAmI) - const minePrivateKey = await queryPrivateKey(whoAmI) - if (!minePrivateKey) throw new TypeError('Not initialized yet') - const stringifiedContent = Alpha38.typedMessageStringify(content) - const localKey = publicShared ? Alpha38.publicSharedAESKey : (await queryLocalKey(whoAmI))! - const { - encryptedContent: encryptedText, - othersAESKeyEncrypted, - ownersAESKeyEncrypted, - iv, - postAESKey, - } = await Alpha38.encrypt1ToN({ - version: -38, - content: stringifiedContent, - othersPublicKeyECDH: Array.from(toKey.values()), - ownersLocalKey: localKey, - privateKeyECDH: minePrivateKey, - iv: crypto.getRandomValues(new Uint8Array(16)), - }) - - const payload: PayloadLatest = { - AESKeyEncrypted: encodeArrayBuffer(ownersAESKeyEncrypted), - encryptedText: encodeArrayBuffer(encryptedText), - iv: encodeArrayBuffer(iv), - signature: '', - sharedPublic: publicShared, - version: -38, - authorUserID: whoAmI, - } - try { - const publicKey = (await queryPersonaByProfileDB(whoAmI))?.publicKey - if (publicKey) payload.authorPublicKey = compressSecp256k1Key(publicKey) - } catch { - // ignore - } - - payload.signature = '_' - - const newPostRecord: PostRecord = { - identifier: new PostIVIdentifier(whoAmI.network, payload.iv), - postBy: whoAmI, - postCryptoKey: postAESKey, - recipients: publicShared ? 'everyone' : recipients, - foundAt: new Date(), - encryptBy: usingPersona.linkedPersona?.identifier, - } - if (isTypedMessageText(content)) { - newPostRecord.summary = getSummary(content) - newPostRecord.interestedMeta = content.meta - } - await createPostDB(newPostRecord) - - const postAESKeyToken = encodeArrayBuffer(iv) - const worker = await getNetworkWorker(whoAmI)! - OthersAESKeyEncryptedMap.set(postAESKeyToken, [worker.gunNetworkHint, othersAESKeyEncrypted]) - return [constructAlpha38(payload, await encodeTextPayloadWorker(whoAmI)), postAESKeyToken] -} - -/** - * MUST call before send post, or othersAESKeyEncrypted will not be published to the internet! - * TODO: If we can use PostIVIdentifier to avoid this hacking way to publish PostAESKey? - * @param iv Token that returns in the encryptTo - */ -export async function publishPostAESKey(iv: string) { - const info = OthersAESKeyEncryptedMap.get(iv) - if (!info) throw new Error(i18n.t('service_publish_post_aes_key_failed')) - if (!info[1].length) return - // Use the latest payload version here since we do not accept new post for older version. - await publishPostAESKey_version39Or38(-38, new Uint8Array(decodeArrayBuffer(iv)), info[0], info[1]) -} - -const SUMMARY_MAX_LENGTH = 40 -function getSummary(content: TypedMessageText) { - let result = '' - const sliceLength = content.content.length > SUMMARY_MAX_LENGTH ? SUMMARY_MAX_LENGTH + 1 : SUMMARY_MAX_LENGTH - - // UTF-8 aware summary - if (Intl.Segmenter) { - // it seems like using "en" can also split the word correctly. - const seg = new Intl.Segmenter('en') - for (const word of seg.segment(content.content)) { - if (result.length >= sliceLength) break - result += word.segment - } - } else { - result = content.content.slice(0, sliceLength) - } - return result -} diff --git a/packages/mask/src/social-network/utils/text-payload-ui.ts b/packages/mask/src/social-network/utils/text-payload-ui.ts index 0fc7cfaba502..744ab5bf30d7 100644 --- a/packages/mask/src/social-network/utils/text-payload-ui.ts +++ b/packages/mask/src/social-network/utils/text-payload-ui.ts @@ -1,9 +1,4 @@ import { activatedSocialNetworkUI } from '../ui' -export function encodeTextPayloadUI(x: string): string { - const f = activatedSocialNetworkUI.utils.textPayloadPostProcessor?.encoder - if (f) return f(x) || x - return x -} export function decodeTextPayloadUI(x: string): string[] { const f = activatedSocialNetworkUI.utils.textPayloadPostProcessor?.decoder if (f) return f(x).concat(x) diff --git a/packages/mask/src/social-network/utils/text-payload-worker.ts b/packages/mask/src/social-network/utils/text-payload-worker.ts deleted file mode 100644 index 13be0c5a6fd2..000000000000 --- a/packages/mask/src/social-network/utils/text-payload-worker.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { getNetworkWorker, NetworkWorkerQuery } from '../worker' -export async function encodeTextPayloadWorker(query: NetworkWorkerQuery): Promise<(text: string) => string> { - const f = (await getNetworkWorker(query)).utils.textPayloadPostProcessor?.encoder - return (x) => f?.(x) || x -} diff --git a/packages/mask/src/utils/type-transform/Payload.ts b/packages/mask/src/utils/type-transform/Payload.ts index 1042756774e7..bf634fabf972 100644 --- a/packages/mask/src/utils/type-transform/Payload.ts +++ b/packages/mask/src/utils/type-transform/Payload.ts @@ -3,9 +3,9 @@ import type { SocialNetwork } from '../../social-network' import { isEnvironment, Environment } from '@dimensiondev/holoflows-kit' import { i18n } from '../../../shared-ui/locales_legacy' import { Result, Ok, Err } from 'ts-results' -import { decodeTextPayloadUI, encodeTextPayloadUI } from '../../social-network/utils/text-payload-ui' +import { decodeTextPayloadUI } from '../../social-network/utils/text-payload-ui' -import { Identifier, Payload, PayloadAlpha38, ProfileIdentifier } from '@masknet/shared-base' +import { Identifier, Payload, ProfileIdentifier } from '@masknet/shared-base' /** * Detect if there is version -40, -39 or -38 payload @@ -74,7 +74,6 @@ const versions = new Set([deconstructAlpha40_Or_Alpha39_Or_Alpha38, deconstructA * @type Decoder - defines decoder. if null, auto select decoder. */ type Decoder = SocialNetwork.PayloadEncoding['decoder'] | null -type Encoder = SocialNetwork.PayloadEncoding['encoder'] export function deconstructPayload(str: string, networkDecoder?: Decoder): Result { if (!networkDecoder) { @@ -92,20 +91,3 @@ export function deconstructPayload(str: string, networkDecoder?: Decoder): Resul if (str.includes('\u{1F3BC}') && str.includes(':||')) return Err(new TypeError(i18n.t('service_unknown_payload'))) return Err(new TypeError(i18n.t('payload_not_found'))) } - -export function constructAlpha38(data: PayloadAlpha38, encoder?: Encoder) { - if (!encoder) { - encoder = isEnvironment(Environment.ContentScript) ? encodeTextPayloadUI : (x) => x - } - const userID = data.authorUserID?.toText().replace('person:', '') || '' - const fields = [ - data.AESKeyEncrypted, - data.iv, - data.encryptedText, - data.signature, - data.authorPublicKey, - data.sharedPublic ? '1' : '0', - userID.includes('|') ? undefined : btoa(userID), - ] - return encoder(`\u{1F3BC}4/4|${fields.join('|')}:||`) -} From 5f20e0b9bfd42f0977b561ce4f287d048b775ad1 Mon Sep 17 00:00:00 2001 From: Jack Works Date: Fri, 18 Mar 2022 19:54:30 +0800 Subject: [PATCH 5/7] feat: replace append with new impl --- .../background/database/persona/helper.ts | 11 +++- .../services/crypto/appendEncryption.ts | 63 +++++++++++++++++++ .../background/services/crypto/decryption.ts | 5 +- .../InjectedComponents/PostInspector.tsx | 7 +-- .../background-script/CryptoService.ts | 2 +- 5 files changed, 79 insertions(+), 9 deletions(-) create mode 100644 packages/mask/background/services/crypto/appendEncryption.ts diff --git a/packages/mask/background/database/persona/helper.ts b/packages/mask/background/database/persona/helper.ts index b24a2758ed3d..63c0adce40aa 100644 --- a/packages/mask/background/database/persona/helper.ts +++ b/packages/mask/background/database/persona/helper.ts @@ -1,3 +1,4 @@ +import { safeUnreachable } from '@dimensiondev/kit' import { AESCryptoKey, AESJsonWebKey, @@ -98,7 +99,7 @@ async function getLocalKeyOf(id: ProfileIdentifier, tx: FullPersonaDBTransaction // #endregion // #region ECDH -export async function deriveAESByECDH(pub: EC_Public_CryptoKey, of?: ProfileIdentifier) { +export async function deriveAESByECDH(pub: EC_Public_CryptoKey, of?: ProfileIdentifier | PersonaIdentifier) { const curve = (pub.algorithm as EcKeyAlgorithm).namedCurve || '' const sameCurvePrivateKeys = new IdentifierMap(new Map(), ECKeyIdentifier) @@ -107,7 +108,13 @@ export async function deriveAESByECDH(pub: EC_Public_CryptoKey, of?: ProfileIden for (const persona of personas) { if (!persona.privateKey) continue if (persona.privateKey.crv !== curve) continue - if (of && !persona.linkedProfiles.has(of)) continue + if (of) { + if (of instanceof ProfileIdentifier) { + if (!persona.linkedProfiles.has(of)) continue + } else if (of instanceof ECKeyIdentifier) { + if (!persona.identifier.equals(of)) continue + } else safeUnreachable(of) + } sameCurvePrivateKeys.set(persona.identifier, persona.privateKey) } }) diff --git a/packages/mask/background/services/crypto/appendEncryption.ts b/packages/mask/background/services/crypto/appendEncryption.ts new file mode 100644 index 000000000000..0468cefe13cf --- /dev/null +++ b/packages/mask/background/services/crypto/appendEncryption.ts @@ -0,0 +1,63 @@ +import { appendEncryptionTarget, EC_Key, EC_KeyCurveEnum } from '@masknet/encryption' +import { + EC_Public_CryptoKey, + IdentifierMap, + PostIVIdentifier, + ProfileIdentifier, + RecipientDetail, + RecipientReason, +} from '@masknet/shared-base' +import { deriveAESByECDH, queryPublicKey } from '../../database/persona/helper' +import { updatePostDB, queryPostDB } from '../../database/post' +import { publishPostAESKey_version39Or38 } from '../../network/gun/encryption/queryPostKey' +import { getPostKeyCache } from './decryption' + +export async function appendShareTarget( + version: -38, + post: PostIVIdentifier, + target: ProfileIdentifier[], + whoAmI: ProfileIdentifier, + reason: RecipientReason, +): Promise { + const key = await getPostKeyCache(post) + const postRec = await queryPostDB(post) + const postBy = postRec?.encryptBy || postRec?.postBy || whoAmI + + if (whoAmI?.isUnknown) throw new Error('Cannot find private key') + if (!key) throw new Error('No post key found') + + const e2e = await appendEncryptionTarget( + { + target, + iv: post.toIV(), + postAESKey: key, + version: -38, + }, + { + async deriveAESKey(pub) { + const result = Array.from((await deriveAESByECDH(pub, postBy)).values()) + if (result.length === 0) throw new Error('No key found') + return result[0] + }, + queryPublicKey: (id) => + queryPublicKey(id).then((key): EC_Key | null => + key ? { algr: EC_KeyCurveEnum.secp256k1, key } : null, + ), + }, + ) + + publishPostAESKey_version39Or38(-38, post.toIV(), whoAmI.network, e2e) + + updatePostDB( + { + identifier: post, + recipients: new IdentifierMap( + new Map( + target.map<[string, RecipientDetail]>((identifier) => [identifier.toText(), { reason: [reason] }]), + ), + ProfileIdentifier, + ), + }, + 'append', + ) +} diff --git a/packages/mask/background/services/crypto/decryption.ts b/packages/mask/background/services/crypto/decryption.ts index 8f6c97aa976d..3ef71f695f44 100644 --- a/packages/mask/background/services/crypto/decryption.ts +++ b/packages/mask/background/services/crypto/decryption.ts @@ -201,10 +201,11 @@ function getNetworkHint(x: SocialNetworkEnum) { unreachable(x) } -async function getPostKeyCache(id: PostIVIdentifier) { +/** @internal */ +export 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, [ + const k = await crypto.subtle.importKey('jwk', post.postCryptoKey, { name: 'AES-GCM', length: 256 }, true, [ 'decrypt', ]) return k as AESCryptoKey diff --git a/packages/mask/src/components/InjectedComponents/PostInspector.tsx b/packages/mask/src/components/InjectedComponents/PostInspector.tsx index 251d5e4a3309..b94ed519b540 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 } from '@masknet/shared-base' +import { PostIVIdentifier, ProfileIdentifier } from '@masknet/shared-base' import type { TypedMessageTuple } from '@masknet/typed-message' import type { Profile } from '../../database' import { useCurrentIdentity, useFriendsList } from '../DataSource/useActivatedUI' @@ -52,13 +52,12 @@ export function PostInspector(props: PostInspectorProps) { requestAppendRecipients={ // version -40 does not support append receiver // version -37 is not implemented yet. - ownersKeyEncrypted && iv && version && version !== -40 && version !== -37 + ownersKeyEncrypted && iv && version && version === -38 ? async (profile) => { setAlreadySelectedPreviously(alreadySelectedPreviously.concat(profile)) return Services.Crypto.appendShareTarget( version, - ownersKeyEncrypted, - iv, + new PostIVIdentifier(whoAmI!.identifier.network, iv), profile.map((x) => x.identifier), whoAmI!.identifier, { type: 'direct', at: new Date() }, diff --git a/packages/mask/src/extension/background-script/CryptoService.ts b/packages/mask/src/extension/background-script/CryptoService.ts index 2963d3b09165..7f0a30ad77d2 100644 --- a/packages/mask/src/extension/background-script/CryptoService.ts +++ b/packages/mask/src/extension/background-script/CryptoService.ts @@ -6,7 +6,7 @@ assertEnvironment(Environment.ManifestBackground) export { encryptComment, decryptComment } from '../../crypto/crypto-alpha-40' // export { encryptTo, publishPostAESKey } from './CryptoServices/encryptTo' export { encryptTo } from '../../../background/services/crypto/encryption' -export { appendShareTarget } from './CryptoServices/appendShareTarget' +export { appendShareTarget } from '../../../background/services/crypto/appendEncryption' export { getPartialSharedListOfPost } from './CryptoServices/getPartialSharedListOfPost' export function steganographyEncodeImage(buf: ArrayBuffer, options: Omit) { From 15a0d4fe1b2528430d35eeb9f9103bd2ede16c7b Mon Sep 17 00:00:00 2001 From: Jack Works Date: Fri, 18 Mar 2022 20:42:03 +0800 Subject: [PATCH 6/7] refactor: remove unused code --- .../background/services/crypto/comment.ts | 40 +++ .../InjectedComponents/PostComments.tsx | 7 +- packages/mask/src/crypto/crypto-alpha-38.ts | 37 --- packages/mask/src/crypto/crypto-alpha-39.ts | 86 ------ packages/mask/src/crypto/crypto-alpha-40.ts | 267 ------------------ packages/mask/src/database/Persona/helpers.ts | 32 +-- .../background-script/CryptoService.ts | 3 +- .../CryptoServices/appendShareTarget.ts | 72 ----- .../CryptoServices/cryptoProviderTable.ts | 8 - .../CryptoServices/prepareRecipientDetail.ts | 20 -- .../background-script/IdentityService.ts | 14 +- .../EllipticBackend/methods.ts | 69 ----- .../src/modules/CryptoAlgorithm/WebCrypto.ts | 71 ----- .../src/modules/CryptoAlgorithm/helper.ts | 30 -- .../interfaces/interface.aes.ts | 11 - .../interfaces/interface.blockchain.ts | 13 - .../interfaces/interface.ec.ts | 13 - .../interfaces/interface.pbkdf2.ts | 14 - .../CryptoAlgorithm/interfaces/interface.ts | 20 -- packages/mask/src/modules/workers.ts | 1 - .../defaults/inject/CommentBox.tsx | 7 +- packages/mask/src/social-network/worker.ts | 11 +- .../mask/src/utils/mnemonic-code/index.ts | 11 +- .../utils/mnemonic-code/localKeyGenerate.ts | 19 +- .../type-transform/CryptoKey-JsonWebKey.ts | 54 ---- .../mask/src/utils/type-transform/index.ts | 1 - packages/shared-base/src/crypto/CryptoType.ts | 1 - packages/shared-base/src/crypto/JWKType.ts | 6 - 28 files changed, 84 insertions(+), 854 deletions(-) create mode 100644 packages/mask/background/services/crypto/comment.ts delete mode 100644 packages/mask/src/crypto/crypto-alpha-38.ts delete mode 100644 packages/mask/src/crypto/crypto-alpha-39.ts delete mode 100644 packages/mask/src/crypto/crypto-alpha-40.ts delete mode 100644 packages/mask/src/extension/background-script/CryptoServices/appendShareTarget.ts delete mode 100644 packages/mask/src/extension/background-script/CryptoServices/cryptoProviderTable.ts delete mode 100644 packages/mask/src/extension/background-script/CryptoServices/prepareRecipientDetail.ts delete mode 100644 packages/mask/src/modules/CryptoAlgorithm/EllipticBackend/methods.ts delete mode 100644 packages/mask/src/modules/CryptoAlgorithm/WebCrypto.ts delete mode 100644 packages/mask/src/modules/CryptoAlgorithm/helper.ts delete mode 100644 packages/mask/src/modules/CryptoAlgorithm/interfaces/interface.aes.ts delete mode 100644 packages/mask/src/modules/CryptoAlgorithm/interfaces/interface.blockchain.ts delete mode 100644 packages/mask/src/modules/CryptoAlgorithm/interfaces/interface.ec.ts delete mode 100644 packages/mask/src/modules/CryptoAlgorithm/interfaces/interface.pbkdf2.ts delete mode 100644 packages/mask/src/modules/CryptoAlgorithm/interfaces/interface.ts delete mode 100644 packages/mask/src/modules/workers.ts delete mode 100644 packages/mask/src/utils/type-transform/CryptoKey-JsonWebKey.ts diff --git a/packages/mask/background/services/crypto/comment.ts b/packages/mask/background/services/crypto/comment.ts new file mode 100644 index 000000000000..847390d89e6e --- /dev/null +++ b/packages/mask/background/services/crypto/comment.ts @@ -0,0 +1,40 @@ +import { encodeArrayBuffer, decodeText, decodeArrayBuffer, encodeText } from '@dimensiondev/kit' +import type { AESCryptoKey } from '@masknet/shared-base' + +// eslint-disable-next-line @dimensiondev/unicode-specific-set +// * Payload format: 🎶2/4|encrypted_comment:|| +export async function encryptComment(postIV: Uint8Array, postContent: string, comment: string) { + const key = await getCommentKey(postIV, postContent) + + const encrypted = await crypto.subtle.encrypt({ name: 'AES-GCM', iv: postIV }, key, encodeText(comment)) + return `\u{1F3B6}2/4|${encodeArrayBuffer(encrypted)}:||` +} +export async function decryptComment(postIV: Uint8Array, postContent: string, encryptComment: string) { + const payload = extractCommentPayload(encryptComment) + if (!payload) return null + + const key = await getCommentKey(postIV, postContent) + const result = await crypto.subtle.decrypt({ name: 'AES-GCM', iv: postIV }, key, decodeArrayBuffer(payload)) + return decodeText(result) +} + +async function getCommentKey(postIV: Uint8Array, postContent: string) { + const pbkdf = await crypto.subtle.importKey('raw', encodeText(postContent), 'PBKDF2', false, [ + 'deriveBits', + 'deriveKey', + ]) + return (await crypto.subtle.deriveKey( + { name: 'PBKDF2', salt: postIV, iterations: 100000, hash: 'SHA-256' }, + pbkdf, + { name: 'AES-GCM', length: 256 }, + true, + ['encrypt', 'decrypt'], + )) as AESCryptoKey +} + +function extractCommentPayload(text: string) { + const [_, toEnd] = text.split('\u{1F3B6}2/4|') + const [content, _2] = (toEnd || '').split(':||') + if (content.length) return content + return +} diff --git a/packages/mask/src/components/InjectedComponents/PostComments.tsx b/packages/mask/src/components/InjectedComponents/PostComments.tsx index eed638689914..01712c86c463 100644 --- a/packages/mask/src/components/InjectedComponents/PostComments.tsx +++ b/packages/mask/src/components/InjectedComponents/PostComments.tsx @@ -9,6 +9,7 @@ import { useAsync } from 'react-use' import Services from '../../extension/service' import { extractTextFromTypedMessage } from '@masknet/typed-message' import { usePostInfoDetails } from '../DataSource/usePostInfo' +import { decodeArrayBuffer } from '@dimensiondev/kit' const useStyle = makeStyles()({ root: { @@ -52,7 +53,11 @@ export function PostComment(props: PostCommentProps) { const dec = useAsync(async () => { const decryptedText = extractTextFromTypedMessage(postContent).unwrap() if (!iv || !decryptedText) throw new Error('Decrypt comment failed') - const result = await Services.Crypto.decryptComment(iv, decryptedText, comment) + const result = await Services.Crypto.decryptComment( + new Uint8Array(decodeArrayBuffer(iv)), + decryptedText, + comment, + ) if (result === null) throw new Error('Decrypt result empty') return result }, [iv, postContent, comment]) diff --git a/packages/mask/src/crypto/crypto-alpha-38.ts b/packages/mask/src/crypto/crypto-alpha-38.ts deleted file mode 100644 index ff4554f2fd6d..000000000000 --- a/packages/mask/src/crypto/crypto-alpha-38.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* eslint @dimensiondev/unicode-specific-set: ["error", { "only": "code" }] */ -import { TypedMessage, makeTypedMessageText, isTypedMessageText } from '@masknet/typed-message' -import type { AESJsonWebKey } from '@masknet/shared-base' -export * from './crypto-alpha-39' - -// @ts-ignore -export const publicSharedAESKey: AESJsonWebKey = { - alg: 'A256GCM', - ext: true, - /* cspell:disable-next-line */ - k: '3Bf8BJ3ZPSMUM2jg2ThODeLuRRD_-_iwQEaeLdcQXpg', - key_ops: ['encrypt', 'decrypt'], - kty: 'oct', -} -/** - * With plugin: {"payload": "data"}🧩My message - * Without plugin: My message - */ -export function typedMessageStringify(x: TypedMessage) { - if (!isTypedMessageText(x)) throw new Error('Not supported typed message.') - if (!x.meta || x.meta.size === 0) return x.content - - const obj: Record = {} - for (const [a, b] of x.meta) obj[a] = b - - return JSON.stringify(obj) + '\u{1F9E9}' + x.content -} -export function typedMessageParse(x: string) { - const [maybeMetadata, ...end] = x.split('\u{1F9E9}') - try { - const json: unknown = JSON.parse(maybeMetadata) - if (typeof json !== 'object' || json === null || Object.keys(json).length === 0) - throw new Error('Not a metadata') - return makeTypedMessageText(end.join('\u{1F9E9}'), new Map(Object.entries(json))) - } catch {} - return makeTypedMessageText(x) -} diff --git a/packages/mask/src/crypto/crypto-alpha-39.ts b/packages/mask/src/crypto/crypto-alpha-39.ts deleted file mode 100644 index 924e53c6d36e..000000000000 --- a/packages/mask/src/crypto/crypto-alpha-39.ts +++ /dev/null @@ -1,86 +0,0 @@ -/** - * @deprecated This version of payload is not in use. - * Please goto Crypto alpha v38 - */ -import { - generateOthersAESKeyEncrypted as generateOthersAESKeyEncryptedV40, - encrypt1ToN as encrypt1ToN40, -} from './crypto-alpha-40' -import type { AESJsonWebKey, EC_Private_JsonWebKey, EC_Public_JsonWebKey } from '@masknet/shared-base' -export type PublishedAESKey = { encryptedKey: string; salt: string } -export type PublishedAESKeyRecordV39OrV38 = { - aesKey: PublishedAESKey - receiverKey: EC_Public_JsonWebKey -} -export async function generateOthersAESKeyEncrypted( - version: -39 | -38, - AESKey: AESJsonWebKey, - privateKeyECDH: EC_Private_JsonWebKey, - othersPublicKeyECDH: EC_Public_JsonWebKey[], -): Promise { - const othersPublicKeyECDH_ = othersPublicKeyECDH.map((x, index) => ({ key: x, name: index.toString() })) - const othersAESKeyEncrypted = await generateOthersAESKeyEncryptedV40( - -40, - AESKey, - privateKeyECDH, - othersPublicKeyECDH_, - ) - const othersAESKeyEncrypted_ = othersAESKeyEncrypted.map((key) => ({ - aesKey: key.key, - receiverKey: othersPublicKeyECDH[Number.parseInt(key.name, 10)], - })) - return othersAESKeyEncrypted_ -} -/** - * Encrypt 1 to N - * - * This function is generally based on encrypt1ToN in crypto-alpha-40 - */ -export async function encrypt1ToN(info: { - version: -38 - /** Message to encrypt */ - content: string | ArrayBuffer - /** Your private key */ - privateKeyECDH: EC_Private_JsonWebKey - /** Your local AES key, used to encrypt the random AES key to decrypt the post by yourself */ - ownersLocalKey: AESJsonWebKey - /** Other's public keys. For everyone, will use 1 to 1 encryption to encrypt the random aes key */ - othersPublicKeyECDH: EC_Public_JsonWebKey[] - /** iv */ - iv: ArrayBuffer -}): Promise<{ - version: -38 - encryptedContent: ArrayBuffer - iv: ArrayBuffer - /** Your encrypted post aes key. Should be attached in the post. */ - ownersAESKeyEncrypted: ArrayBuffer - /** All encrypted post aes key. Should be post on the gun. */ - othersAESKeyEncrypted: PublishedAESKeyRecordV39OrV38[] - /** The raw post AESKey. Be aware to protect it! */ - postAESKey: AESJsonWebKey -}> { - const othersPublicKeyECDH = info.othersPublicKeyECDH.map((x, index) => ({ key: x, name: index.toString() })) - const { encryptedContent, iv, othersAESKeyEncrypted, ownersAESKeyEncrypted, postAESKey } = await encrypt1ToN40({ - ...info, - othersPublicKeyECDH, - version: -40, - }) - const othersAESKeyEncrypted_ = othersAESKeyEncrypted.map((key) => ({ - aesKey: key.key, - receiverKey: othersPublicKeyECDH[Number.parseInt(key.name, 10)].key, - })) - return { - encryptedContent, - iv, - version: info.version, - ownersAESKeyEncrypted, - othersAESKeyEncrypted: othersAESKeyEncrypted_, - postAESKey, - } -} - -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 deleted file mode 100644 index 316a1f62560f..000000000000 --- a/packages/mask/src/crypto/crypto-alpha-40.ts +++ /dev/null @@ -1,267 +0,0 @@ -/* eslint @dimensiondev/unicode-specific-set: ["error", { "only": "code" }] */ -/** - * @deprecated This version of payload is not in use. - * Please goto Crypto alpha v38 - */ -import { - encodeText, - encodeArrayBuffer, - decodeArrayBuffer, - decodeText, - concatArrayBuffer, - memoizePromise, -} from '@dimensiondev/kit' -import { CryptoWorker } from '../modules/workers' -import type { EC_Private_JsonWebKey, EC_Public_JsonWebKey, AESJsonWebKey } from '@masknet/shared-base' -import { makeTypedMessageText } from '@masknet/typed-message' -import { - derive_AES_GCM_256_Key_From_PBKDF2, - derive_AES_GCM_256_Key_From_ECDH_256k1_Keys, -} from '../modules/CryptoAlgorithm/helper' -export type PublishedAESKey = { encryptedKey: string; salt: string } -export type PublishedAESKeyRecordV40 = { - key: PublishedAESKey - name: string -} -// #region Derive AES Key from ECDH key -/** - * Derive the key from your private ECDH key and someone else's ECDH key. - * If the key is ECDSA, it will be transform to ECDH. - * - * If you provide the same privateKey, othersPublicKey and salt, the results will be the same. - * @param privateKey Your private key - * @param othersPublicKey Public key of someone you want to derive key to - * @param salt Salt - */ -async function deriveAESKey( - privateKey: EC_Private_JsonWebKey, - othersPublicKey: EC_Public_JsonWebKey, - /** If salt is not provided, we will generate one. And you should send it to your friend. */ - salt: ArrayBuffer | string = crypto.getRandomValues(new Uint8Array(64)), -): Promise<{ iv: ArrayBuffer; salt: ArrayBuffer; key: AESJsonWebKey }> { - const op = othersPublicKey - const pr = privateKey - const derivedKey = await CryptoWorker.aes_to_raw(await derive_AES_GCM_256_Key_From_ECDH_256k1_Keys(pr, op)) - - const _salt = typeof salt === 'string' ? decodeArrayBuffer(salt) : salt - const UntitledUint8Array = concatArrayBuffer(new Uint8Array(derivedKey), _salt) - const password = await crypto.subtle.digest( - 'SHA-256', - concatArrayBuffer(concatArrayBuffer(UntitledUint8Array, _salt), decodeArrayBuffer('KEY')), - ) - const iv_pre = new Uint8Array( - await crypto.subtle.digest( - 'SHA-256', - concatArrayBuffer(concatArrayBuffer(UntitledUint8Array, _salt), decodeArrayBuffer('IV')), - ), - ) - const iv = new Uint8Array(16) - for (let i = 0; i <= 16; i += 1) { - // eslint-disable-next-line no-bitwise - iv[i] = iv_pre[i] ^ iv_pre[16 + i] - } - const key = await CryptoWorker.raw_to_aes(password) - return { key, salt: _salt, iv } -} -// #endregion -// #region encrypt text -/** - * Encrypt 1 to 1 - */ -export async function encrypt1To1(info: { - version: -38 - /** Message that you want to encrypt */ - content: string | ArrayBuffer - /** Your private key */ - privateKeyECDH: EC_Private_JsonWebKey - /** Other's public key */ - othersPublicKeyECDH: EC_Public_JsonWebKey -}): Promise<{ - version: -40 - encryptedContent: ArrayBuffer - salt: ArrayBuffer -}> { - const { version, privateKeyECDH, othersPublicKeyECDH } = info - let { content } = info - if (typeof content === 'string') content = encodeText(content) - - const { iv, key, salt } = await deriveAESKey(privateKeyECDH, othersPublicKeyECDH) - const encryptedContent = await CryptoWorker.encrypt_aes_gcm(key, iv, content) - return { salt, encryptedContent, version: -40 } -} -export async function generateOthersAESKeyEncrypted( - version: -40, - AESKey: AESJsonWebKey, - privateKeyECDH: EC_Private_JsonWebKey, - othersPublicKeyECDH: { key: EC_Public_JsonWebKey; name: string }[], -): Promise { - const exportedAESKey = encodeText(JSON.stringify(AESKey)) - return Promise.all( - othersPublicKeyECDH.map>(async ({ key, name }) => { - const encrypted = await encrypt1To1({ - // This is the deprecated -40 code path - version: -40 as unknown as -38, - content: exportedAESKey, - othersPublicKeyECDH: key, - privateKeyECDH: privateKeyECDH, - }) - return { - name, - key: { - version: -40, - salt: encodeArrayBuffer(encrypted.salt), - encryptedKey: encodeArrayBuffer(encrypted.encryptedContent), - }, - } - }), - ) -} -/** - * Encrypt 1 to N - */ -export async function encrypt1ToN(info: { - version: -40 - /** Message to encrypt */ - content: string | ArrayBuffer - /** Your private key */ - privateKeyECDH: EC_Private_JsonWebKey - /** Your local AES key, used to encrypt the random AES key to decrypt the post by yourself */ - ownersLocalKey: AESJsonWebKey - /** Other's public keys. For everyone, will use 1 to 1 encryption to encrypt the random aes key */ - othersPublicKeyECDH: { key: EC_Public_JsonWebKey; name: string }[] - /** iv */ - iv: ArrayBuffer -}): Promise<{ - version: -40 - encryptedContent: ArrayBuffer - iv: ArrayBuffer - /** Your encrypted post aes key. Should be attached in the post. */ - ownersAESKeyEncrypted: ArrayBuffer - /** All encrypted post aes key. Should be post on the gun. */ - othersAESKeyEncrypted: PublishedAESKeyRecordV40[] - /** The raw post AESKey. Be aware to protect it! */ - postAESKey: AESJsonWebKey -}> { - const { version, content, othersPublicKeyECDH, privateKeyECDH, ownersLocalKey, iv } = info - const AESKey = await CryptoWorker.generate_aes_gcm() - - const encryptedContent = await CryptoWorker.encrypt_aes_gcm( - AESKey, - iv, - typeof content === 'string' ? encodeText(content) : content, - ) - - const exportedAESKey = encodeText(JSON.stringify(AESKey)) - const ownersAESKeyEncrypted = ( - await encryptWithAES({ - aesKey: ownersLocalKey, - content: exportedAESKey, - iv, - }) - ).content - const othersAESKeyEncrypted = await generateOthersAESKeyEncrypted(-40, AESKey, privateKeyECDH, othersPublicKeyECDH) - return { encryptedContent, iv, version: -40, ownersAESKeyEncrypted, othersAESKeyEncrypted, postAESKey: AESKey } -} -// #endregion -// #region decrypt text -export async function extractAESKeyInMessage( - version: -40 | -39 | -38, - encodedEncryptedKey: string | ArrayBuffer, - _iv: string | ArrayBuffer, - myLocalKey: AESJsonWebKey, -): Promise { - const iv = typeof _iv === 'string' ? decodeArrayBuffer(_iv) : _iv - const encryptedKey = - typeof encodedEncryptedKey === 'string' ? decodeArrayBuffer(encodedEncryptedKey) : encodedEncryptedKey - const decryptedAESKeyJWK = JSON.parse( - decodeText(await decryptWithAES({ aesKey: myLocalKey, iv, encrypted: encryptedKey })), - ) - return decryptedAESKeyJWK -} -/** - * Decrypt the content encrypted by AES - */ -export async function decryptWithAES(info: { - encrypted: string | ArrayBuffer - aesKey: AESJsonWebKey - iv: ArrayBuffer | string -}): Promise { - const { aesKey } = info - const iv = typeof info.iv === 'string' ? decodeArrayBuffer(info.iv) : info.iv - const encrypted = typeof info.encrypted === 'string' ? decodeArrayBuffer(info.encrypted) : info.encrypted - return CryptoWorker.decrypt_aes_gcm(aesKey, iv, encrypted) -} -export async function encryptWithAES(info: { - content: string | ArrayBuffer - aesKey: AESJsonWebKey - iv?: ArrayBuffer -}): Promise<{ content: ArrayBuffer; iv: ArrayBuffer }> { - const iv = info.iv ? info.iv : crypto.getRandomValues(new Uint8Array(16)) - const content = typeof info.content === 'string' ? encodeText(info.content) : info.content - - const encrypted = await CryptoWorker.encrypt_aes_gcm(info.aesKey, iv, content) - return { content: encrypted, iv } -} -// #endregion - -// #region Comment -function extractCommentPayload(text: string) { - const [_, toEnd] = text.split('\u{1F3B6}2/4|') - const [content, _2] = (toEnd || '').split(':||') - if (content.length) return content - return -} -const getCommentKey = memoizePromise( - async function (postIV: string, postContent: string) { - const pbkdf = await CryptoWorker.import_pbkdf2(encodeText(postContent)) - const aes = await derive_AES_GCM_256_Key_From_PBKDF2(pbkdf, encodeText(postIV)) - return aes - }, - (a, b) => a + b, -) -// * Payload format: 🎶2/4|encrypted_comment:|| -export async function encryptComment( - postIV: string | ArrayBuffer, - postContent: string | ArrayBuffer, - comment: string | ArrayBuffer, -) { - if (typeof postIV !== 'string') postIV = encodeArrayBuffer(postIV) - if (typeof postContent !== 'string') postContent = decodeText(postContent) - const key = await getCommentKey(postIV as string, postContent as string) - const x = await encryptWithAES({ - content: comment, - aesKey: key, - iv: decodeArrayBuffer(postIV as string), - }) - return `\u{1F3B6}2/4|${encodeArrayBuffer(x.content)}:||` -} -export async function decryptComment( - postIV: string | ArrayBuffer, - postContent: string | ArrayBuffer, - encryptComment: string | ArrayBuffer, -) { - if (typeof postIV !== 'string') postIV = encodeArrayBuffer(postIV) - if (typeof postContent !== 'string') postContent = decodeText(postContent) - if (typeof encryptComment !== 'string') encryptComment = decodeText(encryptComment) - const payload = extractCommentPayload(encryptComment as string) - if (!payload) return null - const key = await getCommentKey(postIV as string, postContent as string) - try { - const x = await decryptWithAES({ - aesKey: key, - iv: decodeArrayBuffer(postIV as string), - encrypted: payload, - }) - return decodeText(x) - } catch { - return null - } -} -// #endregion - -export function typedMessageStringify(x: any) { - throw new Error('Not supported typed message in version older than v39.') -} -export function typedMessageParse(x: string) { - return makeTypedMessageText(x) -} diff --git a/packages/mask/src/database/Persona/helpers.ts b/packages/mask/src/database/Persona/helpers.ts index 1a7071285603..1ef7cbd3fac8 100644 --- a/packages/mask/src/database/Persona/helpers.ts +++ b/packages/mask/src/database/Persona/helpers.ts @@ -21,14 +21,7 @@ import { } from '../../utils/mnemonic-code' import { deriveLocalKeyFromECDHKey } from '../../utils/mnemonic-code/localKeyGenerate' import { validateMnemonic } from 'bip39' -import { - ProfileIdentifier, - type PersonaIdentifier, - IdentifierMap, - type EC_Public_JsonWebKey, - type EC_Private_JsonWebKey, - type AESJsonWebKey, -} from '@masknet/shared-base' +import { ProfileIdentifier, type PersonaIdentifier, IdentifierMap } from '@masknet/shared-base' import { createPersonaByJsonWebKey } from '../../../background/database/persona/helper' export async function profileRecordToProfile(record: ProfileRecord): Promise { @@ -177,17 +170,6 @@ export function queryPersonaRecord(i: ProfileIdentifier | PersonaIdentifier): Pr return i instanceof ProfileIdentifier ? queryPersonaByProfileDB(i) : queryPersonaDB(i) } -export async function queryPublicKey( - i: ProfileIdentifier | PersonaIdentifier, -): Promise { - return queryPersonaRecord(i).then((x) => x?.publicKey) -} -export async function queryPrivateKey( - i: ProfileIdentifier | PersonaIdentifier, -): Promise { - return queryPersonaRecord(i).then((x) => x?.privateKey) -} - export async function createPersonaByMnemonic( nickname: string | undefined, password: string, @@ -224,15 +206,3 @@ export async function createPersonaByMnemonicV2(mnemonicWord: string, nickname: uninitialized: false, }) } - -export async function queryLocalKey(i: ProfileIdentifier | PersonaIdentifier): Promise { - if (i instanceof ProfileIdentifier) { - const profile = await queryProfileDB(i) - if (!profile) return null - if (profile.localKey) return profile.localKey - if (!profile.linkedPersona) return null - return queryLocalKey(profile.linkedPersona) - } else { - return (await queryPersonaDB(i))?.localKey ?? null - } -} diff --git a/packages/mask/src/extension/background-script/CryptoService.ts b/packages/mask/src/extension/background-script/CryptoService.ts index 7f0a30ad77d2..652536b0dcab 100644 --- a/packages/mask/src/extension/background-script/CryptoService.ts +++ b/packages/mask/src/extension/background-script/CryptoService.ts @@ -3,9 +3,8 @@ import { steganographyEncodeImage as __steganographyEncodeImage, EncodeImageOpti import { steganographyDownloadImage } from './CryptoServices/utils' assertEnvironment(Environment.ManifestBackground) -export { encryptComment, decryptComment } from '../../crypto/crypto-alpha-40' -// export { encryptTo, publishPostAESKey } from './CryptoServices/encryptTo' export { encryptTo } from '../../../background/services/crypto/encryption' +export { encryptComment, decryptComment } from '../../../background/services/crypto/comment' export { appendShareTarget } from '../../../background/services/crypto/appendEncryption' export { getPartialSharedListOfPost } from './CryptoServices/getPartialSharedListOfPost' diff --git a/packages/mask/src/extension/background-script/CryptoServices/appendShareTarget.ts b/packages/mask/src/extension/background-script/CryptoServices/appendShareTarget.ts deleted file mode 100644 index f18e5032fad2..000000000000 --- a/packages/mask/src/extension/background-script/CryptoServices/appendShareTarget.ts +++ /dev/null @@ -1,72 +0,0 @@ -import * as Alpha39 from '../../../crypto/crypto-alpha-39' -import { - ProfileIdentifier, - PostIVIdentifier, - IdentifierMap, - AESJsonWebKey, - EC_Private_JsonWebKey, -} from '@masknet/shared-base' -import { prepareRecipientDetail } from './prepareRecipientDetail' -import { cryptoProviderTable } from './cryptoProviderTable' -import { updatePostDB, RecipientDetail, RecipientReason } from '../../../../background/database/post' -import { getNetworkWorkerUninitialized } from '../../../social-network/worker' -import { queryPrivateKey, queryLocalKey } from '../../../database' -import { publishPostAESKey_version39Or38 } from '../../../../background/network/gun/encryption/queryPostKey' -import { decodeArrayBuffer } from '@dimensiondev/kit' -export async function appendShareTarget( - version: -40 | -39 | -38, - postAESKey: string | AESJsonWebKey, - iv: string, - people: ProfileIdentifier[], - whoAmI: ProfileIdentifier, - reason: RecipientReason, -): Promise { - const cryptoProvider = cryptoProviderTable[version] - if (typeof postAESKey === 'string') { - const AESKey = await cryptoProvider.extractAESKeyInMessage( - version, - postAESKey, - iv, - (await queryLocalKey(whoAmI))!, - ) - return appendShareTarget(version, AESKey, iv, people, whoAmI, reason) - } - const myPrivateKey: EC_Private_JsonWebKey = (await queryPrivateKey(whoAmI))! - if (version === -39 || version === -38) { - const [, toKey] = await prepareRecipientDetail(people) - const othersAESKeyEncrypted = await Alpha39.generateOthersAESKeyEncrypted( - version, - postAESKey, - myPrivateKey, - Array.from(toKey.values()), - ) - const gunHint = getNetworkWorkerUninitialized(whoAmI)?.gunNetworkHint - gunHint && - publishPostAESKey_version39Or38( - version, - new Uint8Array(decodeArrayBuffer(iv)), - gunHint, - othersAESKeyEncrypted, - ) - updatePostDB( - { - identifier: new PostIVIdentifier(whoAmI.network, iv), - recipients: new IdentifierMap( - new Map( - people.map<[string, RecipientDetail]>((identifier) => [ - identifier.toText(), - { - reason: [reason], - published: toKey.has(identifier), - }, - ]), - ), - ProfileIdentifier, - ), - }, - 'append', - ) - } else if (version === -40) { - throw new TypeError('Version -40 cannot create new data anymore due to leaking risks.') - } -} diff --git a/packages/mask/src/extension/background-script/CryptoServices/cryptoProviderTable.ts b/packages/mask/src/extension/background-script/CryptoServices/cryptoProviderTable.ts deleted file mode 100644 index 83f869bdfbdd..000000000000 --- a/packages/mask/src/extension/background-script/CryptoServices/cryptoProviderTable.ts +++ /dev/null @@ -1,8 +0,0 @@ -import * as Alpha38 from '../../../crypto/crypto-alpha-38' -import * as Alpha39 from '../../../crypto/crypto-alpha-39' -import * as Alpha40 from '../../../crypto/crypto-alpha-40' -export const cryptoProviderTable = { - [-40]: Alpha40, - [-39]: Alpha39, - [-38]: Alpha38, -} as const diff --git a/packages/mask/src/extension/background-script/CryptoServices/prepareRecipientDetail.ts b/packages/mask/src/extension/background-script/CryptoServices/prepareRecipientDetail.ts deleted file mode 100644 index 7caf417112be..000000000000 --- a/packages/mask/src/extension/background-script/CryptoServices/prepareRecipientDetail.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { queryPublicKey } from '../../../database' -import type { RecipientDetail, RecipientReason } from '../../../../background/database/post' -import { ProfileIdentifier, IdentifierMap, type EC_Public_JsonWebKey } from '@masknet/shared-base' - -export async function prepareRecipientDetail(to: ProfileIdentifier[]) { - const recipients = new IdentifierMap(new Map(), ProfileIdentifier) - const keys = new IdentifierMap(new Map(), ProfileIdentifier) - await Promise.all( - to.map(async function self(who, _, __, detail: RecipientReason = { at: new Date(), type: 'direct' }) { - const pub = await queryPublicKey(who) - if (pub) keys.set(who, pub) - append(who, detail) - }), - ) - function append(who: ProfileIdentifier, reason: RecipientReason) { - if (!recipients.has(who)) recipients.set(who, { reason: [] }) - recipients.get(who)!.reason.push(reason) - } - return [recipients, keys] as const -} diff --git a/packages/mask/src/extension/background-script/IdentityService.ts b/packages/mask/src/extension/background-script/IdentityService.ts index 50d0d9f85b68..f63b99a4b4ff 100644 --- a/packages/mask/src/extension/background-script/IdentityService.ts +++ b/packages/mask/src/extension/background-script/IdentityService.ts @@ -58,9 +58,11 @@ import { convertBackupFileToObject, fixBackupFilePermission } from '../../utils/ import { assertEnvironment, Environment } from '@dimensiondev/holoflows-kit' import { getCurrentPersonaIdentifier } from './SettingsService' import { MaskMessages } from '../../utils' -import { split_ec_k256_keypair_into_pub_priv } from '../../modules/CryptoAlgorithm/helper' import { first, orderBy } from 'lodash-unified' -import { recover_ECDH_256k1_KeyPair_ByMnemonicWord } from '../../utils/mnemonic-code' +import { + recover_ECDH_256k1_KeyPair_ByMnemonicWord, + split_ec_k256_keypair_into_pub_priv, +} from '../../utils/mnemonic-code' assertEnvironment(Environment.ManifestBackground) @@ -113,13 +115,7 @@ export function removeProfile(id: ProfileIdentifier): Promise { // #endregion // #region Persona -export { - queryPersona, - createPersonaByMnemonic, - createPersonaByMnemonicV2, - renamePersona, - queryPrivateKey, -} from '../../database' +export { queryPersona, createPersonaByMnemonic, createPersonaByMnemonicV2, renamePersona } from '../../database' export async function queryPersonaByMnemonic(mnemonic: string, password: '') { const verify = validateMnemonic(mnemonic) diff --git a/packages/mask/src/modules/CryptoAlgorithm/EllipticBackend/methods.ts b/packages/mask/src/modules/CryptoAlgorithm/EllipticBackend/methods.ts deleted file mode 100644 index 0fbdd1b2a85b..000000000000 --- a/packages/mask/src/modules/CryptoAlgorithm/EllipticBackend/methods.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { WebCryptoNotSupportedMethods, WebCryptoSupportedMethods, WebCryptoMethods } from '../WebCrypto' -import { - getKeyParameter, - CryptoKeyToJsonWebKey, - JsonWebKeyToCryptoKey, -} from '../../../utils/type-transform/CryptoKey-JsonWebKey' -import { - generate_ECDH_256k1_KeyPair_ByMnemonicWord, - recover_ECDH_256k1_KeyPair_ByMnemonicWord, - MnemonicGenerationInformation, -} from '../../../utils/mnemonic-code' -import type { MnemonicWordDetail } from '../interfaces/interface.blockchain' - -const ECDH = getKeyParameter('ecdh')[0] -const ECDSA = getKeyParameter('ecdsa')[0] -function initEllipticBackend(_: WebCryptoSupportedMethods): WebCryptoNotSupportedMethods { - return { - async generate_ec_k256_pair() { - const { privateKey, publicKey } = await crypto.subtle.generateKey( - { name: 'ECDH', namedCurve: 'K-256' }, - true, - [...ECDH], - ) - return { - privateKey: await CryptoKeyToJsonWebKey(privateKey!), - publicKey: await CryptoKeyToJsonWebKey(publicKey!), - } - }, - async derive_aes_from_ecdh_k256(priv, pub, aes = 'AES-GCM', length = 256) { - const key = await crypto.subtle.deriveKey( - { name: 'ECDH', public: await JsonWebKeyToCryptoKey(pub, ...getKeyParameter('ecdh')) }, - await JsonWebKeyToCryptoKey(priv, ...getKeyParameter('ecdh')), - { name: aes, length }, - true, - ['encrypt', 'decrypt'], - ) - return CryptoKeyToJsonWebKey(key) - }, - async generate_ecdh_k256_from_mnemonic(password) { - return _helper(await generate_ECDH_256k1_KeyPair_ByMnemonicWord(password)) - }, - async recover_ecdh_k256_from_mnemonic(words, password) { - return _helper(await recover_ECDH_256k1_KeyPair_ByMnemonicWord(words, password)) - }, - } -} -async function _helper(x: MnemonicGenerationInformation): Promise { - const { - key: { privateKey, publicKey }, - mnemonicRecord: { - parameter: { path, withPassword }, - words, - }, - password, - } = x - return { - mnemonic_words: words, - parameter_path: path, - parameter_with_password: withPassword, - password, - privateKey, - publicKey, - } -} - -export default { - ...WebCryptoMethods, - ...initEllipticBackend(WebCryptoMethods), -} diff --git a/packages/mask/src/modules/CryptoAlgorithm/WebCrypto.ts b/packages/mask/src/modules/CryptoAlgorithm/WebCrypto.ts deleted file mode 100644 index f7410e65f982..000000000000 --- a/packages/mask/src/modules/CryptoAlgorithm/WebCrypto.ts +++ /dev/null @@ -1,71 +0,0 @@ -import type { CryptoAlgorithmProviderMethods } from './interfaces/interface' -import { - JsonWebKeyToCryptoKey, - getKeyParameter, - CryptoKeyToJsonWebKey, -} from '../../utils/type-transform/CryptoKey-JsonWebKey' -import type { AESName } from './interfaces/interface.aes' -import type { AESJsonWebKey, PBKDF2UnknownKey } from '@masknet/shared-base' - -export type WebCryptoSupportedMethods = Pick< - CryptoAlgorithmProviderMethods, - | 'decrypt_aes_gcm' - | 'derive_aes_from_pbkdf2' - | 'digest_sha' - | 'encrypt_aes_gcm' - | 'generate_aes_gcm' - | 'import_pbkdf2' - | 'aes_to_raw' - | 'raw_to_aes' -> -export type WebCryptoNotSupportedMethods = Omit - -/** - * AES-GCM & pbkdf2 related algorithms are WebCrypto supported - * so it is safe to run it on the main thread (browser will make it async) - */ -export const WebCryptoMethods: WebCryptoSupportedMethods = { - async encrypt_aes_gcm(jwk, iv, message) { - const key = await JsonWebKeyToCryptoKey(jwk, ...getKeyParameter('aes')) - return crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, message) - }, - async decrypt_aes_gcm(jwk, iv, message) { - const key = await JsonWebKeyToCryptoKey(jwk, ...getKeyParameter('aes')) - return crypto.subtle.decrypt({ name: 'AES-GCM', iv }, key, message) - }, - async generate_aes_gcm(length = 256) { - const key = await crypto.subtle.generateKey({ name: 'AES-GCM', length }, true, ['encrypt', 'decrypt']) - return CryptoKeyToJsonWebKey(key) - }, - async derive_aes_from_pbkdf2(pbkdf, salt, hash, aes_algr, aes_length, iterations) { - if (!(pbkdf instanceof CryptoKey)) throw new TypeError('Expect PBKDF2UnknownKey to be a CryptoKey at runtime') - const aes = await crypto.subtle.deriveKey( - { name: 'PBKDF2', salt, iterations, hash }, - pbkdf as unknown as CryptoKey, - { name: aes_algr, length: aes_length }, - true, - ['encrypt', 'decrypt'], - ) - return CryptoKeyToJsonWebKey(aes) - }, - digest_sha(alg, data) { - return crypto.subtle.digest(alg, data) - }, - async import_pbkdf2(seed) { - const key = await crypto.subtle.importKey('raw', seed, 'PBKDF2', false, ['deriveBits', 'deriveKey']) - // In the WebCrypto spec, it is not exportable. We choose CryptoKey as our PBKDF2UnknownKey - return key as CryptoKey as unknown as PBKDF2UnknownKey - }, - async aes_to_raw(aes, name: AESName = 'AES-GCM', length: 256 = 256) { - const cryptoKey = await crypto.subtle.importKey('jwk', aes, { name, length } as AesKeyAlgorithm, true, [ - ...getKeyParameter('aes')[0], - ]) - return crypto.subtle.exportKey('raw', cryptoKey) - }, - async raw_to_aes(raw, name: AESName = 'AES-GCM', length: 256 = 256) { - const cryptoKey = await crypto.subtle.importKey('raw', raw, { name, length } as AesKeyAlgorithm, true, [ - ...getKeyParameter('aes')[0], - ]) - return (await crypto.subtle.exportKey('jwk', cryptoKey)) as JsonWebKey as unknown as AESJsonWebKey - }, -} diff --git a/packages/mask/src/modules/CryptoAlgorithm/helper.ts b/packages/mask/src/modules/CryptoAlgorithm/helper.ts deleted file mode 100644 index 36b812a9bf65..000000000000 --- a/packages/mask/src/modules/CryptoAlgorithm/helper.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { CryptoWorker } from '../workers' -import type { - PBKDF2UnknownKey, - EC_Public_JsonWebKey, - EC_Private_JsonWebKey, - JsonWebKeyPair, -} from '@masknet/shared-base' - -export function derive_AES_GCM_256_Key_From_PBKDF2( - pbkdf: PBKDF2UnknownKey, - iv: ArrayBuffer, - hash: 'SHA-256' = 'SHA-256', - iterations = 100000, -) { - return CryptoWorker.derive_aes_from_pbkdf2(pbkdf, iv, hash, 'AES-GCM', 256, iterations) -} - -export function derive_AES_GCM_256_Key_From_ECDH_256k1_Keys(priv: EC_Private_JsonWebKey, pub: EC_Public_JsonWebKey) { - return CryptoWorker.derive_aes_from_ecdh_k256(priv, pub, 'AES-GCM', 256) -} - -export async function split_ec_k256_keypair_into_pub_priv( - key: Readonly, -): Promise> { - const { d, ...pub } = key - if (!d) throw new TypeError('split_ec_k256_keypair_into_pub_priv requires a private key (jwk.d)') - // TODO: maybe should do some extra check on properties - // @ts-expect-error Do a force transform - return { privateKey: { ...key }, publicKey: pub } -} diff --git a/packages/mask/src/modules/CryptoAlgorithm/interfaces/interface.aes.ts b/packages/mask/src/modules/CryptoAlgorithm/interfaces/interface.aes.ts deleted file mode 100644 index 2c903943a2f7..000000000000 --- a/packages/mask/src/modules/CryptoAlgorithm/interfaces/interface.aes.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { AESJsonWebKey } from '@masknet/shared-base' - -export interface AESMethods { - // length 128 or 192 is not used - generate_aes_gcm(length?: 256): PromiseLike - encrypt_aes_gcm(aes: AESJsonWebKey, iv: ArrayBuffer, message: ArrayBuffer): PromiseLike - decrypt_aes_gcm(aes: AESJsonWebKey, iv: ArrayBuffer, message: ArrayBuffer): PromiseLike - aes_to_raw(aes: AESJsonWebKey): PromiseLike - raw_to_aes(raw: ArrayBuffer): PromiseLike -} -export type AESName = 'AES-GCM' // | 'AES-CBC' | 'AES-CTR' | 'AES-KW' diff --git a/packages/mask/src/modules/CryptoAlgorithm/interfaces/interface.blockchain.ts b/packages/mask/src/modules/CryptoAlgorithm/interfaces/interface.blockchain.ts deleted file mode 100644 index 74af0da2935b..000000000000 --- a/packages/mask/src/modules/CryptoAlgorithm/interfaces/interface.blockchain.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { EC_Public_JsonWebKey, EC_Private_JsonWebKey, JsonWebKeyPair } from '@masknet/shared-base' -export interface BlockChainMethods { - /** Blockchain related fns */ - generate_ecdh_k256_from_mnemonic(password: string): PromiseLike - recover_ecdh_k256_from_mnemonic(mnemonic: string, password: string): PromiseLike -} - -export type MnemonicWordDetail = JsonWebKeyPair & { - password: string - mnemonic_words: string - parameter_path: string - parameter_with_password: boolean -} diff --git a/packages/mask/src/modules/CryptoAlgorithm/interfaces/interface.ec.ts b/packages/mask/src/modules/CryptoAlgorithm/interfaces/interface.ec.ts deleted file mode 100644 index 48d65379be0f..000000000000 --- a/packages/mask/src/modules/CryptoAlgorithm/interfaces/interface.ec.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { AESName } from './interface.aes' -import type { JsonWebKeyPair, EC_Public_JsonWebKey, EC_Private_JsonWebKey, AESJsonWebKey } from '@masknet/shared-base' -type GenerateKeyPair = () => PromiseLike> -type DeriveAES = ( - priv: EC_Private_JsonWebKey, - pub: EC_Public_JsonWebKey, - aes: AESName, - length: 256, -) => PromiseLike -export interface ECMethods { - generate_ec_k256_pair: GenerateKeyPair - derive_aes_from_ecdh_k256: DeriveAES -} diff --git a/packages/mask/src/modules/CryptoAlgorithm/interfaces/interface.pbkdf2.ts b/packages/mask/src/modules/CryptoAlgorithm/interfaces/interface.pbkdf2.ts deleted file mode 100644 index ce14175397c6..000000000000 --- a/packages/mask/src/modules/CryptoAlgorithm/interfaces/interface.pbkdf2.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { ShaName } from './interface' -import type { AESName } from './interface.aes' -import type { PBKDF2UnknownKey, AESJsonWebKey } from '@masknet/shared-base' -export interface PBKDF2Methods { - import_pbkdf2(seed: ArrayBuffer): PromiseLike - derive_aes_from_pbkdf2( - pbkdf: PBKDF2UnknownKey, - iv: ArrayBuffer, - sha_algr: ShaName, - aes_algr: AESName, - aes_length: 256, - iterations: number, - ): PromiseLike -} diff --git a/packages/mask/src/modules/CryptoAlgorithm/interfaces/interface.ts b/packages/mask/src/modules/CryptoAlgorithm/interfaces/interface.ts deleted file mode 100644 index 1e84cd0fdaae..000000000000 --- a/packages/mask/src/modules/CryptoAlgorithm/interfaces/interface.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Design goal: - * - Minimal set of Crypto operations that Mask needs - * - Independent of Web Crypto API - * - Easy to implement for other languages (to WASM) - * - Run in Web Worker to speed up - * - Easy to compose different backend use spread(...) operator - * - * Function names: operation_algorithm_other - * Signature: - * - No native CryptoKey, JsonWebKey only - */ -import type { ECMethods } from './interface.ec' -import type { AESMethods } from './interface.aes' -import type { PBKDF2Methods } from './interface.pbkdf2' -import type { BlockChainMethods } from './interface.blockchain' -export interface CryptoAlgorithmProviderMethods extends AESMethods, ECMethods, PBKDF2Methods, BlockChainMethods { - digest_sha(alg: ShaName, data: ArrayBuffer): PromiseLike -} -export type ShaName = 'SHA-256' // | 'SHA-384' | 'SHA-512' diff --git a/packages/mask/src/modules/workers.ts b/packages/mask/src/modules/workers.ts deleted file mode 100644 index d0e1ad5b3b6f..000000000000 --- a/packages/mask/src/modules/workers.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as CryptoWorker } from './CryptoAlgorithm/EllipticBackend/methods' diff --git a/packages/mask/src/social-network/defaults/inject/CommentBox.tsx b/packages/mask/src/social-network/defaults/inject/CommentBox.tsx index 94a25f51fee7..9a15e5c53904 100644 --- a/packages/mask/src/social-network/defaults/inject/CommentBox.tsx +++ b/packages/mask/src/social-network/defaults/inject/CommentBox.tsx @@ -10,6 +10,7 @@ import { noop } from 'lodash-unified' import { MaskMessages } from '../../../utils/messages' import { startWatch } from '../../../utils/watcher' import { extractTextFromTypedMessage } from '@masknet/typed-message' +import { decodeArrayBuffer } from '@dimensiondev/kit' const defaultOnPasteToCommentBox = async ( encryptedComment: string, @@ -36,7 +37,11 @@ export const injectCommentBoxDefaultFactory = function ( const onCallback = useCallback( async (content) => { const decryptedText = extractTextFromTypedMessage(postContent).unwrap() - const encryptedComment = await Services.Crypto.encryptComment(iv!, decryptedText, content) + const encryptedComment = await Services.Crypto.encryptComment( + new Uint8Array(decodeArrayBuffer(iv!)), + decryptedText, + content, + ) onPasteToCommentBox(encryptedComment, info!, dom).catch(console.error) }, [postContent, info, dom, iv], diff --git a/packages/mask/src/social-network/worker.ts b/packages/mask/src/social-network/worker.ts index 6ac06df931e3..37327e56384e 100644 --- a/packages/mask/src/social-network/worker.ts +++ b/packages/mask/src/social-network/worker.ts @@ -21,16 +21,7 @@ async function activateNetworkWorker(id: string): Promise { +export async function getNetworkWorker(network: string | ProfileIdentifier): Promise { if (typeof network === 'string') return activateNetworkWorker(network) return getNetworkWorker(network.network) } - -export function getNetworkWorkerUninitialized( - network: string | ProfileIdentifier, -): SocialNetworkWorker.DeferredDefinition | undefined { - if (typeof network === 'string') - return [...definedSocialNetworkWorkers].find((x) => x.networkIdentifier === network) - return getNetworkWorkerUninitialized(network.network) -} diff --git a/packages/mask/src/utils/mnemonic-code/index.ts b/packages/mask/src/utils/mnemonic-code/index.ts index a474bc0e6cf3..e180cd4994a1 100644 --- a/packages/mask/src/utils/mnemonic-code/index.ts +++ b/packages/mask/src/utils/mnemonic-code/index.ts @@ -9,7 +9,6 @@ import { toBase64URL, decompressSecp256k1Key, } from '@masknet/shared-base' -import { split_ec_k256_keypair_into_pub_priv } from '../../modules/CryptoAlgorithm/helper' // Private key at m/44'/coinType'/account'/change/addressIndex // coinType = ether @@ -67,3 +66,13 @@ function HDKeyToJwk(hdk: wallet.HDKey): JsonWebKey { jwk.d = hdk.privateKey ? toBase64URL(hdk.privateKey) : undefined return jwk } + +export async function split_ec_k256_keypair_into_pub_priv( + key: Readonly, +): Promise> { + const { d, ...pub } = key + if (!d) throw new TypeError('split_ec_k256_keypair_into_pub_priv requires a private key (jwk.d)') + // TODO: maybe should do some extra check on properties + // @ts-expect-error Do a force transform + return { privateKey: { ...key }, publicKey: pub } +} diff --git a/packages/mask/src/utils/mnemonic-code/localKeyGenerate.ts b/packages/mask/src/utils/mnemonic-code/localKeyGenerate.ts index 3ed5dcdf1d5e..3d29fd7eab9d 100644 --- a/packages/mask/src/utils/mnemonic-code/localKeyGenerate.ts +++ b/packages/mask/src/utils/mnemonic-code/localKeyGenerate.ts @@ -1,7 +1,6 @@ import { encodeText } from '@dimensiondev/kit' -import { CryptoWorker } from '../../modules/workers' -import type { EC_Public_JsonWebKey, AESJsonWebKey } from '@masknet/shared-base' -import { derive_AES_GCM_256_Key_From_PBKDF2 } from '../../modules/CryptoAlgorithm/helper' +import type { EC_Public_JsonWebKey, AESJsonWebKey, AESCryptoKey } from '@masknet/shared-base' +import { CryptoKeyToJsonWebKey } from '../../../utils-pure' /** * Local key (AES key) is used to encrypt message to myself. @@ -13,6 +12,16 @@ export async function deriveLocalKeyFromECDHKey( mnemonicWord: string, ): Promise { // ? Derive method: publicKey as "password" and password for the mnemonicWord as hash - const pbkdf2 = await CryptoWorker.import_pbkdf2(encodeText(pub.x! + pub.y!)) - return derive_AES_GCM_256_Key_From_PBKDF2(pbkdf2, encodeText(mnemonicWord)) + const pbkdf2 = await crypto.subtle.importKey('raw', encodeText(pub.x! + pub.y!), 'PBKDF2', false, [ + 'deriveBits', + 'deriveKey', + ]) + const aes = await crypto.subtle.deriveKey( + { name: 'PBKDF2', salt: encodeText(mnemonicWord), iterations: 100000, hash: 'SHA-256' }, + pbkdf2, + { name: 'AES-GCM', length: 256 }, + true, + ['encrypt', 'decrypt'], + ) + return CryptoKeyToJsonWebKey(aes as AESCryptoKey) } diff --git a/packages/mask/src/utils/type-transform/CryptoKey-JsonWebKey.ts b/packages/mask/src/utils/type-transform/CryptoKey-JsonWebKey.ts deleted file mode 100644 index ae6cb8821595..000000000000 --- a/packages/mask/src/utils/type-transform/CryptoKey-JsonWebKey.ts +++ /dev/null @@ -1,54 +0,0 @@ -import stableStringify from 'json-stable-stringify' -const CryptoKeyCache = new Map() -const JsonWebKeyCache = new WeakMap() - -type Algorithms = string | RsaHashedImportParams | EcKeyImportParams | HmacImportParams | AesKeyAlgorithm - -export function getKeyParameter( - type: 'ecdh' | 'ecdsa' | 'aes' | 'pbkdf2', -): [readonly KeyUsage[], Readonly] { - if (type === 'ecdh') return [['deriveKey', 'deriveBits'], { name: 'ECDH', namedCurve: 'K-256' }] - if (type === 'aes') return [['encrypt', 'decrypt'], { name: 'AES-GCM', length: 256 }] - if (type === 'ecdsa') return [['sign', 'verify'], { name: 'ecdsa', namedCurve: 'K-256' }] - throw new TypeError('Invalid key type') -} - -/** - * Get a (cached) CryptoKey from JsonWebKey - * - * JsonWebKeyToCryptoKey(key, ...getKeyParameter('aes')) - * - * @param algorithm - use which algorithm to import this key, defaults to ECDH K-256 - * @param key - The JsonWebKey - * @param usage - Usage - */ -export async function JsonWebKeyToCryptoKey( - key: JsonWebKey, - usage: readonly KeyUsage[], - algorithm: Algorithms, -): Promise { - key = { ...key, key_ops: [...usage] } - - const usages = [...usage].sort().join(',') - const _key = stableStringify(key) + usages - if (CryptoKeyCache.has(_key)) return CryptoKeyCache.get(_key)! - const cryptoKey = await crypto.subtle.importKey('jwk', key, algorithm, true, [...usage]) - CryptoKeyCache.set(_key, cryptoKey) - JsonWebKeyCache.set(cryptoKey, key) - return cryptoKey -} - -/** - * Get a (cached) JsonWebKey from CryptoKey - * @param key - The CryptoKey - */ -export async function CryptoKeyToJsonWebKey(key: CryptoKey): Promise { - // Any of nominal subtype of JsonWebKey in this project is runtime equivalent to JsonWebKey - // so it is safe to do the force cast - if (JsonWebKeyCache.has(key)) return JsonWebKeyCache.get(key)! as T - const jwk = await crypto.subtle.exportKey('jwk', key) - JsonWebKeyCache.set(key, jwk) - const hash = stableStringify(jwk) + [...key.usages].sort().join(',') - CryptoKeyCache.set(hash, key) - return jwk as T -} diff --git a/packages/mask/src/utils/type-transform/index.ts b/packages/mask/src/utils/type-transform/index.ts index f19d98fa2759..a7b5a5bed1ca 100644 --- a/packages/mask/src/utils/type-transform/index.ts +++ b/packages/mask/src/utils/type-transform/index.ts @@ -1,6 +1,5 @@ export * from './asyncIteratorHelpers' export * from './BackupFile' export * from './BackupFormat' -export * from './CryptoKey-JsonWebKey' export * from './Payload' export * from './SECP256k1-ETH' diff --git a/packages/shared-base/src/crypto/CryptoType.ts b/packages/shared-base/src/crypto/CryptoType.ts index d81fe73f3c82..aa44afe9131d 100644 --- a/packages/shared-base/src/crypto/CryptoType.ts +++ b/packages/shared-base/src/crypto/CryptoType.ts @@ -5,7 +5,6 @@ export type EC_CryptoKey = EC_Private_CryptoKey | EC_Public_CryptoKey export interface EC_Public_CryptoKey extends CryptoKey, Nominal<'EC public'> {} export interface EC_Private_CryptoKey extends CryptoKey, Nominal<'EC private'> {} export interface AESCryptoKey extends CryptoKey, Nominal<'AES'> {} -export interface PBKDF2CryptoKey extends Nominal<'PBKDF2'> {} declare class Nominal { /** Ghost property, don't use it! */ private __brand: T diff --git a/packages/shared-base/src/crypto/JWKType.ts b/packages/shared-base/src/crypto/JWKType.ts index f7f67c415fc2..706d3b06fe8a 100644 --- a/packages/shared-base/src/crypto/JWKType.ts +++ b/packages/shared-base/src/crypto/JWKType.ts @@ -9,12 +9,6 @@ export type EC_JsonWebKey = EC_Private_JsonWebKey | EC_Public_JsonWebKey export interface EC_Public_JsonWebKey extends JsonWebKey, Nominal<'EC public'> {} export interface EC_Private_JsonWebKey extends JsonWebKey, Nominal<'EC private'> {} export interface AESJsonWebKey extends JsonWebKey, Nominal<'AES'> {} -/** - * In WebCrypto, PBKDF2 is not possible to exports as JsonWebKey - * Therefore we require a "unknown" unique type of key. - * It should be serializable so we can send them across Realms. - */ -export interface PBKDF2UnknownKey extends Nominal<'PBKDF2'> {} declare class Nominal { /** Ghost property, don't use it! */ From 451c141455f5d74ca53a015375dc6a295e795388 Mon Sep 17 00:00:00 2001 From: Jack Works Date: Fri, 18 Mar 2022 20:45:13 +0800 Subject: [PATCH 7/7] chore: remove unused i18n --- packages/mask/shared-ui/locales/en-US.json | 1 - packages/mask/shared-ui/locales/ja-JP.json | 1 - packages/mask/shared-ui/locales/ko-KR.json | 1 - packages/mask/shared-ui/locales/qya-AA.json | 1 - packages/mask/shared-ui/locales/zh-CN.json | 1 - packages/mask/shared-ui/locales/zh-TW.json | 1 - 6 files changed, 6 deletions(-) diff --git a/packages/mask/shared-ui/locales/en-US.json b/packages/mask/shared-ui/locales/en-US.json index 60e40f45df72..c629e6b73793 100644 --- a/packages/mask/shared-ui/locales/en-US.json +++ b/packages/mask/shared-ui/locales/en-US.json @@ -150,7 +150,6 @@ "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_publish_post_aes_key_failed": "Failed to publish the AES key!", "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 d524c836b238..ddbe55582ad1 100644 --- a/packages/mask/shared-ui/locales/ja-JP.json +++ b/packages/mask/shared-ui/locales/ja-JP.json @@ -66,7 +66,6 @@ "select_specific_friends_dialog__title": "特定の連絡先を選ぶ", "service_decryption_failed": "復号に失敗", "service_invalid_backup_file": "これは Mask のバックアップではないかもしれません。", - "service_publish_post_aes_key_failed": "AES キーを公開するのに失敗しました。", "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 3161d8c48c3c..7f6d0b03a5c8 100644 --- a/packages/mask/shared-ui/locales/ko-KR.json +++ b/packages/mask/shared-ui/locales/ko-KR.json @@ -125,7 +125,6 @@ "select_specific_friends_dialog__title": "지정 연락처 선택", "service_decryption_failed": "해독 실패", "service_invalid_backup_file": "Mask의 백업이 아닌 것 같습니다.", - "service_publish_post_aes_key_failed": "AES 키 공유 실패", "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 c9f261d0bfe7..7c175057512a 100644 --- a/packages/mask/shared-ui/locales/qya-AA.json +++ b/packages/mask/shared-ui/locales/qya-AA.json @@ -150,7 +150,6 @@ "select_specific_friends_dialog__title": "crwdns4341:0crwdne4341:0", "service_decryption_failed": "crwdns4343:0crwdne4343:0", "service_invalid_backup_file": "crwdns4345:0crwdne4345:0", - "service_publish_post_aes_key_failed": "crwdns4351:0crwdne4351: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 1a190aba9051..102e7746c639 100644 --- a/packages/mask/shared-ui/locales/zh-CN.json +++ b/packages/mask/shared-ui/locales/zh-CN.json @@ -142,7 +142,6 @@ "select_specific_friends_dialog__title": "选择特定联系人", "service_decryption_failed": "解密失败", "service_invalid_backup_file": "这似乎不是Mask的备份。", - "service_publish_post_aes_key_failed": "AES密钥发布失败!", "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 878d1517ad88..0d750aed3bd7 100644 --- a/packages/mask/shared-ui/locales/zh-TW.json +++ b/packages/mask/shared-ui/locales/zh-TW.json @@ -98,7 +98,6 @@ "select_specific_friends_dialog__title": "選擇特定聯絡人", "service_decryption_failed": "解密失敗。", "service_invalid_backup_file": "這不是一個 Mask 的備份檔。", - "service_publish_post_aes_key_failed": "發佈 AES 金鑰失敗!", "service_unknown_payload": "未知的貼文版本。您或許需要升級 Mask。", "service_username_invalid": "錯誤的使用者名稱", "settings_enable_debug": "除錯模式",