Table of Contents

Cryptographic Image Steganography & Bit-Plane Forensics Studio Source Code

Create a project directory with three files: index.html, style.css, and script.js.

1. HTML (index.html)

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>StegoVault: Cryptographic Image Steganography & Bit-Plane Forensics</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>

  <header class="navbar">
    <div class="logo">NitroX Web</div>
    <span class="badge">Steganography & Forensics Engine</span>
  </header>

  <main class="studio-wrapper">
    <section class="hero-intro">
      <h1>Pixel Steganography & Bit-Plane Forensics Studio</h1>
      <p>Conceal AES-256-GCM encrypted payloads inside the Least Significant Bits (LSB) of lossless PNG images, or inspect any image's 0-bit plane using the live forensic X-Ray visualizer.</p>
    </section>

    <div class="workspace-grid">
      
      <!-- Left Column: Controls (Encode & Decode Tabs) -->
      <section class="control-card">
        <div class="mode-tabs" role="tablist">
          <button id="tabEncode" class="mode-tab active" data-mode="encode" role="tab" aria-selected="true">1. Hide Secret (Encode)</button>
          <button id="tabDecode" class="mode-tab" data-mode="decode" role="tab" aria-selected="false">2. Extract Secret (Decode)</button>
        </div>

        <!-- Upload Carrier / Stego Image -->
        <div class="upload-zone" id="dropZone">
          <input type="file" id="imageInput" accept="image/png, image/jpeg, image/webp" hidden>
          <div class="upload-content">
            <svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="upload-icon">
              <rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
              <circle cx="8.5" cy="8.5" r="1.5"></circle>
              <polyline points="21 15 16 10 5 21"></polyline>
            </svg>
            <p><strong>Click or Drag Image Here</strong> (PNG, JPG, WebP)</p>
            <span id="imageMeta" class="meta-pill">No image loaded</span>
          </div>
        </div>

        <!-- ENCODE PANEL -->
        <div id="encodePanel" class="panel-body">
          <div class="field-group">
            <div class="label-bar">
              <label for="secretInput">Secret Payload Text</label>
              <span id="capacityMeter" class="capacity-text">0 / 0 Bytes</span>
            </div>
            <textarea id="secretInput" rows="5" placeholder="Enter confidential coordinates, private keys, or secret notes to embed inside the pixels..."></textarea>
          </div>

          <div class="field-group">
            <label for="encodePassword">AES-256 Passphrase <span class="optional">(Optional but Recommended)</span></label>
            <input type="password" id="encodePassword" placeholder="Leave blank for raw LSB, or enter password to encrypt...">
          </div>

          <button id="btnEmbed" class="action-btn primary-mint" type="button" disabled>
            Embed Payload into Pixels
          </button>
        </div>

        <!-- DECODE PANEL -->
        <div id="decodePanel" class="panel-body" hidden>
          <div class="field-group">
            <label for="decodePassword">Decryption Passphrase <span class="optional">(If Encrypted)</span></label>
            <input type="password" id="decodePassword" placeholder="Enter passphrase if payload was encrypted...">
          </div>

          <button id="btnExtract" class="action-btn primary-blue" type="button" disabled>
            Scan LSB Matrix & Extract Secret
          </button>

          <div class="field-group output-group">
            <div class="label-bar">
              <label for="decodedOutput">Extracted Secret Payload</label>
              <button id="btnCopySecret" class="mini-btn" type="button">Copy</button>
            </div>
            <textarea id="decodedOutput" rows="5" readonly placeholder="Extracted message will appear here..."></textarea>
          </div>
        </div>

        <!-- Status Alert Box -->
        <div id="statusBox" class="status-alert" hidden></div>
      </section>

      <!-- Right Column: Visual Canvas & Forensic Bit-Plane Inspector -->
      <section class="visualizer-card">
        <div class="visualizer-header">
          <div>
            <h2>Live Pixel & Bit-Plane Inspector</h2>
            <p class="sub-text">Toggle the X-Ray filter to view the 0th Least Significant Bit (LSB) noise floor.</p>
          </div>
          <div class="view-toggles">
            <button id="btnViewNormal" class="toggle-pill active" type="button">Normal View</button>
            <button id="btnViewForensic" class="toggle-pill" type="button">LSB Forensic X-Ray</button>
          </div>
        </div>

        <div class="canvas-stage">
          <canvas id="stegoCanvas"></canvas>
          <div id="emptyCanvasPrompt" class="empty-stage">
            Load an image on the left to inspect its pixel matrix and bit planes.
          </div>
        </div>

        <div class="forensic-stats">
          <div class="stat-card">
            <span class="stat-label">Dimensions</span>
            <strong id="statDimensions">—</strong>
          </div>
          <div class="stat-card">
            <span class="stat-label">Max LSB Capacity</span>
            <strong id="statCapacity">—</strong>
          </div>
          <div class="stat-card">
            <span class="stat-label">Signature Status</span>
            <strong id="statSignature">Waiting...</strong>
          </div>
        </div>

        <div id="downloadContainer" class="download-bar" hidden>
          <a id="downloadLink" href="#" download="stego-vault-output.png" class="download-btn">
            Download Lossless Stego PNG
          </a>
        </div>
      </section>

    </div>
  </main>

  <script src="script.js"></script>
