<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Keishin Senzaki</title>
    <description>The latest articles on DEV Community by Keishin Senzaki (@reordo).</description>
    <link>https://dev.to/reordo</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4158812%2F38390f13-8b62-4e9f-ac27-269e783ceb05.png</url>
      <title>DEV Community: Keishin Senzaki</title>
      <link>https://dev.to/reordo</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/reordo"/>
    <language>en</language>
    <item>
      <title>Hiding an image in a PNG and decoding it with SVG filters, without CORS pixel access</title>
      <dc:creator>Keishin Senzaki</dc:creator>
      <pubDate>Thu, 08 Oct 2026 04:29:28 +0000</pubDate>
      <link>https://dev.to/reordo/hiding-an-image-in-a-png-and-decoding-it-with-svg-filters-without-cors-pixel-access-5099</link>
      <guid>https://dev.to/reordo/hiding-an-image-in-a-png-and-decoding-it-with-svg-filters-without-cors-pixel-access-5099</guid>
      <description>&lt;p&gt;I wanted to see whether a browser could reveal an image hidden in another image even when the carrier was hosted on a site that did not allow cross-origin pixel reads. Drawing the carrier to a canvas is easy, but reading the canvas back is not: without the image host's CORS permission, the canvas becomes tainted. A server-side proxy would solve a different problem. I wanted the visitor's browser to fetch and display the original PNG directly.&lt;/p&gt;

&lt;p&gt;That experiment became &lt;a href="https://github.com/reordo/VisualNoiseDecoder" rel="noopener noreferrer"&gt;Visual Noise Decoder&lt;/a&gt;, a small, dependency-free set of three HTML pages: one to encode images, one to view a decoded image, and one to assemble a gallery. The interesting part is the viewer. It reconstructs the hidden pixels with SVG filters and CSS blending, while JavaScript never reads pixels from the remotely hosted carrier.&lt;/p&gt;

&lt;p&gt;Here is an encoded example. It looks like an image of Earth, but its low color bits carry an image of the Moon:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpj6fonzor89uouztzj4t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpj6fonzor89uouztzj4t.png" alt="An encoded PNG showing Earth" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://htmlpreview.github.io/?https://github.com/reordo/VisualNoiseDecoder/blob/main/image.html#url=https%3A%2F%2Fraw.githubusercontent.com%2Freordo%2FVisualNoiseDecoder%2Fmain%2Fexamples%2Fgallery-0-earth.vnd-0.png&amp;amp;seed=4-m88F7tBX13YQHZAU8wI8Rp3k7VG44S&amp;amp;index=0" rel="noopener noreferrer"&gt;Open the decoded Moon&lt;/a&gt;, or &lt;a href="https://htmlpreview.github.io/?https://github.com/reordo/VisualNoiseDecoder/blob/main/gallery.html#url=https%3A%2F%2Fraw.githubusercontent.com%2Freordo%2FVisualNoiseDecoder%2Fmain%2Fexamples%2Fgallery-0-earth.vnd-0.png&amp;amp;url=https%3A%2F%2Fraw.githubusercontent.com%2Freordo%2FVisualNoiseDecoder%2Fmain%2Fexamples%2Fgallery-1-hurricane.vnd-1.png&amp;amp;url=https%3A%2F%2Fraw.githubusercontent.com%2Freordo%2FVisualNoiseDecoder%2Fmain%2Fexamples%2Fgallery-2-clouds.vnd-2.png&amp;amp;seed=4-m88F7tBX13YQHZAU8wI8Rp3k7VG44S&amp;amp;index=0" rel="noopener noreferrer"&gt;open all three example images as a gallery&lt;/a&gt;. The example seed is public; this is a demonstration of the rendering technique, not a secret to protect.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why canvas cannot do the whole job
&lt;/h2&gt;

&lt;p&gt;The encoder runs on images that the user selects locally. It can use canvas normally: scale the secret, center-crop the cover, modify pixel bytes, and export a new PNG.&lt;/p&gt;

