Can embed any file in a PNG/WebM/GIF/JPEG and upload it to a third-party host through 4chan
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

382 lines
24 KiB

import { Buffer } from "buffer";
import type { WorkerEmbeddedFile, ImageProcessor } from "./processor.worker";
import { PNGDecoder, PNGEncoder } from "./png";
import { decodeCoom3Payload } from "./utils";
import { settings } from "./stores";
import { filehosts } from "./filehosts";
import * as bs58 from 'bs58';
import { BitstreamReader, BitstreamWriter } from "./bitstream";
import { tinf_uncompress } from "./dh-deflate";
export let csettings: Parameters<typeof settings['set']>[0];
settings.subscribe(b => {
csettings = b;
});
const CUM3 = Buffer.from("doo\0" + "m");
const CUM4 = Buffer.from("voo\0" + "m");
const CUM5 = Buffer.from("boo\0");
const CUM6 = Buffer.from("Creation Time\0");
const CUM7 = Buffer.from("Software\0");
const BufferReadStream = (b: Buffer) => {
const ret = new ReadableStream<Buffer>({
pull(cont) {
cont.enqueue(b);
cont.close();
}
});
return ret;
};
const password = Buffer.from("NOA");
const xor = (a: Buffer, p: Buffer) => {
let n = 0;
for (let i = 0; i < a.byteLength; ++i) {
a[i] ^= p[n];
n++;
n %= p.byteLength;
}
};
const prefs: any = {
'files.catbox.moe': 'c',
'a.pomf.cat': 'p',
'take-me-to.space': 't',
'z.zz.fo': 'z'
};
const rprefs: any = {
'c': 'files.catbox.moe',
'p': 'a.pomf.cat',
't': 'take-me-to.space',
'z': 'z.zz.fo',
};
const extractFromRawDeflate = (b: Buffer) => {
const src = new BitstreamReader();
src.addBuffer(b);
const chnks: number[] = [];
const hidden = new BitstreamWriter({
write(chunk) {
for (const i of chunk) {
if (i >= 0x20 && i <= 128) // only printable ascii
chnks.push(i);
else
throw "Finish";
/* NOOOOOO YOU CANT JUST THROW DURING NORMAL OPERATION THATS A CODE SMEL-ACK!
%@@@@@(////////@
@&/////(@&/////@.
#@@@@@&&(, %@/////////@@@@@
#@@& (@@@@&. &%/////@@#/////%@
&@@ .&@@% ,* (@@@@ @(/%@&/////////(@
*@@ ,@@@@, @@@@. @@.*@%@@ @@@(///////////(@
@@# .@*@@@@@@@. &@& *@@# *@@ %@@* @@%/////#@@&////#@
%@/ , %@@@@@@. @@/ (@@* @% &@@ @(///////////%@@@#
,@@ @@ @@@# .@@, ,@@,.@* @@ @(////////@@///@%
@@, *@& @@@ @@@ .@@@ @& @(/////%@///////@
@@ #@* ,@ @@@ *@@@@% @(///&@/////////@
@@ #@ *, @@( /@@@@@@@@( @@ *@@% @(/(@(//////////@,
#@ (@* %@% @@ @@ #@ %@@ %@@@@@#/////////@,
(@. *@@ %@ @@* @& @@@@@@@@, .@, %@( @//////#@@#////@
@@ @@ @( @@. @@ @@@@@@% .@* @@/ @#///////////%@@@
@* (@ @@ @ @@@% %@. %@@@ @////////@@&///@%
@. @@ @( (@ @@/ @@ (@@ .@/////@@(///////@
@, @@@@@.@@ @@@& .@@ @@ (@//(@#//////////@@
@% @@ .@@@@ #@@ #@, @&&@(////////////@@
#@ @@@ *@@@@@ @& @% /@@& &@@, ,@. @@(//////%@@%////%@
@, @@@@* @@ *((#&@@@@@ @@ @%///////////%@@@@@@
#@ ,@@@@ &@@@@@@@@# ,@@@@@@@@@@@& @ /@////////%@@///////@
@@ @@@ @@/ @@@@@@ @ .@@@@/ @ (@/////@@///////////@
@@ @@.(@ &@,*@@@ @@# @@ @@@@& @@ *&@@@@%, @. @///@%/////////////@
@ @@ @@@@# ,@ @# %@ @# @ (@@@( @& @/@@//////////////#@
&@ @@ @@@@ @@@, @ /@ .@@@ @@ @#/////////////(@(
@& #@@ @@@@ &@@@@@@@@@@@ @@@, #@@@ @@@ @@@///////////(@@,
@@ @@ @@ @@ @% @ @# *@@ @@@@@@@@@@@&///////////(@@
@& /, @@ @@ (/, @@ @* @/ ,((. .@@@@@@@@@@@@@. *@/ *@@//////////////////////@,
@* @.#@ @% %@@@@@@# @@ @% @@. @@ @@&/@@@@ @@ *@/@@@ .@# @@@///////////////////@@
@# @& @# @@@@@. /@. %@ @& @@ #@@((@& @@ #@@@@////@& @@@@@@@@@@@@@@@@@@@@(
@@@. @, @@ .@* @& @@@@* @@@@. %@@/ @@@///////////@@. @@&%///////////////@@
/@@ @@,@. *@* #@* ,@@(/@* @@@@@@#/////////////(@@ @* @@@@@//////////////@@
@@ @( &@. @. #@@@ %@& @@@@@@@@@@@@/////////////&@* @@ .&@@@@&////////////@&
,@. @@ @@ @. @@/(@. @@@@@@@@@@@@@@@@@@@@@(/////////@ (@ @@@@@@@@@@@@@@@
*@@ %&*@, /@# @. &@/(@@@@, .@@@@@@@@%,,,,,,,,,,,,,,,@@@%/////@@ @ %@,&#@@&////////////@*
@@@& %@@* ,@@ @. @(//@@ ,@#@@@@@@%(@*,,,,,,,,,,,,,,,,,,@@@@@///&@ #@ &@ (@@@#//////////@
.@&@@ #@@@@@@@@@@& @, @@@*%@@@@*@@@@@@@,,,,,@@,,,,,,,,,,,,,,,,,,@(. /@//#@ #@ %@ @&/////////(@*
@@@@& @@ @, ,@, %@@, %@@@@@@@,,,,,,,,@@,,,,,,,,,,,,,,,,*@ @@///@/ /@ %@@&%@ @@@@&@@@@@(
%@@@@/@% @ .@.%@( .@@@@@@@@,,,,,,,,,,@@,,,,,,,,,,,,,,,,@@@ @@/@@ *@, @@@@@# @#///%@@
@@#@@@ @ @@( @@///@@@@@/,,,,,,,,,,&@,,,,,,,,,,,,,,,,%@&.,, *@/@@ %@*@@@@@@@@@@@@@%////@,
&@@@@ @ &@////////@@@,,,,,,,,,,,#@,,,,,,,,,,,,,,,,(@/%@@@@@@@(&@ &@ @@ @#///////@#
.@( @/ ,@/////////@*,,,,,,,,,,#@,,,,,,,,,,,,,,,,/@///&@. .@/@@ &@ @@@ @@ @&////////@
/@# &@ @@///////@*,,,,,,,,,,&@,,,,,,,,,,,,,,,,/@/////@@, ,@/@# %@@ @@@@@@@@///////@#
@@ .@* @@/////@/,,,,,,,,,,@@,,,,,,,,,,,,,,,,/@/////&@,..@@/@ *@@@* *@@@@//////@/
(@/ @@ @@///@&,,,,,,,,,,@*,,,,,,,,,,,,,,,&@//////@@. @//@* #@@@@#/#@@
@@@@@ @@ @@/@@,,,,,,,,,/@,,,,,,,,,,,,,,*@@@@/////@@@@@@//@* /@@@@@@@@@*
@@/////(@@ @@ @@@,,,,,,,,,#@,,,,,,,,,,,,,%@///////@@@@&,@#/@& ,&@@@/@@@@@@@////@# %@@%
@/////////%@@ %@ @(,,,,,,,,%@,,,,,,,,,,,,@@//////@@@# (@(/@@../%& (@@@((@&//(@ .@@#
@(///////////@@( ( @@,,,,,,,,/@,,,,,,,,,,*@@@@@@@ @@.*@@#//@@ @@@& #@@@@//////@, /@@@/
.(&@@@@@@@@(////////////(@@# @*,,,,,,,,,,,,,,,,,,@@@@@. @@#@@(//(@@@@ @@,,@ .@@@@///////@@ @@@#
(@@@@@&. @@&//////////&@#/&@@* @*,,,,,,,,,,,,,,,,@@//////////(@@& @@ @@@@& @@@@@@@@@@@@ #@@,
%@@@, %@@@@@&(//////////(@@@/ (. @@,,,,,,,,,,,,,@@ *#%&@@@@@.@% %@ ,* .% *@@@@//////#@ .@@%
/@@@ /@@////////////////@@&@@@( @.@@ @@@@%##&@@@@ , %@ @@@ (@# / @@@@@@////@* @@@
&@@( *@@&/////////(@@///////&@@@@@@@@@/ &@@@.@@ %@@@. .. @@@*@(@.%@@( #@%,@@@@&//(//@, ,
@@@. ,@@@@@%/////////////(@(@@@@@@ @@@ #@ *#./@@@@ & &*.@#@ @#@ @@@&*@@@ (@@@@@@@@@@
@@@. ,@&////////////////@@#//@@@@@@@@@ @@@ @@ @@@@@@ @@@ ,@ @@@@@ &@@@@@@#/////@@
.@@/ @@@%(/////#@@@%////////%@/(##@%/@@@@@@@@@@@@@@@@@@@ @@@ /@ @@@@@@@@@@(#@////@.
@@#//////////%@/////////@(///@@/////&@//#@@@/%@@/#@/@@//(@/@@/////@%
/@@@@@&(////////////@@//////////@&/////////#//#///@@@##%@@@(
.@@#////////////%@&////////@@////////////////#@#
#@@@@@@@@@( /@@&(/////&@@@@@@@@@@@@@,
*/
}
},
});
try {
tinf_uncompress(src, undefined, hidden, undefined);
} catch (e) {
if (e == "Finish")
return Buffer.from(chnks);
}
return false; // possibly incorrect?
};
const extract = async (png: Buffer, doextract = true) => {
const reader = BufferReadStream(png).getReader();
const sneed = new PNGDecoder(reader, false);
const ret: WorkerEmbeddedFile[] = [];
let w: Buffer | undefined;
if (!csettings)
throw new Error("Settings uninit");
try {
let complete = false;
const idats: Buffer[] = [];
for await (const [name, chunk, crc, offset] of sneed.chunks()) {
let buff: Buffer;
switch (name) {
// should exist at the beginning of file to signal decoders if the file indeed has an embedded chunk
case 'tEXt':
buff = chunk;
if (buff.slice(4, 4 + CUM3.length).equals(CUM3)) {
if (!doextract)
return true;
const k = await decodeCoom3Payload(buff.slice(4 + CUM3.length));
ret.push(...k.filter(e => e));
}
if (buff.slice(4, 4 + CUM4.length).equals(CUM4)) {
if (!doextract)
return true;
const passed = buff.slice(4 + CUM4.length);
xor(passed, password);
const k = await decodeCoom3Payload(passed);
ret.push(...k.filter(e => e));
}
if (buff.slice(4, 4 + CUM5.length).equals(CUM5)) {
if (!doextract)
return true;
const passed = buff.slice(4 + CUM5.length);
const decoded = Buffer.from(passed.toString(), 'base64').toString().split(' ').map(e => {
return `https://${rprefs[e[0]]}/${e.slice(1)}`;
}).join(' ');
const k = await decodeCoom3Payload(Buffer.from(decoded));
ret.push(...k.filter(e => e));
}
// eslint-disable-next-line no-cond-assign
if (w = [CUM6, CUM7].find(e => buff.slice(4, 4 + e.length).equals(e))) {
const passed = buff.slice(4 + w.length);
if (!passed.toString().match(/^[0-9a-zA-Z+/=]+$/g)) continue;
const decoders = [(b: Buffer) => Buffer
.from(b.toString(), 'base64').toString(),
(b: Buffer) => Buffer.from(bs58.decode(passed.toString())).toString()];
for (const d of decoders) {
try {
const decoded = d(passed)
.split(' ')
.map(e => {
if (!(e[0] in rprefs))
throw "Uhh";
// should also check if the id has a len of 6-8 or ends in .pee
return `https://${rprefs[e[0]]}/${e.slice(1)}`;
}).join(' ');
if (!doextract)
return true;
const k = await decodeCoom3Payload(Buffer.from(decoded));
ret.push(...k.filter(e => e));
} catch (e) {
//
}
}
}
break;
case 'IDAT':
if (ret.length)
return ret;
buff = chunk;
idats.push(buff.slice(4));
break;
// eslint-disable-next-line no-fallthrough
case 'IEND':
complete = true;
// eslint-disable-next-line no-fallthrough
default:
break;
}
}
if (idats.length) {
let decoded: Buffer | false;
if ((decoded = extractFromRawDeflate(Buffer.concat(idats).slice(2))) === false)
return false;
const dec = decoded
.toString()
.split(' ')
.map(e => {
if (!(e[0] in rprefs) || e.length < 5) // link needs to be long enough, avoid false positives
throw "Uhh";
return `https://${rprefs[e[0]]}/${e.slice(1)}`;
}).join(' ');
if (doextract)
return decodeCoom3Payload(Buffer.from(dec));
return true;
}
} catch (e) {
if (e != "Uhh")
console.error(e);
} finally {
reader.releaseLock();
}
};
const buildChunk = (tag: string, data: Buffer) => {
const ret = Buffer.alloc(data.byteLength + 4);
ret.write(tag.slice(0, 4), 0);
data.copy(ret, 4);
return ret;
};
export const BufferWriteStream = () => {
let b = Buffer.from([]);
const ret = new WritableStream<Buffer>({
write(chunk) {
b = Buffer.concat([b, chunk]);
console.log("finished appending");
}
});
return [ret, () => b] as [WritableStream<Buffer>, () => Buffer];
};
const embedInRawDeflate = (b: Buffer, h: Buffer) => {
const src = new BitstreamReader();
const hid = new BitstreamReader();
hid.addBuffer(h);
src.addBuffer(b);
const chnks: Uint8Array[] = [];
tinf_uncompress(src, undefined, hid, c => chnks.push(c));
return Buffer.concat(chnks);
};
export const inject_data = async (container: File, injb: Buffer) => {
// some badly encoded pngs can emit things after the last character, so we explicitely pad with a 0
injb = Buffer.concat([injb, Buffer.from([0])]);
if (!csettings)
throw new Error("Settings uninit");
if (csettings.pmeth < 5) {
let magic = false;
const [writestream, extract] = BufferWriteStream();
const encoder = new PNGEncoder(writestream);
const decoder = new PNGDecoder(container.stream().getReader());
for await (const [name, chunk, crc, offset] of decoder.chunks()) {
if (magic && name != "IDAT")
break;
if (!magic && name == "IDAT") {
const passed = Buffer.from(injb);
switch (csettings.pmeth) {
case 0:
await encoder.insertchunk(["tEXt", buildChunk("tEXt", Buffer.concat([CUM3, passed])), 0, 0]);
break;
case 1:
xor(passed, password);
await encoder.insertchunk(["tEXt", buildChunk("tEXt", Buffer.concat([CUM4, Buffer.from(Buffer.from(passed).toString("base64"))])), 0, 0]);
break;
case 2:
await encoder.insertchunk(["tEXt", buildChunk("tEXt", Buffer.concat([CUM5, Buffer.from(Buffer.from(passed).toString("base64"))])), 0, 0]);
break;
case 3:
await encoder.insertchunk(["tEXt", buildChunk("tEXt", Buffer.concat([CUM6, Buffer.from(Buffer.from(passed).toString("base64"))])), 0, 0]);
break;
case 4:
await encoder.insertchunk(["tEXt", buildChunk("tEXt", Buffer.concat([CUM7, Buffer.from(bs58.encode(passed))])), 0, 0]);
break;
}
magic = true;
}
await encoder.insertchunk([name, chunk, crc, offset]);
}
await encoder.insertchunk(["IEND",
buildChunk("IEND", Buffer.from([])),
0,
0]);
return extract();
}
let pdec = new PNGDecoder(container.stream().getReader());
const concat: Buffer[] = [];
for await (const chk of pdec.chunks())
if (chk[0] == "IDAT")
concat.push(chk[1].slice(4));
const comp = Buffer.concat(concat);
const head = comp.slice(0, 2); // keep the header the same
const chksum = comp.slice(-4); // checksum is over the uncompressed data, so no need to recalculate
//const orig = zlib.inflateRawSync(comp.slice(2, -4));
const idatblk = embedInRawDeflate(comp.slice(2, -4), injb);
//const norig = zlib.inflateRawSync(idatblk);
//console.log('diff', orig.compare(norig));
const [writestream, extract] = BufferWriteStream();
const penc = new PNGEncoder(writestream);
pdec = new PNGDecoder(container.stream().getReader()); // restart again
let ins = false;
for await (const chk of pdec.chunks()) {
if (chk[0] != "IDAT") {
await penc.insertchunk(chk);
} else {
if (!ins) {
await penc.insertchunk(["IDAT", Buffer.concat([Buffer.from('IDAT'), head, idatblk, chksum]), 0, 0]);
ins = true;
}
}
}
await penc.dtor();
console.log("Finished writing");
return extract();
};
const inject = async (container: File, links: string[]) => {
links = links.map(link => {
for (const h of filehosts) {
if (link.includes(h.serving)) {
const end = link.split('/').slice(-1)[0];
return `${prefs[h.serving]}${end}`;
}
}
return '';
});
const injb = Buffer.from(links.join(' '));
return inject_data(container, injb);
};
const has_embed = async (png: Buffer) => {
const r = await extract(png, false);
return !!r;
};
export default {
extract,
has_embed,
inject,
match: fn => !!fn.match(/\.png$/)
} as ImageProcessor;