</body>
</html>

2. CSS (style.css)

/* ==========================================================================
   NitroX Web Variables & Reset
   ========================================================================== */
:root {
  --bg-dark: #070a12;
  --bg-card: #101626;
  --bg-input: #060911;
  --border-color: #1e293b;
  --text-main: #f8fafc;
  --text-muted: #94a3b8;
  --accent-mint: #10b981;
  --accent-mint-hover: #059669;
  --accent-blue: #3b82f6;
  --accent-blue-hover: #2563eb;
  --danger: #ef4444;
  --radius-lg: 16px;
  --radius-md: 10px;
  --shadow-lg: 0 20px 35px -10px rgba(0, 0, 0, 0.65);
  --transition: 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}

* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

body {
  font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  background-color: var(--bg-dark);
  color: var(--text-main);
  line-height: 1.6;
  min-height: 100vh;
  padding-bottom: 4rem;
}

/* ==========================================================================
   Navigation Bar
   ========================================================================== */
.navbar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 1.2rem 2.25rem;
  border-bottom: 1px solid var(--border-color);
  background: rgba(16, 22, 38, 0.85);
  backdrop-filter: blur(12px);
  position: sticky;
  top: 0;
  z-index: 100;
}

.logo {
  font-weight: 800;
  font-size: 1.25rem;
  letter-spacing: -0.02em;
  color: var(--accent-mint);
}

.badge {
  background: rgba(16, 185, 129, 0.12);
  color: var(--accent-mint);
  font-size: 0.75rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  padding: 0.35rem 0.85rem;
  border-radius: 999px;
}

/* ==========================================================================
   Hero & Grid Layout
   ========================================================================== */
.studio-wrapper {
  max-width: 1180px;
  margin: 2.5rem auto;
  padding: 0 1.5rem;
}

.hero-intro {
  text-align: center;
  margin-bottom: 2.75rem;
}

.hero-intro h1 {
  font-size: 2.15rem;
  font-weight: 850;
  letter-spacing: -0.03em;
  margin-bottom: 0.6rem;
}

.hero-intro p {
  color: var(--text-muted);
  max-width: 740px;
  margin: 0 auto;
  font-size: 0.98rem;
}

.workspace-grid {
  display: grid;
  grid-template-columns: 1fr 1.15fr;
  gap: 2rem;
  align-items: start;
}

.control-card,
.visualizer-card {
  background-color: var(--bg-card);
  border: 1px solid var(--border-color);
  border-radius: var(--radius-lg);
  padding: 2rem;
  box-shadow: var(--shadow-lg);
}

/* ==========================================================================
   Mode Tabs & Upload Zone
   ========================================================================== */
.mode-tabs {
  display: flex;
  background-color: var(--bg-input);
  padding: 0.3rem;
  border-radius: var(--radius-md);
  border: 1px solid var(--border-color);
  margin-bottom: 1.5rem;
}

.mode-tab {
  flex: 1;
  background: transparent;
  border: none;
  color: var(--text-muted);
  font-weight: 700;
  font-size: 0.85rem;
  padding: 0.65rem;
  border-radius: 7px;
  cursor: pointer;
  transition: all var(--transition);
}

.mode-tab.active {
  background-color: var(--border-color);
  color: var(--text-main);
}

.upload-zone {
  border: 2px dashed var(--border-color);
  background-color: var(--bg-input);
  border-radius: var(--radius-md);
  padding: 1.65rem 1rem;
  text-align: center;
  cursor: pointer;
  margin-bottom: 1.5rem;
  transition: border-color var(--transition);
}

.upload-zone:hover {
  border-color: var(--accent-mint);
}

.upload-content {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.5rem;
}

.upload-icon {
  color: var(--accent-mint);
}

.upload-content p {
  font-size: 0.9rem;
  color: var(--text-muted);
}