&lt;p&gt;The viewer has different constraints. A remote &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; or CSS background can be displayed without CORS permission. But if I draw that remote image on a canvas and call &lt;code&gt;getImageData()&lt;/code&gt; or &lt;code&gt;toDataURL()&lt;/code&gt;, the browser's origin-clean restriction prevents access. Applying an SVG filter first does not make that readback legal. The problem is not getting the image onto the screen; it is obtaining its pixel values in JavaScript.&lt;/p&gt;

&lt;p&gt;The solution here is to keep the carrier in the browser's rendering pipeline. The viewer uses it as a CSS background, extracts the required bits with SVG component-transfer filters, combines it with locally generated mask images, and lets the compositor assemble the result. It never exports the filtered carrier to canvas.&lt;/p&gt;

&lt;p&gt;This does not bypass CORS or an image host's access controls. If hotlink protection or a content policy prevents the image from loading, there is nothing to decode.&lt;/p&gt;

&lt;h2&gt;
  
  
  The PNG layout
&lt;/h2&gt;

&lt;p&gt;For a secret of &lt;code&gt;W × H&lt;/code&gt; pixels, the output cover is &lt;code&gt;2W × 2H&lt;/code&gt;. Think of the cover as four &lt;code&gt;W × H&lt;/code&gt; quadrants. One secret pixel maps to four cover pixels at the same relative position: one in each quadrant. Each secret pixel has eight bits per RGB channel; each of those four cover pixels carries two of those bits in its channel's two least significant bits. The upper six bits of each cover channel stay as they were.&lt;/p&gt;

&lt;p&gt;For one channel, the encoder's core operation is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;secretBits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;secretChannel&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;quadrant&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;encodedChannel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;coverChannel&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mi"&gt;252&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;secretBits&lt;/span&gt; &lt;span class="o"&gt;^&lt;/span&gt; &lt;span class="nx"&gt;maskBits&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;maskBits&lt;/code&gt; comes from a deterministic AES-CTR stream. The encoder and viewer generate it in the same way:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;material&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;DOMAIN&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\0&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\0&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;phrase&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;digest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subtle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SHA-256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextEncoder&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;material&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subtle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;importKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;raw&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;AES-CTR&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;encrypt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subtle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encrypt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;AES-CTR&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;counter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;128&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;byteLength&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;DOMAIN&lt;/code&gt; is the fixed string &lt;code&gt;VND-MAIN-HALF-2LSB-MASK-RGB&lt;/code&gt;. Encrypting zero bytes with a zero-initialized counter produces the mask stream; it does &lt;strong&gt;not&lt;/strong&gt; decrypt the remote PNG through Web Crypto. The image index matters for galleries: images encoded with the same seed must use different indices so that they do not reuse the same mask stream.&lt;/p&gt;

&lt;p&gt;The output must remain an &lt;em&gt;original PNG&lt;/em&gt;. Resizing, recompressing, or converting it can change the low bits and destroy the hidden image. A link to the image-hosting page or a thumbnail is not a substitute for a direct link to the encoded PNG.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reconstructing bits in the browser
&lt;/h2&gt;

&lt;p&gt;The viewer knows the PNG dimensions from an ordinary image load. It reproduces the mask stream locally, and it can safely generate mask PNGs on canvas because those pixels are its own. It creates eight mask planes: two bit planes for each of the four quadrants.&lt;/p&gt;

