diff --git a/package.json b/package.json index 60c97b86aee7..c358103dfdc9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "maskbook", - "version": "1.10.10", + "version": "1.10.11", "private": true, "license": "AGPL-3.0-or-later", "scripts": { diff --git a/src/extension/background-script/SteganographyService.ts b/src/extension/background-script/SteganographyService.ts index fc9bb3ef46df..3aa9218b370c 100644 --- a/src/extension/background-script/SteganographyService.ts +++ b/src/extension/background-script/SteganographyService.ts @@ -6,6 +6,7 @@ import { EncodeOptions, DecodeOptions } from 'node-stego/es/stego' import { getUrl, downloadUrl } from '../../utils/utils' import { memoizePromise } from '../../utils/memoize' import { getDimension } from '../../utils/image' +import { decodeArrayBuffer, encodeArrayBuffer } from '../../utils/type-transform/String-ArrayBuffer' OnlyRunInContext('background', 'SteganographyService') @@ -39,10 +40,11 @@ type EncodeImageOptions = { template?: Template } & PartialRequired, 'text' | 'pass'> -export async function encodeImage(buf: Uint8Array, options: EncodeImageOptions) { +export async function encodeImage(buf: string | ArrayBuffer, options: EncodeImageOptions) { const { template } = options - return new Uint8Array( - await encode(buf.buffer, await getMaskBuf(), { + const _buf = typeof buf === 'string' ? decodeArrayBuffer(buf) : buf + return encodeArrayBuffer( + await encode(_buf, await getMaskBuf(), { ...defaultOptions, fakeMaskPixels: template !== 'default', cropEdgePixels: true, @@ -56,18 +58,23 @@ export async function encodeImage(buf: Uint8Array, options: EncodeImageOptions) type DecodeImageOptions = PartialRequired, 'pass'> -export async function decodeImage(buf: Uint8Array, options: DecodeImageOptions) { - const dimension = getDimension(buf) +export async function decodeImage(buf: string | ArrayBuffer, options: DecodeImageOptions) { + const _buf = typeof buf === 'string' ? decodeArrayBuffer(buf) : buf + const dimension = getDimension(_buf) if (!dimensions.some(otherDimension => isSameDimension(dimension, otherDimension))) { return '' } - return decode(buf.buffer, await getMaskBuf(), { + return decode(_buf, await getMaskBuf(), { ...defaultOptions, transformAlgorithm: TransformAlgorithm.FFT1D, ...options, }) } +export async function decodeImageUrl(url: string, options: DecodeImageOptions) { + return decodeImage(await downloadUrl(url), options) +} + export function downloadImage({ buffer }: Uint8Array) { return browser.downloads.download({ url: URL.createObjectURL(new Blob([buffer], { type: 'image/png' })), diff --git a/src/extension/mock-service.ts b/src/extension/mock-service.ts index 1c604064ad48..8872fe7b821a 100644 --- a/src/extension/mock-service.ts +++ b/src/extension/mock-service.ts @@ -19,7 +19,7 @@ export const WelcomeService: Partial = { async encodeImage() { - return new Uint8Array() + return '' }, async decodeImage() { return '' diff --git a/src/manifest.json b/src/manifest.json index 721bc856a1fb..203c6b70ebfb 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -1,7 +1,7 @@ { "$schema": "http://json.schemastore.org/chrome-manifest", "name": "Maskbook", - "version": "1.10.10", + "version": "1.10.11", "manifest_version": 2, "web_accessible_resources": ["*.css", "*.js", "*.jpg", "*.png"], "permissions": ["storage", "downloads", "webNavigation", "activeTab"], diff --git a/src/social-network-provider/facebook.com/UI/collectPosts.tsx b/src/social-network-provider/facebook.com/UI/collectPosts.tsx index 2dcc88b9fe4a..ccd6843f8cc4 100644 --- a/src/social-network-provider/facebook.com/UI/collectPosts.tsx +++ b/src/social-network-provider/facebook.com/UI/collectPosts.tsx @@ -4,7 +4,6 @@ import { getEmptyPostInfoByElement, PostInfo, SocialNetworkUI } from '../../../s import { isMobileFacebook } from '../isMobile' import { getProfileIdentifierAtFacebook } from '../getPersonIdentifierAtFacebook' import Services from '../../../extension/service' -import { downloadUrl } from '../../../utils/utils' const posts = new LiveSelector().querySelectorAll( isMobileFacebook ? '.story_body_container ' : '.userContent, .userContent+*+div>div>div>div>div', @@ -133,7 +132,7 @@ async function getSteganographyContent(node: DOMProxy) { await Promise.all( imgUrls.map(async url => { try { - const content = await Services.Steganography.decodeImage(new Uint8Array(await downloadUrl(url)), { + const content = await Services.Steganography.decodeImageUrl(url, { pass, }) return content.indexOf('🎼') === 0 ? content : '' diff --git a/src/social-network-provider/facebook.com/tasks/uploadToPostBox.ts b/src/social-network-provider/facebook.com/tasks/uploadToPostBox.ts index ec75c1cc19f4..626a5c110158 100644 --- a/src/social-network-provider/facebook.com/tasks/uploadToPostBox.ts +++ b/src/social-network-provider/facebook.com/tasks/uploadToPostBox.ts @@ -2,6 +2,7 @@ import { SocialNetworkUI, getActivatedUI } from '../../../social-network/ui' import { untilDocumentReady } from '../../../utils/dom' import { getUrl, downloadUrl, pasteImageToActiveElements } from '../../../utils/utils' import Services from '../../../extension/service' +import { decodeArrayBuffer } from '../../../utils/type-transform/String-ArrayBuffer' export async function uploadToPostBoxFacebook( text: string, @@ -10,16 +11,17 @@ export async function uploadToPostBoxFacebook( const { warningText, template = 'default' } = options const { currentIdentity } = getActivatedUI() const blankImage = await downloadUrl(getUrl(`/maskbook-steganography-${template}.png`)) - const secretImage = await Services.Steganography.encodeImage(new Uint8Array(blankImage), { - text, - pass: currentIdentity.value ? currentIdentity.value.identifier.toText() : '', - template, - }) - - const image = new Uint8Array(secretImage) - await pasteImageToActiveElements(image) + const secretImage = new Uint8Array( + decodeArrayBuffer( + await Services.Steganography.encodeImage(new Uint8Array(blankImage), { + text, + pass: currentIdentity.value ? currentIdentity.value.identifier.toText() : '', + template, + }), + ), + ) + await pasteImageToActiveElements(secretImage) await untilDocumentReady() - try { // Need a better way to find whether the image is pasted into // throw new Error('auto uploading is undefined') @@ -30,7 +32,7 @@ export async function uploadToPostBoxFacebook( async function uploadFail() { console.warn('Image not uploaded to the post box') if (confirm(warningText)) { - await Services.Steganography.downloadImage(image) + await Services.Steganography.downloadImage(secretImage) } } } diff --git a/src/social-network-provider/twitter.com/ui/tasks.ts b/src/social-network-provider/twitter.com/ui/tasks.ts index 81f670144868..a3d494cc9512 100644 --- a/src/social-network-provider/twitter.com/ui/tasks.ts +++ b/src/social-network-provider/twitter.com/ui/tasks.ts @@ -25,6 +25,7 @@ import { twitterEncoding } from '../encoding' import { createTaskStartImmersiveSetupDefault } from '../../../social-network/defaults/taskStartImmersiveSetupDefault' import { instanceOfTwitterUI } from '.' import { ProfileIdentifier } from '../../../database/type' +import { encodeArrayBuffer, decodeArrayBuffer } from '../../../utils/type-transform/String-ArrayBuffer' /** * Wait for up to 5000 ms @@ -80,17 +81,17 @@ const taskUploadToPostBox: SocialNetworkUI['taskUploadToPostBox'] = async (text, const { warningText, template = 'default' } = options const { currentIdentity } = getActivatedUI() const blankImage = await downloadUrl(getUrl(`/maskbook-steganography-${template}.png`)) - const secretImage = await Services.Steganography.encodeImage(new Uint8Array(blankImage), { - text, - pass: currentIdentity.value ? currentIdentity.value.identifier.toText() : '', - template, - }) - - const image = new Uint8Array(secretImage) - - await pasteImageToActiveElements(image) + const secretImage = new Uint8Array( + decodeArrayBuffer( + await Services.Steganography.encodeImage(encodeArrayBuffer(blankImage), { + text, + pass: currentIdentity.value ? currentIdentity.value.identifier.toText() : '', + template, + }), + ), + ) + await pasteImageToActiveElements(secretImage) await untilDocumentReady() - try { // Need a better way to find whether the image is pasted into // throw new Error('auto uploading is undefined') @@ -101,7 +102,7 @@ const taskUploadToPostBox: SocialNetworkUI['taskUploadToPostBox'] = async (text, async function uploadFail() { console.warn('Image not uploaded to the post box') if (confirm(warningText)) { - await Services.Steganography.downloadImage(image) + await Services.Steganography.downloadImage(secretImage) } } } diff --git a/src/social-network-provider/twitter.com/utils/fetch.ts b/src/social-network-provider/twitter.com/utils/fetch.ts index 4660792dd7f1..c19a8092a870 100644 --- a/src/social-network-provider/twitter.com/utils/fetch.ts +++ b/src/social-network-provider/twitter.com/utils/fetch.ts @@ -1,4 +1,4 @@ -import { regexMatch, downloadUrl } from '../../../utils/utils' +import { regexMatch } from '../../../utils/utils' import { notNullable } from '../../../utils/assert' import { defaultTo } from 'lodash-es' import { nthChild } from '../../../utils/dom' @@ -181,12 +181,9 @@ export const postImageParser = async (node: HTMLElement) => { await Promise.all( imgUrls.map(async url => { try { - const content = await Services.Steganography.decodeImage( - new Uint8Array(await downloadUrl(url)), - { - pass: posterIdentity.toText(), - }, - ) + const content = await Services.Steganography.decodeImageUrl(url, { + pass: posterIdentity.toText(), + }) return /https:\/\/.+\..+\/%20(.+)%40/.test(content) ? content : '' } catch { // for twitter image url maybe absent diff --git a/src/utils/__tests__/image.ts b/src/utils/__tests__/image.ts index dcf8e3074059..fa8015421614 100644 --- a/src/utils/__tests__/image.ts +++ b/src/utils/__tests__/image.ts @@ -55,7 +55,7 @@ function createJPEGBuffer(width: number, height: number) { // EOI 0xff, 0xd9, - ]) + ]).buffer } function createPNGBuffer(width: number, height: number) { @@ -82,7 +82,7 @@ function createPNGBuffer(width: number, height: number) { 0, // compression method 0, // filter method 0, // interlace method - ]) + ]).buffer } test('Get dimension of JPEG buffer', () => { diff --git a/src/utils/image.ts b/src/utils/image.ts index dd65c6566c92..bab1827422f7 100644 --- a/src/utils/image.ts +++ b/src/utils/image.ts @@ -1,12 +1,12 @@ /* eslint-disable no-bitwise */ import { imgType } from 'node-stego/es/helper' -export function getDimension(buf: Uint8Array) { +export function getDimension(buf: ArrayBuffer) { const fallback = { width: 0, height: 0, } - switch (imgType(buf)) { + switch (imgType(new Uint8Array(buf))) { case 'image/jpeg': return getDimensionAsJPEG(buf) ?? fallback case 'image/png': @@ -16,8 +16,8 @@ export function getDimension(buf: Uint8Array) { } } -function getDimensionAsPNG(buf: Uint8Array) { - const dataView = new DataView(buf.buffer, 0, 28) +function getDimensionAsPNG(buf: ArrayBuffer) { + const dataView = new DataView(buf, 0, 28) return { width: dataView.getInt32(16), height: dataView.getInt32(20), @@ -29,8 +29,8 @@ function getDimensionAsPNG(buf: Uint8Array) { * * @see http://vip.sugovica.hu/Sardi/kepnezo/JPEG%20File%20Layout%20and%20Format.htm */ -function getDimensionAsJPEG(buf: Uint8Array) { - const dataView = new DataView(buf.buffer) +function getDimensionAsJPEG(buf: ArrayBuffer) { + const dataView = new DataView(buf) let i = 0 if ( dataView.getUint8(i) === 0xff && @@ -47,9 +47,9 @@ function getDimensionAsJPEG(buf: Uint8Array) { dataView.getUint8(i + 6) === 0x00 ) { let block_length = dataView.getUint8(i) * 256 + dataView.getUint8(i + 1) - while (i < buf.length) { + while (i < dataView.byteLength) { i += block_length - if (i >= buf.length) return + if (i >= dataView.byteLength) return if (dataView.getUint8(i) !== 0xff) return if ( dataView.getUint8(i + 1) === 0xc0 || // SOF0 marker diff --git a/src/utils/type-transform/String-ArrayBuffer.ts b/src/utils/type-transform/String-ArrayBuffer.ts index 0fe367aa00bf..ea3ad184df2d 100644 --- a/src/utils/type-transform/String-ArrayBuffer.ts +++ b/src/utils/type-transform/String-ArrayBuffer.ts @@ -11,7 +11,9 @@ export function decodeArrayBuffer(str: string): ArrayBuffer { return new Uint8Array(uintArr).buffer } export function encodeArrayBuffer(buffer: ArrayBuffer) { - const x = [...new Uint8Array(buffer)] - const encodedString = String.fromCharCode.apply(null, x) + let encodedString = '' + for (const byte of new Uint8Array(buffer)) { + encodedString += String.fromCharCode(byte) + } return btoa(encodedString) }