Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: integrate stego-js@2 #2139

Closed
wants to merge 13 commits into from
2 changes: 1 addition & 1 deletion packages/maskbook/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"@dimensiondev/maskbook-theme": "*",
"@dimensiondev/matrix-js-sdk-type": "=1.0.0-20200731085604-dc2de92",
"@dimensiondev/metamask-extension-provider": "^3.0.0-20210125122006-86e9bae",
"@dimensiondev/stego-js": "=0.11.1-20201027083223-8ab41be",
"@dimensiondev/stego-js": "^0.13.0-20210127113811-f24cb8f",
"@ethersproject/address": "^5.0.4",
"@ethersproject/contracts": "^5.0.4",
"@ethersproject/networks": "^5.0.4",
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { DecodeOptions, EncodeOptions, getImageType } from '@dimensiondev/stego-js'
import { preprocessImage } from '@dimensiondev/stego-js/esm/utils/image'
import type { AsyncCallOptions } from 'async-call-rpc'
import { AsyncCall } from 'async-call-rpc/full'
import { WorkerChannel } from 'async-call-rpc/utils/web/worker'
import { serialization } from '../../../utils/type-transform/Serialization'

export let StegoWorker: Worker | undefined

if (process.env.architecture) {
StegoWorker = new Worker(new URL('./worker.ts', import.meta.url), { name: 'StegoWorker' })
}

const options: AsyncCallOptions = {
channel: new WorkerChannel(StegoWorker),
serializer: serialization,
}

const StegoAPI = AsyncCall<typeof import('./worker-implementation')>({}, options)

export async function encode(image: ArrayBuffer, mask: ArrayBuffer, options: EncodeOptions) {
const a = cutImage(await toImageData(image))
const b = cutImage(await toImageData(mask))
const { data, height, width } = await StegoAPI.encode(a, b, options)
return toBuffer(data, height, width)
}

export async function decode(image: ArrayBuffer, mask: ArrayBuffer, options: DecodeOptions) {
return StegoAPI.decode(await toImageData(image), await toImageData(mask), options)
}

function cutImage(data: ImageData) {
// prettier-ignore
septs marked this conversation as resolved.
Show resolved Hide resolved
return preprocessImage(data, (w, h) =>
createCanvas(w, h)
.getContext('2d')
?.createImageData(w, h)
?? null
)
}

function toImageData(data: ArrayBuffer) {
const type = getImageType(new Uint8Array(data.slice(0, 8)))
const blob = new Blob([data], { type })
const url = URL.createObjectURL(blob)
return new Promise<ImageData>((resolve, reject) => {
const element = new Image()
element.addEventListener('load', () => {
const { width, height } = element
const ctx = createCanvas(width, height).getContext('2d')!
ctx.drawImage(element, 0, 0, width, height)
resolve(ctx.getImageData(0, 0, width, height))
})
element.addEventListener('error', reject)
element.src = url
})
}

function toBuffer(imgData: ImageData, height: number, width: number): Promise<ArrayBuffer> {
const canvas = createCanvas(width, height)
canvas.getContext('2d')!.putImageData(imgData, 0, 0, 0, 0, width, height)
return new Promise<ArrayBuffer>((resolve, reject) => {
const callback: BlobCallback = (blob) => {
if (blob) {
resolve(blob.arrayBuffer())
} else {
reject(new Error('fail to generate array buffer'))
}
}
canvas.toBlob(callback, 'image/png')
})
}