&lt;p&gt;For each plane, an SVG &lt;code&gt;feComponentTransfer&lt;/code&gt; with a discrete lookup table turns one selected low bit of a color channel into a binary value. The table is generated from &lt;code&gt;(value &amp;gt;&amp;gt; bit) &amp;amp; 1&lt;/code&gt; for all 256 input values. The remote carrier remains a CSS background behind that filter.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;values&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;256&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;planeBit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;component&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;discrete&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;component&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tableValues&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The filtered carrier plane and the corresponding mask plane are blended using CSS &lt;code&gt;difference&lt;/code&gt;. For binary values, that implements the required XOR and removes the mask generated above. The rest is &lt;em&gt;bit-plane compositing&lt;/em&gt;: extract a bit, XOR it with its mask bit, weight it by its position, then add the eight planes. CSS opacity supplies the weights, from &lt;code&gt;1/255&lt;/code&gt; to &lt;code&gt;128/255&lt;/code&gt;, and &lt;code&gt;plus-lighter&lt;/code&gt; blending combines the planes into an RGB image. The source quadrant and mask position are aligned with CSS background positioning.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.key&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;mix-blend-mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;difference&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.plane&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;mix-blend-mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;plus-lighter&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;plane&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;opacity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;planeIndex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;difference&lt;/code&gt; blend is between the carrier and mask &lt;em&gt;inside&lt;/em&gt; a plane; opacity weights that result before the planes are added together.&lt;/p&gt;

&lt;p&gt;It is an unusual use of the renderer, and it has a cost. The viewer needs eight filtered carrier planes and eight mask layers; it is not a single canvas draw call. Browser rendering and memory use become significant for large images. The gallery therefore mounts its image viewers progressively as they approach the viewport.&lt;/p&gt;

&lt;p&gt;There was also a Chromium rasterization problem when a heavily reduced decoded image returned from a hidden tab or a minimized window. The viewer refreshes its raster after visibility and scale changes so that the composited result does not turn into noise. That workaround is about rendering, not about re-decoding the PNG.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trying the project
&lt;/h2&gt;

&lt;p&gt;You can &lt;a href="https://htmlpreview.github.io/?https://github.com/reordo/VisualNoiseDecoder/blob/main/encode.html" rel="noopener noreferrer"&gt;open the encoder directly&lt;/a&gt; through HTMLPreview, or serve the repository directory locally with &lt;code&gt;python -m http.server 8080&lt;/code&gt; and open &lt;code&gt;encode.html&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The encoder accepts multiple secret and cover images, including file drops. Covers cycle in filename order across the secrets. Output can be saved as individual PNGs, into a chosen folder where supported, or as a ZIP archive. &lt;a href="https://htmlpreview.github.io/?https://github.com/reordo/VisualNoiseDecoder/blob/main/gallery.html" rel="noopener noreferrer"&gt;The gallery&lt;/a&gt; accepts one direct PNG URL per line, extracts PNG URLs from pasted embed code, or loads a ready-made set of URLs and a seed from its URL fragment. &lt;a href="https://htmlpreview.github.io/?https://github.com/reordo/VisualNoiseDecoder/blob/main/image.html" rel="noopener noreferrer"&gt;&lt;code&gt;image.html&lt;/code&gt;&lt;/a&gt; can also be placed inside an iframe; a square iframe with &lt;code&gt;border-radius: 100%&lt;/code&gt; and &lt;code&gt;pointer-events: none&lt;/code&gt; works as a non-interactive circular image.&lt;/p&gt;

&lt;p&gt;The repository includes three examples with different dimensions and aspect ratios: Moon inside Earth, Sun inside a hurricane view, and an Antarctic aurora inside cloud imagery. Their source images are credited to NASA Goddard in the README. NASA media are generally not subject to US copyright, though its &lt;a href="https://www.nasa.gov/nasa-brand-center/images-and-media/" rel="noopener noreferrer"&gt;media guidelines&lt;/a&gt; describe conditions and exceptions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits of the idea
&lt;/h2&gt;

&lt;p&gt;This is a research demonstration, not an audited encryption system. Reusing the same seed &lt;em&gt;and&lt;/em&gt; image index for two secrets repeats the mask; combining the encoded images can expose a relationship between the secrets. A weak seed phrase is also guessable, since SHA-256 is not a password-hardening function. There is no authentication or integrity check: a wrong seed, wrong index, or damaged PNG can simply produce noise. The visible cover dimensions and the modified low bits may reveal that data is present.&lt;/p&gt;

