Skip to content
Merged
1 change: 1 addition & 0 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,7 @@
"redpackets",
"renfil",
"shapeclip",
"ssrb",
"steth",
"swaptoken",
"testweb",
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"yarn": ">=999.0.0",
"npm": ">=999.0.0"
},
"version": "2.8.0",
"version": "2.8.1",
"private": true,
"license": "AGPL-3.0-or-later",
"scripts": {
Expand Down
73 changes: 50 additions & 23 deletions packages/mask/background/network/gun/encryption/queryPostKey.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { decodeArrayBuffer, encodeArrayBuffer } from '@dimensiondev/kit'
import type { DecryptStaticECDH_PostKey, EncryptionResultE2EMap } from '@masknet/encryption'
import { decodeArrayBuffer, encodeArrayBuffer, isNonNull, unreachable } from '@dimensiondev/kit'
import { type DecryptStaticECDH_PostKey, type EncryptionResultE2EMap, SocialNetworkEnum } 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 { isObject, noop, uniq } from 'lodash-unified'
import { queryPublicKey } from '../../../database/persona/helper'

// !!! Change how this file access Gun will break the compatibility of v40 payload decryption.
Expand Down Expand Up @@ -36,31 +36,47 @@ namespace Version38Or39 {
version: -38 | -39,
iv: Uint8Array,
minePublicKey: EC_Public_CryptoKey,
networkHint: string,
network: SocialNetworkEnum,
abortSignal: AbortSignal,
): AsyncGenerator<DecryptStaticECDH_PostKey, void, undefined> {
const minePublicKeyJWK = await CryptoKeyToJsonWebKey(minePublicKey)
const { keyHash, postHash } = await calculatePostKeyPartition(version, networkHint, iv, minePublicKeyJWK)
const { keyHash, postHash } = await calculatePostKeyPartition(version, network, iv, minePublicKeyJWK)

/* cspell:disable-next-line */
// ? In this step we get something like ["jzarhbyjtexiE7aB1DvQ", "jzarhuse6xlTAtblKRx9"]
console.log(`[@masknet/encryption] Reading key partition [${postHash}][${keyHash}]`)
const internalNodeNames = Object.keys((await getGunData(postHash, keyHash)) || {}).filter((x) => x !== '_')
console.log(
`[@masknet/encryption] Reading key partition [${postHash[0]}][${keyHash}] and [${postHash[1]}][${keyHash}]`,
)
const internalNodeNames = uniq(
(
await Promise.all([
//
getGunData(postHash[0], keyHash),
getGunData(postHash[1], keyHash),
])
)
.filter(isNonNull)
.filter(isObject)
.map(Object.keys)
.flat()
.filter((x) => x !== '_'),
)
// ? In this step we get all keys in this category (gun2[postHash][keyHash])
const resultPromise = internalNodeNames.map((key) => getGunData(key))

const iter = new EventIterator<DecryptStaticECDH_PostKey>((queue) => {
// immediate results
for (const result of resultPromise) result.then(emit, noop)

async function main() {
const iter = subscribeGunMapData([postHash], isValidData, abortSignal)
for await (const data of iter) emit(data)
queue.stop()
// future results
Promise.all([
main(subscribeGunMapData([postHash[1]], isValidData, abortSignal)),
main(subscribeGunMapData([postHash[0]], isValidData, abortSignal)),
]).then(() => queue.stop())

async function main(keyProvider: AsyncGenerator<unknown>) {
for await (const data of keyProvider) emit(data)
}

main()

function emit(result: any) {
function emit(result: unknown) {
if (abortSignal.aborted) return
if (!isValidData(result)) return
queue.push({
Expand All @@ -86,10 +102,10 @@ namespace Version38Or39 {
export async function publishPostAESKey_version39Or38(
version: -39 | -38,
postIV: Uint8Array,
networkHint: string,
network: SocialNetworkEnum,
receiversKeys: PublishedAESKeyRecord[] | EncryptionResultE2EMap,
) {
const postHash = await hashIV(networkHint, postIV)
const [postHash] = await hashIV(network, postIV)
if (Array.isArray(receiversKeys)) {
// Store AES key to gun
receiversKeys.forEach(async ({ aesKey, receiverKey }) => {
Expand Down Expand Up @@ -137,23 +153,34 @@ namespace Version38Or39 {

async function calculatePostKeyPartition(
version: -38 | -39,
networkHint: string,
network: SocialNetworkEnum,
iv: Uint8Array,
key: EC_Public_JsonWebKey,
) {
const postHash = await hashIV(networkHint, iv)
const postHash = await hashIV(network, iv)
// In version > -39, we will use stable hash to prevent unstable result for key hashing

const keyHash = version === -39 ? await hashKey39(key) : await hashKey38(key)
return { postHash, keyHash }
}

async function hashIV(networkHint: string, iv: Uint8Array) {
async function hashIV(network: SocialNetworkEnum, iv: Uint8Array): Promise<[string, string]> {
const hashPair = '9283464d-ee4e-4e8d-a7f3-cf392a88133f'
const N = 2

const hash = await work(encodeArrayBuffer(iv), hashPair)
return networkHint + '-' + hash.slice(0, N)
const hash = (await work(encodeArrayBuffer(iv), hashPair)).slice(0, N)
const networkHint = getNetworkHint(network)
return [`${networkHint}${hash}`, `${networkHint}-${hash}`]
}

function getNetworkHint(x: SocialNetworkEnum) {
if (x === SocialNetworkEnum.Facebook) return ''
if (x === SocialNetworkEnum.Twitter) return 'twitter-'
if (x === SocialNetworkEnum.Minds) return 'minds-'
if (x === SocialNetworkEnum.Instagram) return 'instagram-'
if (x === SocialNetworkEnum.Unknown)
throw new TypeError('[@masknet/encryption] Current SNS network is not correctly configured.')
unreachable(x)
}

// The difference between V38 and V39 is: V39 is not stable (JSON.stringify)
Expand Down
11 changes: 9 additions & 2 deletions packages/mask/background/services/crypto/appendEncryption.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { appendEncryptionTarget, EC_Key, EC_KeyCurveEnum, SupportedPayloadVersions } from '@masknet/encryption'
import {
appendEncryptionTarget,
EC_Key,
EC_KeyCurveEnum,
SocialNetworkEnum,
SupportedPayloadVersions,
} from '@masknet/encryption'
import type { EC_Public_CryptoKey, PostIVIdentifier, ProfileIdentifier } from '@masknet/shared-base'
import { deriveAESByECDH, queryPublicKey } from '../../database/persona/helper'
import { updatePostDB, queryPostDB } from '../../database/post'
Expand All @@ -10,6 +16,7 @@ export async function appendShareTarget(
post: PostIVIdentifier,
target: ProfileIdentifier[],
whoAmI: ProfileIdentifier,
network: SocialNetworkEnum,
): Promise<void> {
if (version === -39 || version === -40) throw new TypeError('invalid version')
const key = await getPostKeyCache(post)
Expand Down Expand Up @@ -38,7 +45,7 @@ export async function appendShareTarget(
},
)

publishPostAESKey_version39Or38(-38, post.toIV(), whoAmI.network, e2e)
publishPostAESKey_version39Or38(-38, post.toIV(), network, e2e)

updatePostDB(
{
Expand Down
16 changes: 3 additions & 13 deletions packages/mask/background/services/crypto/decryption.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { encodeArrayBuffer, unreachable } from '@dimensiondev/kit'
import { encodeArrayBuffer } from '@dimensiondev/kit'
import {
decrypt,
parsePayload,
Expand Down Expand Up @@ -158,7 +158,7 @@ async function* decryption(payload: string | Uint8Array, context: DecryptionCont
-38,
iv,
author,
getNetworkHint(context.currentSocialNetwork),
context.currentSocialNetwork,
signal || new AbortController().signal,
)
},
Expand All @@ -170,7 +170,7 @@ async function* decryption(payload: string | Uint8Array, context: DecryptionCont
-39,
iv,
author,
getNetworkHint(context.currentSocialNetwork),
context.currentSocialNetwork,
signal || new AbortController().signal,
)
},
Expand All @@ -185,16 +185,6 @@ async function* decryption(payload: string | Uint8Array, context: DecryptionCont
return null
}

function getNetworkHint(x: SocialNetworkEnum) {
if (x === SocialNetworkEnum.Facebook) return ''
if (x === SocialNetworkEnum.Twitter) return 'twitter-'
if (x === SocialNetworkEnum.Minds) return 'minds-'
if (x === SocialNetworkEnum.Instagram) return 'instagram-'
if (x === SocialNetworkEnum.Unknown)
throw new TypeError('[@masknet/encryption] Current SNS network is not correctly configured.')
unreachable(x)
}

/** @internal */
export async function getPostKeyCache(id: PostIVIdentifier) {
const post = await queryPostDB(id)
Expand Down
10 changes: 5 additions & 5 deletions packages/mask/background/services/crypto/encryption.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
EncryptionResultE2EMap,
EncryptTargetE2E,
EncryptTargetPublic,
SocialNetworkEnum,
SocialNetworkEnumToProfileDomain,
} from '@masknet/encryption'
import { encryptByLocalKey, deriveAESByECDH, queryPublicKey } from '../../database/persona/helper'
import { savePostKeyToDB } from '../../database/post/helper'
Expand All @@ -18,13 +20,11 @@ export async function encryptTo(
content: SerializableTypedMessages,
target: EncryptTargetPublic | EncryptTargetE2E,
whoAmI: ProfileIdentifier | undefined,
IdentifierNetwork: string,
network: SocialNetworkEnum,
): Promise<string> {
// always use identifier network from ProfileIdentifier if possible.
if (whoAmI) IdentifierNetwork = whoAmI.network
const { identifier, output, postKey, e2e } = await encrypt(
{
network: IdentifierNetwork,
network: whoAmI?.network || SocialNetworkEnumToProfileDomain(network),
author: whoAmI,
message: content,
target,
Expand Down Expand Up @@ -59,7 +59,7 @@ export async function encryptTo(
})().catch((error) => console.error('[@masknet/encryption] Failed to save post key to DB', error))

if (target.type === 'E2E') {
publishPostAESKey_version39Or38(-38, identifier.toIV(), IdentifierNetwork, e2e!)
publishPostAESKey_version39Or38(-38, identifier.toIV(), network, e2e!)
}
if (typeof output !== 'string') throw new Error('version -37 is not enabled yet!')
return output
Expand Down
4 changes: 2 additions & 2 deletions packages/mask/shared-ui/locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -337,14 +337,14 @@
"plugin_wallet_name_placeholder": "Enter 1-12 characters",
"plugin_wallet_fail_to_sign": "Failed to sign password.",
"plugin_wallet_cancel_sign": "Signature canceled.",
"plugin_nft_avatar_recommend_feature_description": "Set your NFT as profile picture with exclusive aura",
"plugin_nft_avatar_recommend_feature_description": "Set your NFT as profile picture with exclusive aura.",
"plugin_red_packet_display_name": "Plugin: Lucky Drop",
"application_hint": "Socialize and show off your NFTs. People can bid,buy, view your valuable NFTs without leaving Twitter.",
"plugin_red_packet_claimed": "Claimed",
"plugin_red_packet_erc20_tab_title": "Token",
"plugin_red_packet_erc721_tab_title": "Collectibles",
"plugin_red_packet_name": "Lucky Drop",
"plugin_red_packet_recommend_feature_description": "Astonish your friends with a surprise gift",
"plugin_red_packet_recommend_feature_description": "Astonish your friends with a surprise gift.",
"plugin_red_packet_description": "Gift crypto or NFTs to any users, first come, first served.",
"plugin_red_packet_erc721_insufficient_balance": "Insufficient Balance",
"plugin_red_packet_details": "Lucky Drop Details",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ const useStyles = makeStyles()((theme) => ({
color: theme.palette.text.secondary,
marginRight: 12,
},
divider: {
width: '100%',
height: 1,
background: theme.palette.divider,
margin: '8px 0',
},
rightIcon: {
marginLeft: 'auto',
},
Expand Down Expand Up @@ -48,8 +54,8 @@ export function EncryptionMethodSelector(props: EncryptionMethodSelectorProps) {
value={EncryptionMethodType.Text}
title={t('compose_encrypt_method_text')}
subTitle={t('compose_encrypt_method_text_sub_title')}
showDivider
/>
<div className={classes.divider} />
<PopoverListItem
value={EncryptionMethodType.Image}
title={t('compose_encrypt_method_image')}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,21 +90,22 @@ export function EncryptionTargetSelector(props: EncryptionTargetSelectorProps) {
const { t } = useI18N()
const { classes } = useStyles()
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null)
const e2eDisabledMessage = props.e2eDisabled ? (
<div className={classes.flex}>
<Typography className={classes.mainTitle}>{t('persona_required')}</Typography>
<Typography
className={classes.create}
onClick={() => {
if (props.e2eDisabled === E2EUnavailableReason.NoLocalKey) return
props.e2eDisabled === E2EUnavailableReason.NoPersona
? props.onCreatePersona()
: props.onConnectPersona()
}}>
{props.e2eDisabled === E2EUnavailableReason.NoPersona ? t('create') : t('connect')}
</Typography>
</div>
) : null
const e2eDisabledMessage =
props.e2eDisabled && props.e2eDisabled !== E2EUnavailableReason.NoLocalKey ? (
<div className={classes.flex}>
<Typography className={classes.mainTitle}>{t('persona_required')}</Typography>
<Typography
className={classes.create}
onClick={() => {
if (props.e2eDisabled === E2EUnavailableReason.NoLocalKey) return
props.e2eDisabled === E2EUnavailableReason.NoPersona
? props.onCreatePersona()
: props.onConnectPersona()
}}>
{props.e2eDisabled === E2EUnavailableReason.NoPersona ? t('create') : t('connect')}
</Typography>
</div>
) : null
const noLocalKeyMessage = props.e2eDisabled === E2EUnavailableReason.NoLocalKey && (
<div className={classes.flex}>
<Typography className={classes.mainTitle}>{t('compose_no_local_key')}</Typography>
Expand Down Expand Up @@ -137,18 +138,18 @@ export function EncryptionTargetSelector(props: EncryptionTargetSelectorProps) {
value={EncryptionTargetType.Public}
title={t('compose_encrypt_visible_to_all')}
subTitle={t('compose_encrypt_visible_to_all_sub')}
showDivider
/>
<div className={classes.divider} />
<PopoverListItem
onItemClick={() => props.onChange(EncryptionTargetType.Self)}
disabled={!!props.e2eDisabled}
value={EncryptionTargetType.Self}
title={t('compose_encrypt_visible_to_private')}
subTitle={t('compose_encrypt_visible_to_private_sub')}
showDivider
/>
{e2eDisabledMessage}
{noLocalKeyMessage}
<div className={classes.divider} />
<PopoverListItem
onItemClick={() => {
props.onChange(EncryptionTargetType.E2E)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,6 @@ const useStyles = makeStyles()((theme) => ({
display: 'flex',
alignItems: 'center',
},
divider: {
width: '100%',
height: 1,
background: theme.palette.divider,
margin: '8px 0',
},
mainTitle: {
fontSize: 14,
color: theme.palette.text.primary,
Expand All @@ -34,11 +28,10 @@ interface PopoverListItemProps {
title: string
subTitle?: string
disabled?: boolean
showDivider?: boolean
onItemClick?(): void
}
export function PopoverListItem(props: PopoverListItemProps) {
const { title, subTitle, value, itemTail, disabled, showDivider, onItemClick } = props
const { title, subTitle, value, itemTail, disabled, onItemClick } = props
const { classes, cx } = useStyles()
return (
<>
Expand All @@ -53,7 +46,6 @@ export function PopoverListItem(props: PopoverListItemProps) {
</Box>
{itemTail}
</div>
{showDivider && <div className={classes.divider} />}
</>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export function useSubmit(onClose: () => void, reason: 'timeline' | 'popup' | 'r
content,
target,
lastRecognizedIdentity?.identifier ?? fallbackProfile,
activatedSocialNetworkUI.networkIdentifier,
activatedSocialNetworkUI.encryptionNetwork,
)
const encrypted = socialNetworkEncoder(activatedSocialNetworkUI.encryptionNetwork, _encrypted)

Expand Down
Loading