function createCanvas(width: number, height: number) {
const canvas = document.createElement('canvas')
canvas.width = width
canvas.height = height
return canvas
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { assertEnvironment, Environment } from '@dimensiondev/holoflows-kit'
import { decodeArrayBuffer, memoizePromise } from '@dimensiondev/kit'
import {
AlgorithmVersion,
DecodeOptions,
EncodeOptions,
GrayscaleAlgorithm,
TransformAlgorithm,
} from '@dimensiondev/stego-js'
import { getDimension } from '../../../utils/image'
import { downloadUrl, getUrl } from '../../../utils/utils'
import { decode, encode } from './api'

assertEnvironment(Environment.ManifestBackground)

type Template = 'v1' | 'v2' | 'v3' | 'v4' | 'eth' | 'dai' | 'okb'
type Mask = 'v1' | 'v2' | 'v4' | 'transparent'

type Dimension = {
width: number
height: number
}

const dimensionPreset: (Dimension & { mask: Mask })[] = [
{ width: 1024, height: 1240, mask: 'v1' },
{ width: 1200, height: 681, mask: 'v2' },
{ width: 1200, height: 680, mask: 'transparent' },
{ width: 1000, height: 558, mask: 'transparent' },
{ width: 1000, height: 560, mask: 'v4' },
]

const defaultOptions: Pick<EncodeOptions, 'size' | 'narrow' | 'copies' | 'tolerance'> = {
size: 8,
narrow: 0,
copies: 3,
tolerance: 128,
}

const isSameDimension = (dimension: Dimension, otherDimension: Dimension) =>
dimension.width === otherDimension.width && dimension.height === otherDimension.height

const getMaskBuf = memoizePromise(
async (type: Mask) => (await downloadUrl(getUrl(`/image-payload/mask-${type}.png`))).arrayBuffer(),
undefined,
)

type EncodeImageOptions = {
template?: Template
} & PartialRequired<Required<EncodeOptions>, 'text' | 'pass'>

export async function encodeImage(buf: ArrayBuffer, options: EncodeImageOptions): Promise<ArrayBuffer> {
const { template } = options
const mask = await getMaskBuf(template === 'v2' || template === 'v4' ? template : 'transparent')
const encodedOptions: EncodeOptions = {
...defaultOptions,
version: AlgorithmVersion.V2,
fakeMaskPixels: false,
cropEdgePixels: template !== 'v2' && template !== 'v3' && template !== 'v4',
exhaustPixels: true,
grayscaleAlgorithm: template === 'v3' ? GrayscaleAlgorithm.LUMINANCE : GrayscaleAlgorithm.NONE,
transformAlgorithm: TransformAlgorithm.FFT1D,
...options,
}
return encode(buf, mask, encodedOptions)
}

type DecodeImageOptions = PartialRequired<Required<DecodeOptions>, 'pass'>

export async function decodeImage(buf: ArrayBuffer, options: DecodeImageOptions): Promise<string> {
buf = typeof buf === 'string' ? decodeArrayBuffer(buf) : buf
const dimension = getDimension(buf)
const preset = dimensionPreset.find((d) => isSameDimension(d, dimension))
if (!preset) return ''
const _options: DecodeOptions = {
...defaultOptions,
version: AlgorithmVersion.V2,
transformAlgorithm: TransformAlgorithm.FFT1D,
...options,
}
try {
return await decode(buf, await getMaskBuf(preset.mask), _options)
} catch {
_options.version = AlgorithmVersion.V1
return decode(buf, await getMaskBuf(preset.mask), _options)
}
}

export async function decodeImageUrl(url: string, options: DecodeImageOptions): Promise<string> {
return decodeImage(await (await downloadUrl(url)).arrayBuffer(), options)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { algorithms, DecodeOptions, EncodeOptions } from '@dimensiondev/stego-js'

export async function encode(image: ImageData, mask: ImageData, options: EncodeOptions) {
return algorithms[options.version].encode(image, mask, options)
}

export function decode(image: ImageData, mask: ImageData, options: DecodeOptions) {
return algorithms[options.version].decode(image, mask, options)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { AsyncCall, AsyncCallOptions } from 'async-call-rpc/full'
import { WorkerChannel } from 'async-call-rpc/utils/web/worker'
import { serialization } from '../../../utils/type-transform/Serialization'
import * as implementation from './worker-implementation'

const options: AsyncCallOptions = {
channel: new WorkerChannel(),
serializer: serialization,
}

AsyncCall(implementation, options)
Original file line number Diff line number Diff line change
@@ -1,34 +1,26 @@
import { SocialNetworkUI, getActivatedUI } from '../../../social-network/ui'
import { untilDocumentReady } from '../../../utils/dom'
import { getUrl, downloadUrl, pasteImageToActiveElements } from '../../../utils/utils'
import { GrayscaleAlgorithm } from '@dimensiondev/stego-js'
import Services from '../../../extension/service'
import { decodeArrayBuffer } from '../../../utils/type-transform/String-ArrayBuffer'
import { GrayscaleAlgorithm } from '@dimensiondev/stego-js/cjs/grayscale'
import { getActivatedUI, SocialNetworkUI } from '../../../social-network/ui'
import { untilDocumentReady } from '../../../utils/dom'
import { MaskMessage } from '../../../utils/messages'
import { downloadUrl, getUrl, pasteImageToActiveElements } from '../../../utils/utils'

export async function uploadToPostBoxFacebook(
text: string,
options: Parameters<SocialNetworkUI['taskUploadToPostBox']>[1],
) {
const { autoPasteFailedRecover, relatedText, template = 'v2' } = options
const { lastRecognizedIdentity } = getActivatedUI()
const blankImage = await downloadUrl(
getUrl(
`${
template === 'v2' ? '/image-payload' : template === 'v3' ? '/election-2020' : '/wallet'
}/payload-${template}.png`,
),
).then((x) => x.arrayBuffer())
const blankPrefix = template === 'v2' ? '/image-payload' : template === 'v3' ? '/election-2020' : '/wallet'
const blankImage = await downloadUrl(getUrl(`${blankPrefix}/payload-${template}.png`)).then((x) => x.arrayBuffer())
const secretImage = new Uint8Array(
decodeArrayBuffer(
await Services.Steganography.encodeImage(new Uint8Array(blankImage), {
text,
pass: lastRecognizedIdentity.value ? lastRecognizedIdentity.value.identifier.toText() : '',
template,
// ! the color image cannot compression resistance in Facebook
grayscaleAlgorithm: GrayscaleAlgorithm.LUMINANCE,
}),
),
await Services.Steganography.encodeImage(blankImage, {
text,
pass: lastRecognizedIdentity.value ? lastRecognizedIdentity.value.identifier.toText() : '',
template,
// ! the color image cannot compression resistance in Facebook
grayscaleAlgorithm: GrayscaleAlgorithm.LUMINANCE,
}),
)
pasteImageToActiveElements(secretImage)
await untilDocumentReady()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,13 +86,11 @@ const taskUploadToPostBox: SocialNetworkUI['taskUploadToPostBox'] = async (text,
),
).then((x) => x.arrayBuffer())
const secretImage = new Uint8Array(
decodeArrayBuffer(
await Services.Steganography.encodeImage(encodeArrayBuffer(blankImage), {
text,
pass: lastRecognizedIdentity.value ? lastRecognizedIdentity.value.identifier.toText() : '',
template,
}),
),
await Services.Steganography.encodeImage(blankImage, {
text,
pass: lastRecognizedIdentity.value ? lastRecognizedIdentity.value.identifier.toText() : '',
template,
}),
)
pasteImageToActiveElements(secretImage)
await untilDocumentReady()
Expand Down
4 changes: 2 additions & 2 deletions packages/maskbook/src/utils/image.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
/* eslint-disable no-bitwise */
import { imgType } from '@dimensiondev/stego-js/cjs/helper'
import { getImageType } from '@dimensiondev/stego-js'

export function getDimension(buf: ArrayBuffer) {
const fallback = {
width: 0,
height: 0,
}
switch (imgType(new Uint8Array(buf))) {
switch (getImageType(new Uint8Array(buf))) {
case 'image/jpeg':
return getDimensionAsJPEG(buf) ?? fallback
case 'image/png':
Expand Down
Loading