&lt;p&gt;A shared viewer URL includes the seed in its fragment. That fragment is not part of the HTTP request for the page, but it is visible to anyone who receives the full link and to scripts running on the viewer page. In particular, using HTMLPreview means trusting a third-party page with the demonstration seed. Do not put a private seed into a viewer you do not trust.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/reordo/VisualNoiseDecoder" rel="noopener noreferrer"&gt;source code and examples&lt;/a&gt; are available under BSD-2-Clause-Patent. I would be interested in feedback on the SVG/CSS reconstruction, browser compatibility, and whether there is a simpler CORS-independent rendering path that preserves the original PNG's low bits.&lt;/p&gt;

&lt;p&gt;Disclosure: This article was drafted with AI assistance. Its technical descriptions were checked against the project's source code and examples.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>css</category>
    </item>
    <item>
      <title>Booting a VeraCrypt-encrypted Windows VHD through Ventoy</title>
      <dc:creator>Keishin Senzaki</dc:creator>
      <pubDate>Sat, 03 Oct 2026 04:47:53 +0000</pubDate>
      <link>https://dev.to/reordo/booting-a-veracrypt-encrypted-windows-vhd-through-ventoy-317e</link>
      <guid>https://dev.to/reordo/booting-a-veracrypt-encrypted-windows-vhd-through-ventoy-317e</guid>
      <description>&lt;p&gt;I often use Ventoy as a multiboot USB drive: installer ISOs, live systems, recovery tools. Discovering its Windows VHD boot plugin suggested another use for it. Instead of giving every Windows installation its own set of partitions, I could put one in a VHD file, set it up in VirtualBox while another OS was running, and later boot that same installation directly on physical hardware through Ventoy. Replacing a system could then mean preparing a new file instead of repartitioning the drive.&lt;/p&gt;

&lt;p&gt;I also encrypt the systems and data I use with VeraCrypt. I wanted the flexibility of a VHD file and direct hardware boot without giving up system encryption. My first attempt was to boot an ordinary Windows VHD through Ventoy and turn on VeraCrypt system encryption there. The encryption wizard failed in that native-VHD setup. I then encrypted the Windows installation inside its UEFI VirtualBox VM. It booted in the VM, but Ventoy's VHD boot path could no longer start it.&lt;/p&gt;

&lt;p&gt;That led me to build &lt;a href="https://github.com/reordo/EncryptedVhdBoot" rel="noopener noreferrer"&gt;EncryptedVhdBoot&lt;/a&gt;, an experimental x64 UEFI bridge. It authenticates the encrypted volume, decrypts the VHD reads needed before Windows starts, and passes VeraCrypt's standard boot state to &lt;code&gt;veracrypt.sys&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The bridge hooks VHD parser callbacks in &lt;strong&gt;two&lt;/strong&gt; Windows boot applications, with a third hook at the point where Boot Manager starts Winload. The VHD and Microsoft's boot files stay unchanged on disk. Below is how those hooks fit into the boot path.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqhhs8xhht2t3fpyjzwdr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqhhs8xhht2t3fpyjzwdr.png" alt="EncryptedVhdBoot password prompt with masked input during a physical x64 UEFI boot" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The visible password prompt during a physical x64 UEFI boot. The entered characters are masked.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the normal boot paths do not meet
&lt;/h2&gt;

&lt;p&gt;Ventoy's Windows VHD boot image starts a Microsoft Boot Manager that knows how to open VHD files. For an unencrypted Windows VHD, Boot Manager reads the virtual disk, launches &lt;code&gt;winload.efi&lt;/code&gt;, and Windows continues.&lt;/p&gt;