.meta-pill {
  background-color: var(--bg-card);
  border: 1px solid var(--border-color);
  font-family: monospace;
  font-size: 0.78rem;
  padding: 0.25rem 0.75rem;
  border-radius: 6px;
  color: var(--text-main);
}

/* ==========================================================================
   Forms, Inputs & Buttons
   ========================================================================== */
.panel-body {
  display: flex;
  flex-direction: column;
  gap: 1.25rem;
}

.field-group {
  display: flex;
  flex-direction: column;
  gap: 0.45rem;
}

.label-bar {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

label {
  font-size: 0.84rem;
  font-weight: 700;
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: 0.03em;
}

.optional {
  font-weight: 400;
  text-transform: none;
  font-size: 0.78rem;
}

.capacity-text {
  font-family: monospace;
  font-size: 0.78rem;
  color: var(--accent-mint);
}

textarea,
input[type="password"] {
  width: 100%;
  background-color: var(--bg-input);
  border: 1.5px solid var(--border-color);
  border-radius: var(--radius-md);
  padding: 0.8rem 0.95rem;
  color: var(--text-main);
  font-family: inherit;
  font-size: 0.92rem;
  outline: none;
  resize: vertical;
  transition: border-color var(--transition);
}

textarea:focus,
input[type="password"]:focus {
  border-color: var(--accent-mint);
}

.action-btn {
  width: 100%;
  padding: 0.9rem 1.25rem;
  border: none;
  border-radius: var(--radius-md);
  font-size: 0.95rem;
  font-weight: 700;
  cursor: pointer;
  transition: opacity var(--transition), background-color var(--transition);
}

.action-btn:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

.primary-mint {
  background-color: var(--accent-mint);
  color: #05080e;
}
.primary-mint:hover:not(:disabled) {
  background-color: var(--accent-mint-hover);
}

.primary-blue {
  background-color: var(--accent-blue);
  color: #ffffff;
}
.primary-blue:hover:not(:disabled) {
  background-color: var(--accent-blue-hover);
}

.mini-btn {
  background-color: var(--border-color);
  color: var(--text-main);
  border: none;
  border-radius: 5px;
  padding: 0.2rem 0.65rem;
  font-size: 0.75rem;
  font-weight: 600;
  cursor: pointer;
}

.status-alert {
  margin-top: 1.25rem;
  padding: 0.85rem 1rem;
  border-radius: var(--radius-md);
  font-size: 0.86rem;
  font-weight: 600;
}

.status-alert.success {
  background: rgba(16, 185, 129, 0.12);
  color: var(--accent-mint);
  border: 1px solid rgba(16, 185, 129, 0.3);
}

.status-alert.error {
  background: rgba(239, 68, 68, 0.12);
  color: var(--danger);
  border: 1px solid rgba(239, 68, 68, 0.3);
}

/* ==========================================================================
   Visualizer & Bit-Plane Canvas Stage
   ========================================================================== */
.visualizer-header {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: 1rem;
  margin-bottom: 1.25rem;
  flex-wrap: wrap;
}

.visualizer-header h2 {
  font-size: 1.25rem;
  font-weight: 800;
}

.sub-text {
  font-size: 0.82rem;
  color: var(--text-muted);
}

.view-toggles {
  display: flex;
  background-color: var(--bg-input);
  padding: 0.25rem;
  border-radius: 8px;
  border: 1px solid var(--border-color);
}

.toggle-pill {
  background: transparent;
  border: none;
  color: var(--text-muted);
  font-size: 0.78rem;
  font-weight: 700;
  padding: 0.45rem 0.8rem;
  border-radius: 6px;
  cursor: pointer;
}

.toggle-pill.active {
  background-color: var(--accent-mint);
  color: #05080e;
}

.canvas-stage {
  background-color: var(--bg-input);
  border: 1px solid var(--border-color);
  border-radius: var(--radius-md);
  min-height: 310px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1rem;
  position: relative;
  overflow: hidden;
  margin-bottom: 1.25rem;
}

canvas {
  max-width: 100%;
  max-height: 360px;
  border-radius: 6px;
  display: none;
}

.empty-stage {
  color: var(--text-muted);
  font-size: 0.88rem;
  text-align: center;
  max-width: 280px;
}

.forensic-stats {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 0.85rem;
}

.stat-card {
  background-color: var(--bg-input);
  border: 1px solid var(--border-color);
  border-radius: 8px;
  padding: 0.75rem;
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
}

.stat-label {
  font-size: 0.72rem;
  text-transform: uppercase;
  color: var(--text-muted);
  font-weight: 700;
}

.stat-card strong {
  font-family: monospace;
  font-size: 0.9rem;
  color: var(--text-main);
}

.download-bar {
  margin-top: 1.25rem;
}

.download-btn {
  display: block;
  width: 100%;
  text-align: center;
  background-color: var(--accent-blue);
  color: #ffffff;
  text-decoration: none;
  font-weight: 700;
  font-size: 0.92rem;
  padding: 0.85rem;
  border-radius: var(--radius-md);
  transition: background-color var(--transition);
}

.download-btn:hover {
  background-color: var(--accent-blue-hover);
}

@media (max-width: 900px) {
  .workspace-grid {
    grid-template-columns: 1fr;
  }
}

3. JavaScript (script.js)

document.addEventListener('DOMContentLoaded', () => {
  // Custom 3-Byte Magic Header: 'NXS' (NitroX Stego) = [78, 88, 83]
  const MAGIC_BYTES = [78, 88, 83];
  const HEADER_SIZE_BYTES = 8; // 3 Magic + 1 Flag + 4 Length (Uint32)

  // DOM Elements
  const tabEncode = document.getElementById('tabEncode');
  const tabDecode = document.getElementById('tabDecode');
  const encodePanel = document.getElementById('encodePanel');
  const decodePanel = document.getElementById('decodePanel');

  const dropZone = document.getElementById('dropZone');
  const imageInput = document.getElementById('imageInput');
  const imageMeta = document.getElementById('imageMeta');

  const secretInput = document.getElementById('secretInput');
  const capacityMeter = document.getElementById('capacityMeter');
  const encodePassword = document.getElementById('encodePassword');
  const btnEmbed = document.getElementById('btnEmbed');

  const decodePassword = document.getElementById('decodePassword');
  const btnExtract = document.getElementById('btnExtract');
  const decodedOutput = document.getElementById('decodedOutput');
  const btnCopySecret = document.getElementById('btnCopySecret');

  const statusBox = document.getElementById('statusBox');
  const stegoCanvas = document.getElementById('stegoCanvas');
  const ctx = stegoCanvas.getContext('2d', { willReadFrequently: true });
  const emptyCanvasPrompt = document.getElementById('emptyCanvasPrompt');

  const btnViewNormal = document.getElementById('btnViewNormal');
  const btnViewForensic = document.getElementById('btnViewForensic');

  const statDimensions = document.getElementById('statDimensions');
  const statCapacity = document.getElementById('statCapacity');
  const statSignature = document.getElementById('statSignature');
  const downloadContainer = document.getElementById('downloadContainer');
  const downloadLink = document.getElementById('downloadLink');

  // State
  let workingImageData = null; // Stores current ImageData (Normal View)
  let maxPayloadBytes = 0;
  let isForensicView = false;

  // ==========================================================================
  // 1. UI Status & Mode Switching
  // ==========================================================================
  const showStatus = (message, type = 'success') => {
    statusBox.textContent = message;
    statusBox.className = `status-alert ${type}`;
    statusBox.hidden = false;
  };

  const switchMode = (mode) => {
    statusBox.hidden = true;
    if (mode === 'encode') {
      tabEncode.classList.add('active');
      tabDecode.classList.remove('active');
      encodePanel.hidden = false;
      decodePanel.hidden = true;
    } else {
      tabDecode.classList.add('active');
      tabEncode.classList.remove('active');
      decodePanel.hidden = false;
      encodePanel.hidden = true;
    }
  };

  tabEncode.addEventListener('click', () => switchMode('encode'));
  tabDecode.addEventListener('click', () => switchMode('decode'));

  // ==========================================================================
  // 2. Web Crypto API (PBKDF2 + AES-256-GCM)
  // ==========================================================================
  const deriveKey = async (password, salt) => {
    const enc = new TextEncoder();
    const keyMaterial = await window.crypto.subtle.importKey(
      'raw',
      enc.encode(password),
      'PBKDF2',
      false,
      ['deriveKey']
    );
    return window.crypto.subtle.deriveKey(
      { name: 'PBKDF2', salt, iterations: 50000, hash: 'SHA-256' },
      keyMaterial,
      { name: 'AES-GCM', length: 256 },
      false,
      ['encrypt', 'decrypt']
    );
  };

  const encryptPayload = async (plainBytes, password) => {
    const salt = window.crypto.getRandomValues(new Uint8Array(16));
    const iv = window.crypto.getRandomValues(new Uint8Array(12));
    const key = await deriveKey(password, salt);
    const cipherBuffer = await window.crypto.subtle.encrypt(
      { name: 'AES-GCM', iv },
      key,
      plainBytes
    );
    const cipherBytes = new Uint8Array(cipherBuffer);
    // Bundle: [16B Salt][12B IV][Ciphertext]
    const bundled = new Uint8Array(28 + cipherBytes.length);
    bundled.set(salt, 0);
    bundled.set(iv, 16);
    bundled.set(cipherBytes, 28);
    return bundled;
  };

  const decryptPayload = async (bundledBytes, password) => {
    if (bundledBytes.length < 29) {
      throw new Error('Corrupted encrypted payload.');
    }
    const salt = bundledBytes.subarray(0, 16);
    const iv = bundledBytes.subarray(16, 28);
    const ciphertext = bundledBytes.subarray(28);
    const key = await deriveKey(password, salt);
    const plainBuffer = await window.crypto.subtle.decrypt(
      { name: 'AES-GCM', iv },
      key,
      ciphertext
    );
    return new Uint8Array(plainBuffer);
  };

  // ==========================================================================
  // 3. Bitwise LSB Injection & Extraction Engine
  // ==========================================================================
  // Embeds a Uint8Array byte stream into RGB Least Significant Bits
  // Forces Alpha (index + 3) to 255 to prevent browser premultiplied-alpha corruption
  const embedBytesIntoImageData = (imgData, byteStream) => {
    const pixels = new Uint8ClampedArray(imgData.data);
    const totalBits = byteStream.length * 8;
    let bitIndex = 0;

    for (let i = 0; i < pixels.length; i += 4) {
      // Force opaque Alpha channel so canvas never alters RGB values
      pixels[i + 3] = 255;

      // Inject into R, G, B channels (offsets 0, 1, 2)
      for (let channel = 0; channel < 3; channel++) {
        if (bitIndex < totalBits) {
          const bytePos = Math.floor(bitIndex / 8);
          const bitOffset = 7 - (bitIndex % 8);
          const bitVal = (byteStream[bytePos] >> bitOffset) & 1;

          // Clear LSB and set new bit
          pixels[i + channel] = (pixels[i + channel] & 0xFE) | bitVal;
          bitIndex++;
        }
      }
    }
    return new ImageData(pixels, imgData.width, imgData.height);
  };

  // Reads N bytes from the RGB Least Significant Bits starting at a byte offset
  const extractBytesFromImageData = (imgData, byteOffset, byteCount) => {
    const pixels = imgData.data;
    const output = new Uint8Array(byteCount);
    const startBit = byteOffset * 8;
    const endBit = startBit + byteCount * 8;

    let currentBitIndex = 0;

    for (let i = 0; i < pixels.length && currentBitIndex < endBit; i += 4) {
      for (let channel = 0; channel < 3; channel++) {
        if (currentBitIndex >= startBit && currentBitIndex < endBit) {
          const targetBitPos = currentBitIndex - startBit;
          const targetByteIndex = Math.floor(targetBitPos / 8);
          const bitShift = 7 - (targetBitPos % 8);
          const lsb = pixels[i + channel] & 1;

          output[targetByteIndex] |= (lsb << bitShift);
        }
        currentBitIndex++;
      }
    }
    return output;
  };

  // ==========================================================================
  // 4. Forensic Bit-Plane X-Ray Visualizer
  // ==========================================================================
  const renderCanvasView = () => {
    if (!workingImageData) return;

    if (!isForensicView) {
      ctx.putImageData(workingImageData, 0, 0);
    } else {
      // Amplify the 0th bit (LSB) of R, G, B to full 0 or 255 high contrast
      const src = workingImageData.data;
      const xrayPixels = new Uint8ClampedArray(src.length);

      for (let i = 0; i < src.length; i += 4) {
        xrayPixels[i]     = (src[i] & 1) * 255;     // Red LSB
        xrayPixels[i + 1] = (src[i + 1] & 1) * 255; // Green LSB
        xrayPixels[i + 2] = (src[i + 2] & 1) * 255; // Blue LSB
        xrayPixels[i + 3] = 255;
      }
      const xrayData = new ImageData(xrayPixels, workingImageData.width, workingImageData.height);
      ctx.putImageData(xrayData, 0, 0);
    }
  };

  btnViewNormal.addEventListener('click', () => {
    isForensicView = false;
    btnViewNormal.classList.add('active');
    btnViewForensic.classList.remove('active');
    renderCanvasView();
  });

  btnViewForensic.addEventListener('click', () => {
    isForensicView = true;
    btnViewForensic.classList.add('active');
    btnViewNormal.classList.remove('active');
    renderCanvasView();
  });

  // Quick signature check on loaded image
  const inspectSignature = (imgData) => {
    const header = extractBytesFromImageData(imgData, 0, HEADER_SIZE_BYTES);
    const isNXS =
      header[0] === MAGIC_BYTES[0] &&
      header[1] === MAGIC_BYTES[1] &&
      header[2] === MAGIC_BYTES[2];

    if (isNXS) {
      const isEncrypted = header[3] === 1;
      statSignature.textContent = isEncrypted ? 'NXS (Encrypted)' : 'NXS (Cleartext)';
      statSignature.style.color = 'var(--accent-mint)';
    } else {
      statSignature.textContent = 'Clean / No NXS Tag';
      statSignature.style.color = 'var(--text-muted)';
    }
  };

  // ==========================================================================
  // 5. Image Loading & Capacity Calculation
  // ==========================================================================
  const loadImageFile = (file) => {
    if (!file || !file.type.startsWith('image/')) {
      showStatus('Please select a valid image file (PNG, JPG, or WebP).', 'error');
      return;
    }

    const reader = new FileReader();
    reader.onload = (e) => {
      const img = new Image();
      img.onload = () => {
        stegoCanvas.width = img.width;
        stegoCanvas.height = img.height;
        ctx.drawImage(img, 0, 0);

        workingImageData = ctx.getImageData(0, 0, img.width, img.height);
        emptyCanvasPrompt.hidden = true;
        stegoCanvas.style.display = 'block';

        // Calculate capacity: 3 bits per pixel (R, G, B) -> / 8 for bytes
        const totalUsableBytes = Math.floor((img.width * img.height * 3) / 8);
        maxPayloadBytes = Math.max(0, totalUsableBytes - HEADER_SIZE_BYTES - 48); // Reserve header + crypto overhead

        imageMeta.textContent = `${file.name} (${img.width}×${img.height})`;
        statDimensions.textContent = `${img.width} × ${img.height} px`;
        statCapacity.textContent = `${(maxPayloadBytes / 1024).toFixed(1)} KB`;

        updateCapacityCounter();
        inspectSignature(workingImageData);
        renderCanvasView();

        btnEmbed.disabled = false;
        btnExtract.disabled = false;
        downloadContainer.hidden = true;
        statusBox.hidden = true;
      };
      img.src = e.target.result;
    };
    reader.readAsDataURL(file);
  };

  dropZone.addEventListener('click', () => imageInput.click());
  dropZone.addEventListener('dragover', (e) => {
    e.preventDefault();
    dropZone.style.borderColor = 'var(--accent-mint)';
  });
  dropZone.addEventListener('dragleave', () => {
    dropZone.style.borderColor = 'var(--border-color)';
  });
  dropZone.addEventListener('drop', (e) => {
    e.preventDefault();
    dropZone.style.borderColor = 'var(--border-color)';
    if (e.dataTransfer.files.length) loadImageFile(e.dataTransfer.files[0]);
  });
  imageInput.addEventListener('change', (e) => {
    if (e.target.files.length) loadImageFile(e.target.files[0]);
  });

  const updateCapacityCounter = () => {
    const usedBytes = new TextEncoder().encode(secretInput.value).length;
    capacityMeter.textContent = `${usedBytes} / ${maxPayloadBytes} Bytes`;
    capacityMeter.style.color = usedBytes > maxPayloadBytes ? 'var(--danger)' : 'var(--accent-mint)';
  };

  secretInput.addEventListener('input', updateCapacityCounter);

  // ==========================================================================
  // 6. Embed Secret Action (Encode)
  // ==========================================================================
  btnEmbed.addEventListener('click', async () => {
    if (!workingImageData) return;
    const text = secretInput.value.trim();
    if (!text) {
      showStatus('Please enter a secret message to embed.', 'error');
      return;
    }

    try {
      btnEmbed.disabled = true;
      const rawBytes = new TextEncoder().encode(text);
      const pass = encodePassword.value;
      const isEncrypted = pass.length > 0 ? 1 : 0;

      const payloadBytes = isEncrypted
        ? await encryptPayload(rawBytes, pass)
        : rawBytes;

      if (payloadBytes.length > maxPayloadBytes) {
        showStatus('Payload exceeds the pixel capacity of this carrier image!', 'error');
        return;
      }

      // Build 8-Byte Header: [78, 88, 83, Flag, Len3, Len2, Len1, Len0]
      const totalStream = new Uint8Array(HEADER_SIZE_BYTES + payloadBytes.length);
      totalStream.set(MAGIC_BYTES, 0);
      totalStream[3] = isEncrypted;

      const len = payloadBytes.length;
      totalStream[4] = (len >>> 24) & 0xFF;
      totalStream[5] = (len >>> 16) & 0xFF;
      totalStream[6] = (len >>> 8) & 0xFF;
      totalStream[7] = len & 0xFF;

      totalStream.set(payloadBytes, HEADER_SIZE_BYTES);

      // Inject into LSBs
      workingImageData = embedBytesIntoImageData(workingImageData, totalStream);
      renderCanvasView();
      inspectSignature(workingImageData);

      // Generate Lossless PNG Download from Unfiltered ImageData
      const exportCanvas = document.createElement('canvas');
      exportCanvas.width = workingImageData.width;
      exportCanvas.height = workingImageData.height;
      exportCanvas.getContext('2d').putImageData(workingImageData, 0, 0);

      exportCanvas.toBlob((blob) => {
        const url = URL.createObjectURL(blob);
        downloadLink.href = url;
        downloadContainer.hidden = false;
        showStatus(`Success! Embedded ${payloadBytes.length} bytes into the pixel LSB matrix.`);
      }, 'image/png');

    } catch (err) {
      showStatus(`Embedding failed: ${err.message}`, 'error');
    } finally {
      btnEmbed.disabled = false;
    }
  });

  // ==========================================================================
  // 7. Extract Secret Action (Decode)
  // ==========================================================================
  btnExtract.addEventListener('click', async () => {
    if (!workingImageData) return;
    decodedOutput.value = '';

    try {
      btnExtract.disabled = true;
      const header = extractBytesFromImageData(workingImageData, 0, HEADER_SIZE_BYTES);

      if (
        header[0] !== MAGIC_BYTES[0] ||
        header[1] !== MAGIC_BYTES[1] ||
        header[2] !== MAGIC_BYTES[2]
      ) {
        showStatus('No valid StegoVault (NXS) signature detected in this image.', 'error');
        return;
      }

      const isEncrypted = header[3] === 1;
      const payloadLength =
        ((header[4] << 24) >>> 0) +
        ((header[5] << 16) >>> 0) +
        ((header[6] << 8) >>> 0) +
        header[7];

      const maxPossible = Math.floor((workingImageData.width * workingImageData.height * 3) / 8) - HEADER_SIZE_BYTES;
      if (payloadLength <= 0 || payloadLength > maxPossible) {
        showStatus('Corrupted length header inside pixel matrix.', 'error');
        return;
      }

      const extractedPayload = extractBytesFromImageData(workingImageData, HEADER_SIZE_BYTES, payloadLength);

      if (isEncrypted) {
        const pass = decodePassword.value;
        if (!pass) {
          showStatus('This image contains an AES-256 encrypted payload. Enter the passphrase above.', 'error');
          return;
        }
        const decryptedBytes = await decryptPayload(extractedPayload, pass);
        decodedOutput.value = new TextDecoder().decode(decryptedBytes);
        showStatus('Payload successfully decrypted and extracted!');
      } else {
        decodedOutput.value = new TextDecoder().decode(extractedPayload);
        showStatus('Cleartext payload extracted from LSB matrix!');
      }
    } catch (err) {
      showStatus('Decryption failed: Invalid passphrase or altered image pixels.', 'error');
    } finally {
      btnExtract.disabled = false;
    }
  });

  // Copy Extracted Output
  btnCopySecret.addEventListener('click', async () => {
    if (!decodedOutput.value) return;
    await navigator.clipboard.writeText(decodedOutput.value);
    btnCopySecret.textContent = 'Copied!';
    setTimeout(() => (btnCopySecret.textContent = 'Copy'), 1800);
  });
});

Building a Cryptographic Image Steganography & Bit-Plane Forensics Studio in HTML, CSS & JS

While encryption transforms a readable message into unreadable ciphertext, the mere presence of an encrypted file often attracts unwanted attention. Steganography takes a fundamentally different approach: it conceals the very existence of the data by embedding the binary stream directly inside an ordinary, innocuous carrier file—such as a photograph.

When you combine AES-256-GCM authenticated encryption with Least Significant Bit (LSB) pixel manipulation and a Bit-Plane Forensic Visualizer, you get a zero-knowledge privacy studio that runs 100% inside the browser without uploading a single byte to an external server.

In this technical guide, we will break down how digital images store sub-pixel color values, why standard HTML5 <canvas> alpha channels silently corrupt naive steganography scripts, and how our custom binary framing protocol encodes and decodes hidden data effortlessly.

1. The Mathematics of Least Significant Bit (LSB) Injection

Every pixel rendered on an HTML5 <canvas> is represented inside an ImageData array as four consecutive 8-bit unsigned integers ranging from 0 to 255: Red (R), Green (G), Blue (B), and Alpha (A).

In binary, an 8-bit Red channel value of 214 looks like this:214_{10} = 11010110_2

If we overwrite the rightmost bit—known as the 0th bit or Least Significant Bit (LSB)—from 0 to 1, the value changes from 214 (11010110) to 215 (11010111). Because the human eye cannot perceive a \frac{1}{255} shift in luminance or chrominance, we can repurpose the LSB of every Red, Green, and Blue channel to store our own binary payload bits.

Using bitwise operators in JavaScript, clearing the LSB and injecting a payload bit (b \in \{0, 1\}) takes a single line of code:

// 0xFE is 11111110 in binary. ANDing clears the LSB; ORing inserts bitVal.
pixels[i + channel] = (pixels[i + channel] & 0xFE) | bitVal;

Since each pixel donates 3 bits (one each from R, G, and B), the maximum theoretical byte capacity C of an image with width W and height H is:C = \left\lfloor \frac{W \times H \times 3}{8} \right\rfloor \text{ bytes}

For a standard 1920 \times 1080 Full HD image, that equals 777,600 bytes (~759 KB) of invisible storage capacity.

2. Solving the Browser “Premultiplied Alpha” Corruption Bug

A critical reason most client-side JavaScript steganography tutorials fail in production is Premultiplied Alpha.

According to the HTML5 Canvas specification, whenever you call ctx.putImageData() and export an image, the browser’s rendering engine internally multiplies the R, G, and B values by the pixel’s Alpha transparency fraction (\frac{\alpha}{255}) and rounds the result. If a pixel has an Alpha value of 254 instead of 255, the browser will round the RGB integers, destroying the Least Significant Bits you just embedded!

Our engine eliminates this bug permanently with two rules:

  1. Alpha Channel Pinning: During the embedding loop, we never store data in the Alpha channel (pixels[i + 3]) and explicitly lock every pixel’s Alpha byte to 255 (100% opaque).
  2. Lossless PNG Export: Lossy formats like JPEG or WebP apply Discrete Cosine Transform (DCT) compression, which discards high-frequency LSB data. Even if a user uploads a .jpg or .webp carrier image, our studio exports the modified canvas strictly as an uncompressed image/png blob.

3. The Custom NXS Binary Frame Protocol

When decoding an image, how does the JavaScript engine know whether the image actually contains a hidden message, whether it is encrypted, and where the message stops so it doesn’t read millions of trailing random pixel bits?

We prepend an 8-byte (64-bit) Binary Header before the payload bits:

+-----------------------------------------------------------------------+
| Bytes 0..2 (24 bits) | Byte 3 (8 bits) | Bytes 4..7 (32-bit Uint32)   |
| "NXS" Magic [78,88,83]| Crypto Flag 0/1 | Exact Payload Byte Length    |
+-----------------------------------------------------------------------+
  1. Magic Signature (NXS): On decode, the script extracts the first 24 bits (8 pixels). If they do not equal ASCII [78, 88, 83], the engine immediately knows the image does not contain a StegoVault payload.
  2. Encryption Flag (Byte 3): Indicates whether the payload is raw UTF-8 text (0) or an AES-256-GCM bundle (1) containing a 16-byte PBKDF2 salt, 12-byte IV, and authenticated ciphertext.
  3. 32-Bit Big-Endian Length (Bytes 4..7): Tells the bit-reader the exact byte count to extract, halting the loop the microsecond the last payload bit is read.

4. How the Live Bit-Plane Forensic X-Ray Works

Steganalysis (the forensic art of detecting hidden data in images) often begins with Bit-Plane Slicing. In a natural photograph or illustration, even the 0th Least Significant Bit exhibits subtle spatial contours along sharp edges and flat backgrounds. However, when an encrypted payload is injected into the top of an image, those pixels exhibit pure, uniform 50/50 Shannon entropy noise.

When you click LSB Forensic X-Ray in our studio, the visualizer isolates the 0th bit of every color channel and multiplies it by 255:

xrayPixels[i]     = (src[i] & 1) * 255;     // Red 0th Bit Plane
xrayPixels[i + 1] = (src[i + 1] & 1) * 255; // Green 0th Bit Plane
xrayPixels[i + 2] = (src[i + 2] & 1) * 255; // Blue 0th Bit Plane

This turns invisible 0 and 1 micro-variations into a high-contrast neon bitmap, allowing you to visually see the exact horizontal band at the top of the image where your encrypted payload bits are stored.