&lt;p&gt;An encrypted VHD creates two separate obstacles. In the tested configuration, VeraCrypt 1.26.29 reports an error when &lt;strong&gt;System &amp;gt; Encrypt System Partition/Drive&lt;/strong&gt; is opened from Windows already running by native VHD boot, so the system has to be encrypted inside the VM. Once it is encrypted, Ventoy can still open the VHD file, but Boot Manager and Winload receive ciphertext from their VHD parsers before the Windows VeraCrypt driver can take over.&lt;/p&gt;

&lt;p&gt;Here is the path the bridge creates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Ventoy selects a Windows VHD
  -&amp;gt; EncryptedVhdBoot UEFI application
     -&amp;gt; original Windows Boot Manager
        -&amp;gt; VHD1 read callback: read ciphertext, return plaintext
        -&amp;gt; launch winload.efi
           -&amp;gt; its own VHD1 read callback: return plaintext
           -&amp;gt; Windows kernel and boot-start drivers
              -&amp;gt; veracrypt.sys takes over encrypted disk I/O
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The word &lt;em&gt;own&lt;/em&gt; matters: patching Boot Manager's VHD reader alone is insufficient. &lt;code&gt;winload.efi&lt;/code&gt; has a separate VHD1 parser and continues reading the disk after Boot Manager launches it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Intercepting virtual-disk reads
&lt;/h2&gt;

&lt;p&gt;The supported Ventoy input is the official &lt;strong&gt;Win10Based v3.0&lt;/strong&gt; &lt;code&gt;ventoy_vhdboot.img&lt;/code&gt;. Its UEFI boot image contains a specific 64-bit Windows Boot Manager build, version &lt;code&gt;10.0.10240.16384&lt;/code&gt;. Microsoft public symbols and the PE layout let me identify its &lt;code&gt;Vhd1Parser&lt;/code&gt; table. The bridge loads the original Boot Manager, locates the table at RVA &lt;code&gt;0x2CF8&lt;/code&gt;, validates all four original table entries (&lt;code&gt;Open&lt;/code&gt;, &lt;code&gt;Close&lt;/code&gt;, &lt;code&gt;Read&lt;/code&gt;, and &lt;code&gt;Write&lt;/code&gt;) against that build, then replaces the relocated read and write pointers in RAM:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;mOriginalVhdRead&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EVB_VHD_IO&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;mVhd1Parser&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;EVB_VHD_READ_INDEX&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="n"&gt;mOriginalVhdWrite&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EVB_VHD_IO&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;mVhd1Parser&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;EVB_VHD_WRITE_INDEX&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="n"&gt;mVhd1Parser&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;EVB_VHD_READ_INDEX&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UINTN&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;EvbVhdReadPassThrough&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;mVhd1Parser&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;EVB_VHD_WRITE_INDEX&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UINTN&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;EvbVhdWritePassThrough&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;MemoryFence&lt;/span&gt; &lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These are the actual callback assignments in &lt;a href="https://github.com/reordo/EncryptedVhdBoot/blob/main/EvbPkg/Application/EvbPreloader/EvbPreloader.c" rel="noopener noreferrer"&gt;EvbPreloader.c&lt;/a&gt;. The original callbacks remain available to the bridge; no VHD routine is copied or permanently rewritten.&lt;/p&gt;

&lt;p&gt;Why work at this level? The VHD parser has already translated file layout into offsets on the &lt;em&gt;virtual disk&lt;/em&gt;. A fixed VHD and a dynamic VHD store their bytes differently in the host file, but the parser presents the same disk-offset view to its callers. That gives the decryption code a useful boundary: it can reason about VeraCrypt sectors without reimplementing Ventoy's VHD file handling.&lt;/p&gt;

&lt;p&gt;The Boot Manager wrapper first calls the saved original &lt;code&gt;VhdRead&lt;/code&gt;. On its first successful read it authenticates the VeraCrypt header, or takes the plain-VHD path if the password is empty. For an encrypted read, &lt;a href="https://github.com/reordo/EncryptedVhdBoot/blob/main/EvbPkg/Application/EvbPreloader/EvbVeraCrypt.c" rel="noopener noreferrer"&gt;EvbVeraCryptDecryptRead&lt;/a&gt; limits the transformation to the encrypted region:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;IntersectStart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ByteOffset&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;mEncryptedStart&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;ByteOffset&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;mEncryptedStart&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;IntersectEnd&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ReadEnd&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;mEncryptedEnd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;ReadEnd&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;mEncryptedEnd&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IntersectStart&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;IntersectEnd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;EFI_SUCCESS&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An aligned interior can be decrypted in place, using the virtual-disk sector number as the XTS data-unit number. For an unaligned edge, the code rereads the whole sector through the original callback, decrypts it in a scratch buffer, and copies back only the requested span. This preserves bytes outside the encrypted area. Early writes to an encrypted VHD are rejected as write-protected. The bridge does not attempt to implement encrypted writes during the pre-kernel phase. With an empty password, it leaves the ordinary unencrypted-VHD read and write behavior in place.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding the second parser
&lt;/h2&gt;

&lt;p&gt;Boot Manager's parser is tied to one validated binary profile. For &lt;code&gt;winload.efi&lt;/code&gt;, a version-only allowlist would be brittle: Windows 10 and 11 ship many builds, and the VHD parser's address moves.&lt;/p&gt;

&lt;p&gt;Instead, the bridge patches Boot Manager's &lt;code&gt;ImgArchEfiStartBootApplication&lt;/code&gt; entry point at RVA &lt;code&gt;0x6B468&lt;/code&gt; after validating its expected 15-byte prologue. The patch redirects control to &lt;code&gt;EvbStartBootApplication&lt;/code&gt;. A trampoline preserves the overwritten instructions and lets that wrapper call the original function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;CopyMem&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;Trampoline&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;mBootApplicationHookOriginal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;EVB_BOOT_APP_HOOK_SIZE&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;EvbWriteAbsoluteJump&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;Trampoline&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;EVB_BOOT_APP_HOOK_SIZE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UINTN&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;mBootApplicationHookTarget&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;EVB_BOOT_APP_HOOK_SIZE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;EvbWriteAbsoluteJump&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;mBootApplicationHookTarget&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UINTN&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;EvbStartBootApplication&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The jump uses an indirect RIP-relative x64 encoding that preserves registers. This boundary is reached after Boot Manager has loaded and authenticated the child boot application, but before it starts running.&lt;/p&gt;

&lt;p&gt;At that point, the bridge scans the loaded Winload PE image for a VHD1 parser table. The scanner considers initialized, readable, non-executable sections. Its decisive checks include:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;Candidate&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;EVB_VHD_WRITE_INDEX&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt;
     &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Candidate&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;EVB_VHD_READ_INDEX&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;EVB_VHD_WRAPPER_SIZE&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
    &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;EvbMatchesVhdWrapperPair&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Candidate&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;EVB_VHD_READ_INDEX&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
    &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;EvbRangeInSection&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Candidate&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;EVB_VHD_OPEN_INDEX&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TRUE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
    &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;EvbRangeInSection&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Candidate&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;EVB_VHD_CLOSE_INDEX&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TRUE&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Match&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;Match&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Candidate&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The read and write wrappers must have the validated instruction shapes and sit 48 bytes apart; the &lt;code&gt;Open&lt;/code&gt; and &lt;code&gt;Close&lt;/code&gt; targets must be executable. A second candidate makes the result ambiguous, so the scan returns no match.&lt;/p&gt;

&lt;p&gt;Only after finding that unique table does the bridge replace Winload's own callbacks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;mOriginalWinloadVhdRead&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EVB_VHD_IO&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;Parser&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;EVB_VHD_READ_INDEX&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="n"&gt;mOriginalWinloadVhdWrite&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EVB_VHD_IO&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;Parser&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;EVB_VHD_WRITE_INDEX&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="n"&gt;Parser&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;EVB_VHD_READ_INDEX&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UINTN&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;EvbWinloadVhdRead&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;Parser&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;EVB_VHD_WRITE_INDEX&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UINTN&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;EvbWinloadVhdWrite&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;MemoryFence&lt;/span&gt; &lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Winload read wrapper calls its original callback and decrypts the returned sectors. If the encrypted path is active and a suitable parser cannot be identified unambiguously, the child launch is aborted. The bridge does not guess an address or let Winload continue reading ciphertext.&lt;/p&gt;

&lt;p&gt;This is still a compatibility technique, not a promise that every future Windows build will match. The project records the tested versions and the structural checks in its &lt;a href="https://github.com/reordo/EncryptedVhdBoot/blob/main/docs/architecture.md" rel="noopener noreferrer"&gt;architecture notes&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  From password to Windows driver
&lt;/h2&gt;

&lt;p&gt;On the first Boot Manager VHD read, the bridge can use the saved original callback to read the virtual disk's MBR and VeraCrypt system header. It asks for a password and an optional system PIM, then uses VeraCrypt-compatible header and crypto code to authenticate the volume. After header authentication, it checks that decrypting the first encrypted sector produces the expected NTFS signature before allowing the boot to proceed.&lt;/p&gt;

&lt;p&gt;The bridge also prepares VeraCrypt's standard &lt;code&gt;BootArguments&lt;/code&gt; and &lt;code&gt;BOOT_CRYPTO_HEADER&lt;/code&gt; handoff. The relevant part of &lt;a href="https://github.com/reordo/EncryptedVhdBoot/blob/main/EvbPkg/Application/EvbPreloader/EvbVeraCrypt.c" rel="noopener noreferrer"&gt;EvbPrepareBootParams&lt;/a&gt; shows what goes into that handoff:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;Args&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;CryptoInfoOffset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UINT16&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;OFFSET_OF&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EVB_BOOT_PARAMS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BootCryptoInfo&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;CopyMem&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;Args&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;BootPassword&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;VcPassword&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Args&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;BootPassword&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="n"&gt;Args&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;Flags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UINT32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;Pim&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;CopyMem&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;Args&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;BootDriveSignature&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="n"&gt;Mbr&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;EVB_MBR_SIGNATURE_OFFSET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="k"&gt;sizeof&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Args&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;BootDriveSignature&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The PIM uses the upper 16 bits of &lt;code&gt;BootArguments.Flags&lt;/code&gt;, matching VeraCrypt-DCS and the Windows driver. The full handoff includes lengths, CRCs, and crypto-header fields; the excerpt highlights the password, disk signature, and PIM. The boot-start &lt;code&gt;veracrypt.sys&lt;/code&gt; driver receives the state it needs when firmware services are gone.&lt;/p&gt;

&lt;p&gt;At &lt;code&gt;ExitBootServices&lt;/code&gt;, the bridge wipes its private expanded-key workspace and partial-sector buffer. It retains the handoff block needed by &lt;code&gt;veracrypt.sys&lt;/code&gt;; the driver later validates, consumes, and burns that block. Once Windows is running, normal encrypted-disk operation belongs to the VeraCrypt driver, not the UEFI bridge.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building and using the image
&lt;/h2&gt;

&lt;p&gt;The project builds a replacement &lt;code&gt;ventoy_vhdboot.img&lt;/code&gt; from the verified upstream Win10Based v3.0 image. The original Boot Manager remains in the image; the new UEFI entry point loads it, installs the in-memory hooks, and starts it. The builder also preserves an easy-to-miss detail of the original ISO: its BIOS and UEFI BCD paths share one physical extent. Ventoy patches one path in memory, and the UEFI path must see the same bytes. Making independent BCD copies breaks boot.&lt;/p&gt;

&lt;p&gt;For an encrypted system disk, the tested preparation path is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Install Windows in an x64 UEFI VirtualBox machine whose system disk is a fixed or dynamic VHD1.&lt;/li&gt;
&lt;li&gt;Complete VeraCrypt system encryption and its pretest &lt;strong&gt;inside the VM&lt;/strong&gt;. On Windows 11, fully remove BitLocker or device encryption first.&lt;/li&gt;
&lt;li&gt;Shut down the VM, keep a recoverable copy of the VHD, and put the VHD on the Ventoy data partition.&lt;/li&gt;
&lt;li&gt;Install the derived vhdboot image and select the VHD from Ventoy in x64 UEFI mode.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For Windows 11, open an elevated Command Prompt inside the VM before enabling VeraCrypt system encryption:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;manage-bde -off C:
manage-bde -status C:
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wait until &lt;code&gt;manage-bde -status&lt;/code&gt; shows &lt;strong&gt;Conversion Status: Fully Decrypted&lt;/strong&gt; and &lt;strong&gt;Percentage Encrypted: 0.0%&lt;/strong&gt;. &lt;code&gt;Protection Status: Protection Off&lt;/code&gt; alone does not mean decryption has finished.&lt;/p&gt;

&lt;p&gt;The repository's &lt;a href="https://github.com/reordo/EncryptedVhdBoot#readme" rel="noopener noreferrer"&gt;README&lt;/a&gt; has the exact build, install, restore, and preparation commands. An empty password takes the original unencrypted-VHD path, so the same image can still boot ordinary VHDs.&lt;/p&gt;

&lt;p&gt;There is also an optional &lt;code&gt;/ventoy/ventoy_vhdboot.silent&lt;/code&gt; marker file. When it is present beside &lt;code&gt;ventoy_vhdboot.img&lt;/code&gt;, the bridge clears its screen and accepts password and PIM input without showing its own prompts, characters, masking symbols, or status text. This affects only output from the bridge; Ventoy, firmware, and Windows can still display their own interfaces. It is a display option, not a claim of stronger encryption.&lt;/p&gt;

&lt;h2&gt;
  
  
  What has been tested, and what has not
&lt;/h2&gt;

&lt;p&gt;End-to-end boot and driver handoff have been tested with VeraCrypt 1.26.29 on Windows 10 LTSC 2019, 1909, 21H2, and 22H2, and Windows 11 25H2. The tests include fixed and dynamic VHD1 files, default and custom PIM values, and several cipher and PRF combinations. The &lt;a href="https://github.com/reordo/EncryptedVhdBoot#compatibility" rel="noopener noreferrer"&gt;compatibility matrix&lt;/a&gt; gives the exact Winload versions and results.&lt;/p&gt;

&lt;p&gt;The current release requires x64 UEFI with &lt;strong&gt;Secure Boot disabled&lt;/strong&gt; and the specified Ventoy vhdboot image. VHDX, differencing VHDs, Legacy BIOS, arbitrary Boot Manager builds, hidden systems, and rescue workflows are outside the supported scope. The project has &lt;strong&gt;not received an independent security audit&lt;/strong&gt;. Boot tests demonstrate compatibility in the listed setups; they do not certify the security of the bridge.&lt;/p&gt;

&lt;p&gt;The code and detailed design are available in the &lt;a href="https://github.com/reordo/EncryptedVhdBoot" rel="noopener noreferrer"&gt;repository&lt;/a&gt;. I would especially welcome review of the VeraCrypt driver handoff, the Winload parser validation, and the lifetime of sensitive boot state. This is an independent project and is not affiliated with or endorsed by Ventoy, VeraCrypt, or Microsoft.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: This article was drafted with AI assistance. Its technical descriptions were checked against the project's source code and documentation.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>windows</category>
      <category>security</category>
      <category>uefi</category>
    </item>
  </channel>
</rss>
